Monday, July 8, 2013

Custom Workflow Action to Create a Site

One most commonly requested automated process is the creation of a site. For example: A team needs 28 sites using a template "A" for a project. I am sure this request will give the site owner or the SharePoint team a Heart attack!
Anyway, Just to add some spice to my blog, I am going to pen down an example scenario I recently worked on which required the automated process of creating sites. (This will help me too in my future if I have to deal with similar projects)
My company prefers to maintain separate sites for each Request for Proposal, but of course each request for a new site needs to be approved by the site owner (assuming SharePoint team is not involved as no customizations are needed). So..... 

1. When a RFP is received, the team responsible for handling it submits a request to create a site with the corresponding RFP name (or any other criteria).
To achieve this, I built an Info Path form that collects all required information about the site from the user.
2. Upon submission, an approval workflow kicks off on the list item (can be stored in a form library too) and the respective Site Owner is notified and assigned a task via email. (I am sure everyone knows how this looks and works)
3. When this request is approved, the workflow will create a site using the details entered in the form. This is achieved by developing (Coding arghhhh... ) a custom workflow action which uses the overload method of SPWebCollection.Add.

Frankly, to an extent this still sounds a bit manual to me as it is one form submission per site request, which  I suppose is a hassle for the users. But looking at it from the maintenance side; this is a great way to keep track of each new site being built anywhere in the agency by anyone. All info about these requests and builds are stored in the data list
 (Does that sound too complicated?! Feel free to post any workarounds :) )

Another simple scenario I found (This may be a more common one) Programmatically creating a SharePoint Site based on user input

Notes on the activity:
1. This activity is used to create a site.
2. This activity can span across site collections.
3. This activity (if set to yes) will inherit permissions from parent.
4. This activity (if set to yes) will inherit global navigation from parent.

5. This activity does not create the site if a predefined template is not specified. If a user requests a site to be built off of a new template, the workflow stops and notifies our team.

Initially I included the code for creating security groups and adding users to the security groups automatically as a part of this solution, but I felt it would make more sense in having a separate activity as the functionality may be used in other scenarios too.
Please see post:
 
Ok!! so not wasting any more time, lets get to work.

Please refer to my post on Custom workflow activity to move a file from one site to another and follow the same steps on how to create a visual studio solution.
Follow each step from 1-7.
(Name your project appropriately)

1. When you reach step 7, add the following code to your (ActivityName).cs file




using System;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Collections;
using System.Linq;
using System.Workflow.ComponentModel.Compiler;
using System.Workflow.ComponentModel.Serialization;
using System.Workflow.ComponentModel;
using System.Workflow.ComponentModel.Design;
using System.Workflow.Runtime;
using System.Workflow.Activities;
using System.Workflow.Activities.Rules;
using Microsoft.SharePoint;
using Microsoft.SharePoint.WorkflowActions;
using Microsoft.SharePoint.Utilities;
using Microsoft.SharePoint.Workflow;
using Microsoft.SharePoint.Navigation;
using System.Threading;

namespace CustomWorkflowActivities
{
    public partial class CreateSite : SequenceActivity
    {
        public CreateSite()
        {
            InitializeComponent();
        }
        //Current workflow Context

        public WorkflowContext __Context
        {
            get { return (WorkflowContext)GetValue(__ContextProperty); }
            set { SetValue(__ContextProperty, value); }
        }

        public static readonly DependencyProperty __ContextProperty =

            DependencyProperty.Register("__Context", typeof(WorkflowContext), typeof(CreateSite));
       
        //Parent web Url
 
        public static readonly DependencyProperty parentWebUrlProperty =
            DependencyProperty.Register("parentWebUrl", typeof(string), typeof(CreateSite));
        [DescriptionAttribute("A string that contains the URL of the Parent web under which new web is created")]
        [BrowsableAttribute(true)]
        [DesignerSerializationVisibilityAttribute(DesignerSerializationVisibility.Visible)]
        [ValidationOption(ValidationOption.Optional)]

        public string parentWebUrl
        {
            get { return (string)GetValue(parentWebUrlProperty); }
            set { SetValue(parentWebUrlProperty, value); }
        }

        //New Web Url
        public static readonly DependencyProperty newWebUrlProperty =
            DependencyProperty.Register("newWebUrl", typeof(string), typeof(CreateSite));
        [DescriptionAttribute("A string that contains the new website URL relative to the root website in the site collection. For example, to create a website at 'http://MyServer/sites/MySiteCollection/MyNewWebsite', specify MyNewWebsite")]
        [BrowsableAttribute(true)]
        [DesignerSerializationVisibilityAttribute(DesignerSerializationVisibility.Visible)]
        [ValidationOption(ValidationOption.Optional)]
        public string newWebUrl
        {

            get { return (string)GetValue(newWebUrlProperty); }

            set { SetValue(newWebUrlProperty, value); }
        }

        //Title
        public static readonly DependencyProperty strTitleProperty =
            DependencyProperty.Register("Title", typeof(string), typeof(CreateSite));
        [DescriptionAttribute("A string that contains the Title of the site to be created")]
        [BrowsableAttribute(true)]
        [DesignerSerializationVisibilityAttribute(DesignerSerializationVisibility.Visible)]
        [ValidationOption(ValidationOption.Optional)]

        public string strTitle
        {
           get { return (string)GetValue(TitleProperty); }
           set { SetValue(TitleProperty, value); }
        }

        //Description
       public static readonly DependencyProperty strDescriptionProperty =
            DependencyProperty.Register("strDescription", typeof(string), typeof(CreateSite));
        [DescriptionAttribute("A string that contains the description of the site to be created")]
        [BrowsableAttribute(true)]
        [DesignerSerializationVisibilityAttribute(DesignerSerializationVisibility.Visible)]
        [ValidationOption(ValidationOption.Optional)]

        public string strDescription
        {
            get { return (string)GetValue(strDescriptionProperty); }
            set { SetValue(strDescriptionProperty, value); }
        }

        //Web Template
       public static readonly DependencyProperty webTemplateProperty =
          DependencyProperty.Register("webTemplate", typeof(string), typeof(CreateSite));
        [DescriptionAttribute("A string that contains the name of template based on which the new site is created")]
        [BrowsableAttribute(true)]
        [DesignerSerializationVisibilityAttribute(DesignerSerializationVisibility.Visible)]
        [ValidationOption(ValidationOption.Optional)]
        public string webTemplate
        {
            get { return (string)GetValue(webTemplateProperty); }
            set { SetValue(webTemplateProperty, value); }
        }

        //Permissions
        public static readonly DependencyProperty useUniquePermissionsProperty =
           DependencyProperty.Register("useUniquePermissions", typeof(bool), typeof(CreateSite));
        [DescriptionAttribute("True indicates the new web will 'NOT' inherit permissions from the parent site;False indicates the new web will inherit permissions from the parent site")]
        [BrowsableAttribute(true)]
        [DesignerSerializationVisibilityAttribute(DesignerSerializationVisibility.Visible)]
        [ValidationOption(ValidationOption.Optional)]
        public bool useUniquePermissions
        {
            get { return (bool)GetValue(useUniquePermissionsProperty); }
            set { SetValue(useUniquePermissionsProperty, value); }
        }

        //Top Link Bar
        public static readonly DependencyProperty InheritTopLinkBarProperty =
          DependencyProperty.Register("InheritTopLinkBar", typeof(bool), typeof(CreateSite));
        [DescriptionAttribute("true to inherit Global navigation from parent site; otherwise, false")]
        [BrowsableAttribute(true)]
        [DesignerSerializationVisibilityAttribute(DesignerSerializationVisibility.Visible)]
        [ValidationOption(ValidationOption.Optional)]
        public bool InheritTopLinkBar
        {
            get { return (bool)GetValue(InheritTopLinkBarProperty); }
            set { SetValue(InheritTopLinkBarProperty, value); }
        }

        protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext)
        {
           CreateSiteMethod();
            return ActivityExecutionStatus.Closed;
        }

        private void CreateSiteMethod()
        {
            //Open Parent Site Collection
            using (SPSite parentSite = new SPSite(parentWebUrl))
            {
                //Open Parent Web
                using (SPWeb parentWeb = parentSite.OpenWeb())
                {
                    //Get the current Web on which workflow is running
                    using (SPWeb currentWeb = __Context.Web)
                    {
                       try
                        {
                            SPWeb newWeb = null;
                            string strWebUrl = null;
                            parentSite.AllowUnsafeUpdates = true;

                            //If Site Description is not entered by user, set it to blank
                            if (strDescription == null)
                                strDescription = "";

                            //If no template is selected, set default =  Team Site(STS#0)
                            if (String.IsNullOrEmpty(webTemplate))
                                webTemplate = "STS#0";

                            // Declare Locale ID : A 32-bit unsigned integer that specifies the locale ID. You can also specify 1033.
                            uint nLCID = parentWeb.Language;

                            //This step is required only to avoid any human error (Optional)
                            //If the URL entered by a user is a well formatted URL; get the last segment which represents the new web URL only
                            if (Uri.IsWellFormedUriString(newWebUrl, UriKind.Absolute))
                            {
                                Uri uri = new Uri(newWebUrl);
                               strWebUrl = uri.Segments.Last();
                            }
                            else //If a user enters a single word which represents the new web URL segment
                            {
                                strWebUrl = newWebUrl;
                            }

                            try
                            {
                               //Create a new site using overload method SPWebCollection.Add(String, String, String, UInt32, String, Boolean, Boolean)
                                newWeb = parentWeb.Webs.Add(strWebUrl, strTitle, strDescription, nLCID, webTemplate, useUniquePermissions, false);

                                //Create a workflow history event
                            SPWorkflow.CreateHistoryEvent(currentWeb, __Context.WorkflowInstanceId, 0,
                             currentWeb.CurrentUser, TimeSpan.Zero, "Success",
                             "Message: " + "Site created successfully", string.Empty);
                            }
                           catch (ArgumentException) { }

                           if (newWeb != null)
                            {
                                if (InheritTopLinkBar == true) //Optional
                                {
                                   // Set the new web's top link bar to inherit links from its parent web.
                                   newWeb.Navigation.UseShared = true;

                                    // Create a link pointing to the new web.
                                    SPNavigationNode node = new SPNavigationNode(newWeb.Title, newWeb.ServerRelativeUrl);

                                    // Find out if the parent inherits .
                                    bool parentInheritsTopNav = newWeb.ParentWeb.Navigation.UseShared;
                                    if (!parentInheritsTopNav)
                                    {
                                        // Add the link to the top link bar on the parent web.
                                        newWeb.ParentWeb.Navigation.TopNavigationBar.AddAsLast(node);
                                    }
                                }
                            }

//Optional (as per user requirement)
 

if (useUniquePermissions == true)
{
  try
     {
       newWeb.AllowUnsafeUpdates = true;

     //Set the name of each group to be added
     String owners = strTitle + " Owners";
     String members = strTitle + " Members";
     String visitors = strTitle + " Visitors";


      //Create Groups in the new Web                                       
newWeb.SiteGroups.Add(owners, parentWeb.AssociatedOwnerGroup, null, "This group has full control to" + strTitle + "site");
newWeb.SiteGroups.Add(members, parentWeb.AssociatedOwnerGroup, null, "This group has contribute access to" + strTitle + "site");
newWeb.SiteGroups.Add(visitors, parentWeb.AssociatedOwnerGroup, null, "This group has read access to" + strTitle + "site");

 //Get an instance of each new group created
SPGroup ownersGroup = newWeb.SiteGroups[owners];
SPGroup membersGroup = newWeb.SiteGroups[members];
SPGroup visitorsGroup = newWeb.SiteGroups[visitors];

  //Add role assignment to each new group created
SPRoleAssignment ownersRoleAssignment = new SPRoleAssignment(ownersGroup);
SPRoleAssignment membersRoleAssignment = new SPRoleAssignment(membersGroup);
SPRoleAssignment visitorsRoleAssignment = new SPRoleAssignment(visitorsGroup);

  //Get role definitions defined in new Web
 SPRoleDefinition contributeRoleDefinition = newWeb.RoleDefinitions["Contribute"];
 SPRoleDefinition readRoleDefinition = newWeb.RoleDefinitions["Read"];
 SPRoleDefinition fullControlRoleDefinition = newWeb.RoleDefinitions["Full Control"];

 //Add Role Definition Bindings to the groups
ownersRoleAssignment.RoleDefinitionBindings.Add(fullControlRoleDefinition);
membersRoleAssignment.RoleDefinitionBindings.Add(contributeRoleDefinition);                                visitorsRoleAssignment.RoleDefinitionBindings.Add(readRoleDefinition);
                                        
//Add role assignments to new web
newWeb.RoleAssignments.Add(ownersRoleAssignment);
newWeb.RoleAssignments.Add(membersRoleAssignment);                                       newWeb.RoleAssignments.Add(visitorsRoleAssignment);
                                                                                

SPWorkflow.CreateHistoryEvent(currentWeb, __Context.WorkflowInstanceId, 0,
                                          currentWeb.CurrentUser, TimeSpan.Zero, "Success", "Message: Security Groups created successfully", string.Empty);

//assign values to the output variable. Purpose: Can be used in the workflow
this.memberGroupNameOutput = membersGroupName;                                        this.visitorGroupNameOuput = visitorsGroupName;
   }
     catch (ArgumentException) {}
}

                            parentSite.AllowUnsafeUpdates = false;
                            newWeb.Dispose();
                            currentWeb.Dispose();
                            parentWeb.Dispose();
                            parentSite.Dispose();

                        }

                        catch (Exception ex)
                        {

                            string Message = ex.Message;
                            SPWorkflow.CreateHistoryEvent(currentWeb, __Context.WorkflowInstanceId, 0,
                              currentWeb.CurrentUser, TimeSpan.Zero, "Error",
                              "Message: " + Message, string.Empty);
                            throw ex;

                        }

                    }

                }

            }

        }
    }
}


2. Follow steps 8-9
At step 10, paste this code into .actions files
<?xml version="1.0" encoding="utf-8" ?>
<WorkflowInfo>
  <Actions Sequential="then" Parallel="and">
    <Action Name="Create site"
    ClassName="CreateSiteAction.CreateSite"
    Assembly="CreateSiteAction, Version=1.0.0.0, Culture=neutral, PublicKeyToken=/*Public Key Token of your project*/"
    AppliesTo="all"
    UsesCurrentItem="true"
    Category="Custom Workflow Actions">
      <RuleDesigner Sentence="Create Site: Title:%1, Description:%2, URL:%3 Parent Site: %4 Template %5; Use Unique Permissions: %6, Inherit Top Link Bar: %7
        <FieldBind Field="strTitle" Text="Title" Id="1" DesignerType="TextArea" />
     <FieldBind Field="strDescription" Text="Description" Id="2" DesignerType="TextArea" />
        <FieldBind Field="newWebUrl" Text="URL" Id="3" DesignerType="TextArea" />
        <FieldBind Field="parentWebUrl" Text="Parent Site URL. Must be in the form of http://ServerName/(SiteName)" Id="4" DesignerType="TextArea" />
        <FieldBind Field="webTemplate" Text="Template" Id="5" DesignerType="TextArea" />
        <FieldBind Field="useUniquePermissions" Text="Permissions" Id="6" DesignerType="TextArea" />
        <FieldBind Field="InheritTopLinkBar" Text="Inherit Top Link Bar" Id="7" DesignerType="System.Boolean" />

      </RuleDesigner>
      <Parameters>
        <Parameter Name="__Context" Type="Microsoft.SharePoint.WorkflowActions.WorkflowContext" Direction="In" />

        <Parameter Name="newWebUrl" Type="System.String, mscorlib" Direction="In" />
        <Parameter Name="parentWebUrl" Type="System.String, mscorlib" Direction="In" />
        <Parameter Name="strTitle" Type="System.String, mscorlib" Direction="In" />
    <Parameter Name="strDescription" Type="System.String, mscorlib" Direction="Optional" />
        <Parameter Name="webTemplate" Type="System.String, mscorlib" Direction="Optional" />
        <Parameter Name="useUniquePermissions" Type="System.String, mscorlib" Direction="In" />
        <Parameter Name="InheritTopLinkBar" Type="System.Boolean, mscorlib" Direction="In" />
      </Parameters>
    </Action>

3. Follow steps 11 through 18.

Errors:
1. Sometimes, while updating and redeploying the solution, designer will throw an error when you add the action to workflow. Error spells something like "The _____ property does not exist in the context" (I don't remember the accurate sentence)
Solution: Retract, Clean, Build and redeploy the solution.

References:

1. http://google.com
2. http://msdn.microsoft.com/en-us/library/ms412285.aspx
3. http://ankurmadaan.blogspot.com/2012/03/creating-custom-workflow-activity-using.html
4. http://blogs.planetcloud.co.uk/mygreatdiscovery/post/SharePoint-Custom-Workflow-to-create-a-site.aspx




No comments:

Post a Comment