Using OOB Workflows to Provision Sites

A large part of a governance strategy is having some control on how and where SharePoint sites get created. Traditionally people focus on whether or not to turn on Self Service Provisioning. Unfortunately, this setting is at a web application level and is like a big on or off switch. If you turn it on, your users could create site collections in the web application. The issue from a governance perspective is that the users can create any site, with any title, at any URL, with a selection of any installed template. Typically, you want more control of your environment.

The solution I put together allows an organization to create forms that provision sites and to use the out of the box workflows for gaining approval of the site requests. This is a big improvement because many examples out there walk you down the path of creating a custom workflow for every type of site provisioning process you want. In fact, some developers would probably find this article interesting just with the technique of adding a "twist" to the out of the box workflows.

The solution is based on recognizing that the organization would likely have several different forms that could possibly go through different workflow approval steps that could result in the creation of a site collection or subsite. The trick to this solution is that I really don't care what the workflow steps are; just that the form is approved. I will leverage a library's moderation feature so that forms that are saved there land with a status of Pending. You can turn this setting on any library as shown in the pic below;

 

Setting Content Approval to Yes will mean that every new form will first start with a status of Pending. Then the out of the box workflow support setting this status to approved upon successful completion. These next two pic show and OOB Approval workflow called Class Site Approval and the setting where you tell the workflow to use the Content Approval feature.

 

 

 

 

I am also assuming that each of the forms that represent site requests are published as content types. I happened to have used InfoPath forms and Forms Services, but that is by no means required. It could be a simple list. The benefit of using content types is that it is easy to detect and I will use the content type of the form to retrieve settings information about what type of site to provision. Here is my very simple form. Notice that there is a Class ID and Class Name fields within the form. My solution supports using form data for the Name, Url, and Description of the resulting site (more on that in a bit).

 

So where is all the custom code? There is no custom workflow. Instead, I use an Event Receiver on my forms library that acts when it sees an update to a form where its status is Approved. I used a custom feature to deploy the Event Receiver. This feature creates a SiteProvisioningForms form library, makes sure its content approval setting is turned on, and wires up the event receiver. It should also include the ProvisionSettings list, but I haven't gotten around to creating a template out of it yet. So before I show you the code of the event handler, lets first look at the feature. Here is the element manifest which creates the forms library and handles the event receiver.

<Elements xmlns="https://schemas.microsoft.com/sharepoint/">
  <ListInstance
     Id="30002"
     FeatureId="00BFEA71-1E1D-4562-B56A-F05371BB0115"
     Description="SiteProvisionForms"
     TemplateType="115"
     Title="SiteProvisionForms"
     OnQuickLaunch="TRUE"
     QuickLaunchUrl="SiteProvisionForms/AllItems.aspx"
     Url="SiteProvisionForms"
     />
  <Receivers ListTemplateId="115">
    <Receiver>
      <Name>SiteProvisioningEventHandler</Name>
      <Type>ItemUpdated</Type>
      <SequenceNumber>10000</SequenceNumber>
      <Assembly>SiteProvisioning, Version=1.0.0.0, Culture=neutral, PublicKeyToken=45a2811e9ad438d2</Assembly>
      <Class>SiteProvisioning.SiteProvisioningEventHandler</Class>
      <Data></Data>
      <Filter></Filter>
    </Receiver>
  </Receivers>
</Elements>

My feature also has a feature receiver that acts when the feature is activated. Mainly this ensures that the Content Approval setting is set correctly.

        public override void FeatureActivated(SPFeatureReceiverProperties properties)
        {
            //Find the created form library and turn on content approval
            //and allow content types
            SPWeb web = (SPWeb)properties.Feature.Parent;
            SPList formLibrary = web.Lists["SiteProvisionForms"];
            if (formLibrary != null)
            {
                formLibrary.EnableModeration = true;
                formLibrary.Update();
            }
          
        }

Now when the event receiver responds to a form update, it is going to look at a custom list I setup for provisioning settings. This one basically provides all the details for creating the site based on the content type of the form request. It details whether the site should be a site collection or a subsite. If it is a site collection, the managed path it should be under. The ParentSiteUrl column should be the web application URL for site collections or the parent site if it is a subweb. The SiteTemplate column details what type of site to create. The SiteNameField, SiteDescriptionField, and SiteUrlField columns tell the event receiver which form data fields to use. In this case I promoted these properties as part of the form publish action so that they were available as columns of the content type. Here is a screenshot:

 

 

Last, but not least, here is the code for my Event Receiver. It is only listening to forms in the SiteProvisioningForms library and for updates to the form. Yes the completion of the workflow triggers this event. It makes sure the form has been approved and then goes to work. The work runs within an ElevatedPrivileges block since the user may not have access to create sites themselves or even the settings list. The event receiver gets the content type of the request and looks in the settings list for corresponding information. After retrieving that, it pulls out the necessary form data. For site collections, I set the owner to be the user who submitted the form. It then creates either the site or site collection. Just a note that I am using the ItemUpdated event which is async so it may take a few seconds for your sites to appear.

        public override void ItemUpdated(SPItemEventProperties properties)
        {
            CheckApproval(properties);
            base.ItemUpdated(properties);
        }

        private void CheckApproval(SPItemEventProperties properties)
        {
            if (properties.ListTitle == "SiteProvisionForms" && properties.ListItem != null)
            {
                if (properties.ListItem.ModerationInformation.Status == SPModerationStatusType.Approved)
                {
                    SPSecurity.RunWithElevatedPrivileges(delegate()
                    {
                    //retrieve FormName
                    string formName = properties.AfterProperties["ContentType"].ToString();
                    //retrieve settings
                    SPWeb currentWeb = properties.OpenWeb();
                    SPList settingsList = currentWeb.Lists["ProvisionSettings"];
                    if (settingsList == null) return;
                    SPQuery query = new SPQuery();
                    query.Query = String.Format("<Where><Eq><FieldRef Name='Title'/><Value Type='Text'>{0}</Value></Eq></Where>",formName);
                    SPListItemCollection items = settingsList.GetItems(query);
                    string siteType;
                    string managedPath;
                    string parentSiteUrl;
                    string siteTemplate;
                    string siteNameField;
                    string siteDescriptionField;
                    string siteUrlField;
                    siteType = items[0]["SiteType"].ToString();
                    managedPath = items[0]["ManagedPath"].ToString();
                    parentSiteUrl = items[0]["ParentSiteUrl"].ToString();
                    siteTemplate = items[0]["SiteTemplate"].ToString();
                    siteNameField = items[0]["SiteNameField"].ToString();
                    siteDescriptionField = items[0]["SiteDescriptionField"].ToString();
                    siteUrlField = items[0]["SiteUrlField"].ToString();
                   
                    //retrieve promoted fields for site name, description url
                    string siteName;
                    string siteDescription;
                    string siteUrl;
                    siteName = properties.ListItem[siteNameField].ToString();
                    siteDescription = properties.ListItem[siteDescriptionField].ToString();
                    siteUrl = properties.ListItem[siteUrlField].ToString();
                    //capture owner
                    Contact contact = Contact.FromSharePointUserString(properties.ListItem["Created By"].ToString(), currentWeb);
                    currentWeb.Close();

                    //create the site
                    if (siteType == "Site Collection")
                    {
                        SPSite currentSite = currentWeb.Site;
                        SPWebApplication webApp = currentSite.WebApplication;
                        SPSiteCollection sites = webApp.Sites;
                        string url = parentSiteUrl + managedPath + siteUrl;
                        string ownerName = contact.DisplayName;
                        string ownerEmail = contact.EmailAddress;
                        string ownerAccount = contact.LoginName;
                        SPSite newSite = sites.Add(url, siteName, siteDescription, 1033, siteTemplate, ownerAccount, ownerName, ownerEmail);
                    }
                    else
                    {
                        SPSite parentSite = new SPSite(parentSiteUrl);
                        parentSite.AllWebs.Add(siteUrl, siteName, siteDescription, 1033, siteTemplate, true, false);
                    }
                    });
                }
            }
        }

There is obviously some room for improvement, but I've gotten such positive feedback on this approach, I wanted to share sooner rather than later.