SharePoint Developer Support Team Blog

The Official blog of the SharePoint Developer Support Team

September, 2012

  • HOW TO: Create a custom ASPX association and initiation forms for Visual Studio Workflow

    This post is a contribution from Himani Sharma, an engineer with the SharePoint Developer Support team.

    Requirement

    You want to use custom Association and Initiation forms with your Visual Studio workflows. InfoPath forms do not fulfill your requirement due to the limitation they pose in terms of controls available in browser enabled forms. Hence you’d like to use custom ASPX forms.

    Analysis

    Back in SharePoint 2007 with which the preferred way of development was Visual Studio 2008, we did not have any templates to create these custom ASPX forms. Hence the entire code had to be written from scratch. The amount of effort involved was huge, since custom association and initiation forms required us to handle the association and workflow management ourselves via code.

    This changed with the advent of Visual Studio 2010 (preferred IDE for SharePoint 2010). The new version comes with Visual studio templates for workflow Association and InfoPath Forms. Task Form templates are not available since those would be standard ASPX page with your custom controls and code behind. So, let’s see how we can create these ASPX forms using the Visual Studio 2010.

    Steps

    I assume you already know how to create a basic approval workflow in Visual Studio. If not please refer to the SharePoint Foundation SDK.

    1. Open Visual Studio 2010 and create a new Sequential workflow project for SharePoint 2010.

    2. Create a basic Task Approval workflow as shown below.

    image

    3. Once the basic workflow has been built and compiled, we can move to the next step of adding custom ASPX forms to it. So, select the workflow item in the project created and ‘Add New Item’.

    image

    4. Select ‘SharePoint’ > ‘2010’ from Installed Templates and scroll down to select ‘Workflow Association Form’.

    image

    5. Once the form gets added you’ll get the following view.

    image

    6. Open the .ASPX form markup and examine it. It will contain all the necessary SharePoint namespaces and master page references. The content placeholder – ‘PlaceHolderMain’ contains two buttons –‘AssociateWorkflow’ and ‘Cancel’. These have corresponding event handlers automatically added to the code behind file.

    7. Since this is an association form, add a few controls in the content placeholder – ‘PlaceHolderMain’ to get data from end user. My sample will obtain username to whom task would be assigned everytime the workflow creates a task; and gets data to set the due-date for the assigned task.

    NOTE: Ensure you add them above the button controls. I have added a ‘PeopleEditor’ and a ‘Datetime’ control to it.

    <table border="0">
      <tr>
        <td>
          Task Assigned to:
        </td>
        <td>
          <SharePoint:PeopleEditor AllowEmpty="false" ValidatorEnabled="true" ID="taskAssignedToPicker" runat="server" ShowCreateButtonInActiveDirectoryAccountCreationMode="true" SelectionSet="User" />
        </td>
      </tr>
      <tr>
        <td>
          Task Due Date:
        </td>
        <td>
          <SharePoint:DateTimeControl ID="taskDueDateControl" runat="server" DateOnly="true" IsRequiredField="true" />
        </td>
      </tr>
    </table>

    8. Let’s create a new class type to store the data entered by user. Ensure the class is marked as [Serializable].

    [Serializable]
    public class MyCustomAssociationData
    {
      private string _taskAssignedto = default(string);
      
      public string TaskAssignedTo
      {
        get { return _taskAssignedto; }
        set { _taskAssignedto = value; }
      }
     
      private DateTime _taskDuedate = default(DateTime);
     
      public DateTime TaskDuedate
      {
        get { return _taskDuedate; }
        set { _taskDuedate = value; }
      }
    }

    9. Create another class to serialize this data to XML.  This XML data would be later de-serialized by the workflow and read.

    public class SerializeAssociationData
    {
      //serailize data to XML
      public static string SerializeFormToXML(MyCustomAssociationData data)
      {
        XmlSerializer serializer = new XmlSerializer(typeof(MyCustomAssociationData));
        using (StringWriter writer = new StringWriter())
        {
          serializer.Serialize(writer, data);
          return writer.ToString();
        }
      }
    }

    NOTE: Either use same namespace as the workflow’s code behind file Or reference appropriately.

    10. Now, open the code behind file for this association form. If you examine it you’d see that the template has taken care of the entire workflow association management itself. All you need to do is to get the association data. Enter the following code in the function named – ‘GetAssociationData’ (available in the template by default). This method is called when the user clicks the button to associate the workflow or the association data is edited.

    // This method is called when the user clicks the button to associate the workflow.
     
    private string GetAssociationData()
    {
      // TODO: Return a string that contains the association data that will be passed to the workflow. 
      //Typically, this is in XML format.
      MyCustomAssociationData assocForm = new MyCustomAssociationData();
     
      PickerEntity pe = taskAssignedToPicker.ResolvedEntities[0] as PickerEntity;
      assocForm.TaskAssignedTo = pe.Key;
     
      if (!taskDueDateControl.IsDateEmpty)
        assocForm.TaskDuedate = taskDueDateControl.SelectedDate;
     
      return SerializeAssociationData.SerializeFormToXML(assocForm);
    }

    11. We are basically reading the form values, storing them in object of type ‘MyCustomAssociationData’ and then serializing the object to XML.

    12. Open the elements.xml file under the Workflow module. This is the workflow manifest.

    image

    13. You’d see that ‘AssociationUrl’ property is automatically set to the association form created. Note that it is being picked from Layouts folder because this is where the ASPX form will get deployed.

    <Workflow
         Name=" Workflow with ASPX Forms"
         Description="My SharePoint Workflow"
         Id="a79d998a-9c9d-417b-83ad-0ccff08fd789"
         CodeBesideClass="WorkflowASPXForms.Workflow1.Workflow1"
         CodeBesideAssembly="$assemblyname$" AssociationUrl="_layouts/WorkflowASPXForms/Workflow1/WorkflowAssociationForm1.aspx"
    >

    14. Now open the workflow code behind file. We would read the association data inside ‘OnWorkflowActivated’ event handler.

    15. Create two local variables to store the data read from association form. Create an object of type – ‘MyCustomAssociationData’ class created in step 8.

    #region Data coming from Association Form
        
    private MyCustomAssociationData assocData = default(MyCustomAssociationData);
    private string TaskAssignedTo = default(string);
    private DateTime TaskDuedate = default(DateTime);
     
    #endregion

    16. Inside the OnWorkflowActivated’ event handler, add following code.

    private void onWorkflowActivated1_Invoked(object sender, ExternalDataEventArgs e)
    {
      //deserialize association data
      //Deserialize the XML data.
      XmlSerializer serializer = new XmlSerializer(typeof(MyCustomAssociationData));
      XmlTextReader reader = new XmlTextReader(new StringReader(workflowProperties.AssociationData));
      assocData = serializer.Deserialize(reader) as MyCustomAssociationData;
      TaskAssignedTo = assocData.TaskAssignedTo;
      TaskDuedate = assocData.TaskDuedate;
    }

    17. The serialized Association Data is available through WorkflowProperties.AssociationData. This data is in XML format and hence de-serialized using the XMLSerializer and XMLTextReader class, into ‘MyCustomAssociationData’ object. Once done, we store it in local variables created in step 16.

    18. Next, open the feature.xml file by double clicking the feature name as shown below. This should open the properties in the properties window.

    image

    19. Add data to ReceiverAssembly and ReceiverClass properties.

    ReceiverAssembly -> Microsoft.Office.Workflow.Feature, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c
    ReceiverClass -> Microsoft.Office.Workflow.Feature.WorkflowFeatureReceiver

    20. This is all about using and consuming Association Data using ASPX forms.

    21. In order to use custom ASPX Initiation form, we follow the same drill.

    22. Select the workflow item in the project created and ‘Add New Item’. Note that you select the workflow module and not project module otherwise, you wouldn’t find Association and Initiation form templates.

    image

    23. Select ‘SharePoint’ > ‘2010’ from Installed Templates and scroll down to select ‘Workflow Initiation Form’.

    image

    24. Open the .ASPX form markup and examine it. It will contain all the necessary SharePoint namespaces and master page references. The content placeholder – ‘PlaceHolderMain’ contains two buttons –‘StartWorkflow’ and ‘Cancel’. These have corresponding event handlers automatically added to the code behind file.

    25. Since this is an Initiation form, add a few controls in the content placeholder – ‘PlaceHolderMain’ to get Initiation data from end user. My sample workflow’s job is to create sites. The initiation form will obtain site name and site description for the site to be created when a workflow is started (NOTE: this data is obtained for each new workflow instance).

    NOTE: Ensure you add them above the button controls. I have added two ‘textbox’ controls.

    <asp:Content ID="Main" ContentPlaceHolderID="PlaceHolderMain" runat="server">
      <table border="0">
        <tr>
          <td>
            Enter Site Title:
          </td>
          <td>
            <asp:TextBox ID="titleBox" runat="server"></asp:TextBox>
          </td>
        </tr>
        <tr>
          <td>
            Enter Site Description:
          </td>
          <td>
            <asp:TextBox ID="DescBox" runat="server"></asp:TextBox>
          </td>
        </tr>
      </table>

    26. Create two classes – one to store the initiation data and other one to serialize it to XML (in the same fashion as earlier).

    [Serializable]
    public class SiteInformationForm
    {
      private string _siteName = default(string);
      private string _siteDesc = default(string);
     
      public string SiteName
      {
        get { return this._siteName; }
        set { this._siteName = value; }
      }
     
      public string SiteDesc
      {
        get { return this._siteDesc; }
        set { this._siteDesc = value; }
      }
    }
     
    public class SerializeInitiationForm
    {
      public static string SerializeFormtoXML(SiteInformationForm siteinfoobj)
      {
        XmlSerializer serializer = new XmlSerializer(typeof(SiteInformationForm));
        using (StringWriter writer = new StringWriter())
        {
          serializer.Serialize(writer,siteinfoobj);
          return writer.ToString();
        }
      }
    }

    27. Now, open the code behind file for this Initiation form. If you examine it you’d see that the template has taken care of the entire workflow Initiation data management itself. All you need to do is to get the Initiation data from end user, store it and use it. Enter the following code in the function named – ‘GetInitiationData’ (available in the template by default). This method is called when the user clicks submit button on custom ASPX initiation form.

    private string GetInitiationData()
    {
      SiteInformationForm info = new SiteInformationForm();
      info.SiteName = titleBox.Text.ToString();
      info.SiteDesc = DescBox.Text.ToString();
     
      //serialize data to xml
      string convertedXML = SerializeInitiationForm.SerializeFormtoXML(info);
      return convertedXML;
    }

    28. We are basically reading the form values, storing them in object of type ‘SiteInformationForm’ and then serializing the object to XML.

    29. Open the elements.xml file under the Workflow module. This is the workflow manifest.

    image

    30. You’d see that ‘InstantiationUrl’ property is automatically set to the Initiation form created. Note that it is being picked from Layouts folder because this is where the ASPX form will get deployed.

    <Workflow
         Name="My Site Workflow"
         Description="My SharePoint Workflow"
         Id="fb6354e7-1e38-45ec-9537-c69e06cc972b"
         CodeBesideClass="SiteWorkflow.Workflow1.Workflow1"
         CodeBesideAssembly="$assemblyname$" 
         InstantiationUrl="_layouts/SiteWorkflow/Workflow1/SiteWFInitForm.aspx"
    >

    31. Now open the workflow code behind file. We would read the Initiation form data inside ‘OnWorkflowActivated’ event handler.

    32. Create two local variables to store the data read from Initiation form. Create an object of type – ‘SiteInformationForm’’ class created in step 8.

    #region Data coming from Initiation Form
     
    private SiteInformationForm siteinfo = default(SiteInformationForm);
    private string SiteName = default(string);
    private string SiteDesc = default(string);
     
    #endregion

    33. Inside the OnWorkflowActivated’ event handler, add following code.

    private void onWorkflowActivated1_Invoked(object sender, ExternalDataEventArgs e)
    {
      //Deserialize the XML data.
      XmlSerializer serializer = new XmlSerializer(typeof(SiteInformationForm));
      XmlTextReader reader = new XmlTextReader(new StringReader(workflowProperties.InitiationData));
      siteinfo = serializer.Deserialize(reader) as SiteInformationForm;
      SiteName = siteinfo.SiteName;
      SiteDesc = siteinfo.SiteDesc;
    }

    34. The serialized Initiation Data is available through WorkflowProperties.InitiationData. This data is in XML format and hence de-serialized using the XMLSerializer and XMLTextReader class, into ‘SiteInformationForm’ object. Once done, we store it in local variables created in step 30.

    35. Repeat step 19 if you’re using only a custom ASPX Initiation Form. Once done, compile and deploy the solution.

    Hope this walk-through was helpful!

  • HOW TO: Programmatically set/pass WorkflowContext from a Visual Studio workflow to a custom workflow action in SharePoint 2010

    This post is a contribution from Himani Sharma, an engineer with the SharePoint Developer Support team.

    Scenario:

    You have a workflow custom action that you’d like to be used in SharePoint 2010 workflow.  This activity uses Microsoft.SharePoint.WorkflowActions.WorkflowContext to obtain WorkflowContext and hence use the workflow instance specific properties like ItemId, ListId, TaskListGuid, WorkflowInstanceId etc.,

    Analysis:

    In order to use this workflow custom action in SharePoint designer workflow, we create a custom workflow actions file.  The WorkflowContext parameter expected by the custom action is passed like this:

    Excerpt from a sample file workflow actions file:

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

    But how do we pass the WorkflowContext from within a Visual Studio workflow?

    Here are the steps:

    1. This is how the workflow context is defined in the workflow custom action class.

    #region workflow context
     
    public static DependencyProperty __ContextProperty = DependencyProperty.Register("__Context", typeof(WorkflowContext), typeof(ESPTaskActivity));
     
    public WorkflowContext __Context
    {
      get { return (WorkflowContext)GetValue(__ContextProperty); }
      set { SetValue(__ContextProperty, value); }
    }
     
    #endregion

    2. This is how you can set or pass the value from a Visual Studio workflow that uses this workflow custom action.

    image

    a. Define a public variable within the workflow class file.

    public WorkflowContext wfContext { get; set; }

    b. Set the value from within OnWorkflowActivated event in the workflow class file. NOTE: As long as you’ve access to SPWorkflowActivationProperties, you can set it from anywhere within the workflow.

    private void onWorkflowActivated1_Invoked(object sender, ExternalDataEventArgs e)
    {
      //Initialize workflowcontext using SPWorkflowActivationProperties
      wfContext.Initialize(workflowProperties);
                
      //initialize task properties.
      CustomTaskActivity1.__Context = wfContext;
    }

    You are all set!  Hope this post was helpful.

  • HOW TO: Prevent site deletion with a custom event receiver

    This post is a contribution from Charls Tom Jacob, an engineer with the SharePoint Developer Support team.

    In this blog I will describe how to develop a custom site event receiver to prevent users from deleting a SharePoint site. By default, any user with the sufficient permission can delete a site. This custom solution can be implemented as an extra check to prevent users from knowingly or unknowingly delete a site.

    SharePoint provides event receivers at site collection, web, list, item, and field levels to handle various scenarios like create, delete, update etc. In our case, objective is to prevent a user from deleting a web.

    In order to achieve this, we need to build to components:

    1. A Web event receiver

    2. A Feature event receiver

    Web event receiver is to handle the “WebDeleting” event which is fired (like any other “ing” event) when users attempts to delete a site, and before the site is actually deleted.

    Feature event receiver is to control this functionality; when the feature is activated, it registers the web event receiver to the web in context, ultimately preventing users from deleting the site. Deactivating the feature removes the web event receiver from the web, thereby allowing a site to be deleted as usual.

    Now, let’s see how we can build this solution using Visual Studio 2010.  Start visual studio and create a new event receiver project. Select the event receiver settings as below:

    image

    Replace the WebDeleting event with the following code:

    /// <summary>
    /// A site is being deleted.
    /// </summary>
    public override void WebDeleting(SPWebEventProperties properties)
    {
        properties.Status = SPEventReceiverStatus.CancelWithError;
        properties.ErrorMessage = "This site cannot be deleted; Contact your administrator";
        properties.Cancel = true;
    }

    Above code does the job of preventing site deletion. The error message gets displayed in the error page displayed.

    Now, in order to turn on/off this functionality, we need to add a feature receiver. Right click on Feature1 in your project and add an event receiver.

    Modify the Feature activated and deactivating events as below to attach/detach the web event receiver created in the previous section.

    public override void FeatureActivated(SPFeatureReceiverProperties properties)
    {
        SPWeb web = properties.Feature.Parent as SPWeb;
        SPEventReceiverDefinition siteDeletingReceiver = web.EventReceivers.Add();
        siteDeletingReceiver.Class = "MySiteEventReceiver.EventReceiver1.EventReceiver1";
        siteDeletingReceiver.Assembly = "MySiteEventReceiver, Version=1.0.0.0, Culture=neutral, PublicKeyToken =3cb47bda8f8a66de";
        siteDeletingReceiver.SequenceNumber = 3000;
        siteDeletingReceiver.Type = SPEventReceiverType.SiteDeleting;
        siteDeletingReceiver.Update();
    }

    Modify the above code to have the correct assembly, class and public key token.

    public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
    {
        SPWeb web = properties.Feature.Parent as SPWeb;
        foreach (SPEventReceiverDefinition ev in web.EventReceivers)
        {
            if (ev.Name.Equals("EventReceiver1WebDeleting"))
            {
                ev.Delete();
                break;
            }
        }
    }

    Above code deletes the WebDeleting event receiver when the feature is activated. You can find the name of the event receiver in the element.xml part of the project.

    This is how the error page appears, when somebody attempts to delete a site.

    image

    Sample visual studio solution is available for download

    Hope you found this post helpful. Happy coding!!

  • HOW TO: Include symbols in rich text editor in MOSS 2007

    This post is a contribution from Jaishree Thiyagarajan, an engineer with the SharePoint Developer Support team.

    Note: The below walk-through is based off of a publishing site.

    We can include symbols in rich text editor in MOSS 2007 in just 3 easy steps.

    Step1

    Copy the image (symbol.jpg) to C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\TEMPLATE\LAYOUTS\Symbol (where *Symbol* is a new folder). [You can use any name for the image and the folder, but make sure to provide the correct path in Step3 below (line number 2 in the second code snippet below)].

    Step2

    Navigate to master page gallery.  Within editing menu folder, you can find RTE2ToolbarExtension.xml, update the file as shown below [ensure that you checkin and approve this file after the modification is done].

    <?xml version="1.0" encoding="utf-8" ?>
        <RTE2ToolbarExtensions>
        <RTE2ToolbarExtraButton id="symbolInsertion" src="RTESymbolInsertion.js"/>
    </RTE2ToolbarExtensions>

    Step3

    Create a javascript file named “RTESymbolInsertion.js” to C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\TEMPLATE\LAYOUTS\1033 and add the below script.

    Note:

    a. I have added 3 symbols to the array (line number 17 in the below code snippet).  You can add many more symbols to the array.  The first parameter is the “symbol” & the second is the “tooltip”.

    b. You can arrange the menu items in columns.  I have added the symbols in the first column (line number 23 in the below code snippet).  The first parameter for the function RTE_DD_GenerateMenuItemScriptHtml is the column number.

       1: RTE2_RegisterToolbarButton("symbolInsertion",
       2:                             "_layouts/symbol/symbol.jpg",
       3:                             "Symbols",
       4:                             "Insert symbols",
       5:                             SymButtonOnClick,
       6:                             SymButtonOnResetState,
       7:                             new Array());
       8:  
       9: function SymButtonOnClick(strBaseElementID, arguments) {
      10:  
      11:     var docEditor = RTE_GetEditorDocument(strBaseElementID);
      12:     if (docEditor == null) { return; }
      13:  
      14:     var selectedRange = docEditor.selection.createRange();
      15:  
      16:     //Array of symbols
      17:     var symbols = [['\u00A9', 'Copyright'], ['\&#151;', 'Emdash'], ['\&#174;', 'Rights']];
      18:     p = symbols.length;
      19:  
      20:     var sHTML = RTE_DD_GenerateMenuOpenHtml();
      21:  
      22:     for (i = 0; i < p; i++) {
      23:         sHTML = sHTML + RTE_DD_GenerateMenuItemScriptHtml("1", i, "var docEditor = RTE_GetEditorDocument('" + strBaseElementID + "'); var s=docEditor.selection.createRange();s.text='" + symbols[i][0] + "';RTE_DD_CloseMenu();", symbols[i][0], symbols[i][1], "", "", "");
      24:     }
      25:  
      26:     sHTML = sHTML + RTE_DD_GenerateMenuCloseHtml();
      27:     RTE_DD_OpenMenu(strBaseElementID, "symbolInsertion", sHTML, "1");
      28:     return true;
      29: }
      30:  
      31: // The method that is called when the button's state is reset.
      32: function SymButtonOnResetState(strBaseElementID, arguments) {
      33:  
      34:     var docEditor = RTE_GetEditorDocument(strBaseElementID);
      35:     if (docEditor == null) { return; }
      36:  
      37:     if (!RTE2_PopupMode(strBaseElementID)) {
      38:         RTE_RestoreSelection(strBaseElementID);
      39:  
      40:     }
      41:     if (!RTE2_IsSourceView(strBaseElementID)) {
      42:         RTE_TB_SetEnabledFromCondition(strBaseElementID, true, "symbolInsertion");
      43:         return true;
      44:     }
      45:     else {
      46:         RTE_TB_SetEnabledFromCondition(strBaseElementID, false, "symbolInsertion");
      47:     }
      48: }

    Once this is done, I can edit my publishing page (screen shots are from a collaboration site where publishing feature is enabled) and point to a content section I want to edit.  This will bring up the rich text editor.  I’ll be able to see the new menu strip and the symbols I added will be usable.

    image

    More Information

    Some details about the functions used.

    1. RTE2_RegisterToolbarButton : Used to register new Button to RTE

    2. SymButtonOnClick : This method will be called when you click the Symbol image/button

    3. SymButtonOnResetState : The method is called when the button's state is reset

    4. RTE_DD_GenerateMenuOpenHtml : This method will construct the opening tag for menu.

    5. RTE_DD_CloseMenu: This method will close the curent menu which is in open state

    6. RTE_DD_GenerateMenuItemScriptHtml: This method expects “menu html” as the parameter. This will dynamically constructs the menuhtml with all necessary  functions (such as onclick,onmouseover,onmouseout) included.

    7. RTE_DD_GenerateMenuCloseHtml : This method will construct the closing tag for menu

    8. RTE_GetEditorDocument : This method will return the textarea of the editor.

    9. RTE2_PopupMode : This method will return “true” when the editor is in popupmode

    10. RTE2_IsSourceView: This method will return “true” when the editor is in HtmlSourceView

    11. RTE_RestoreSelection: This method will restore the selected text in selection state.

    12. RTE_TB_SetEnabledFromCondition: Via this method we can either enable/disable a button in RTE.

    Hope this post was helpful.

  • SharePoint 2010 Environment can get corrupt when deploying and retracting a WSP solution that was created using “Save site as template” option

    This post is a contribution from Amy Luu, an engineer with the SharePoint Developer Support team.

    Steps to repro this problem:

    1. Create a new team site in SharePoint 2010 and save this site as template (ts.wsp).  Download the WSP to your local drive.

    2. Launch VS 2010 professional.

    3. In Visual Studio, File > New > Project, select “SharePoint” Template > “Import SharePoint Solution Package”.  Note: Make sure .NET Framework 3.5 is selected.

    image

    4. Clicking OK will get you to the following screen.  Provide a valid SharePoint site this solution will be deployed to and choose “Deploy as a sandboxed solution”.

    image

    5. Click “Next”, browse to the ts.wsp that was downloaded to your local drive.

    6. Click “Next”, then “Finish” to import all the contents from the WSP.

    7. Browse to your SharePoint site where the solution will be deployed to and click to view all the site content, notice the last modified time stamp.

    image

    8. From Visual Studio, simply build and deploy this solution [Build menu > Deploy solution].  Check view all site content, specifically the modified date.  Lists would have got recreated with new time stamps, existing data that were in the document library would be deleted.

    image

    9. If you retract the solution from the Visual Studio project, OOB site columns and content types will be deleted. You can view the site columns and Content types gallery, ALL of the OOB columns and content types got removed.

    Redeploy the WSP will add the content types and site columns back.

    Note of advice is not to do VS deployment directly on a site collection where there is working data even on a test/development environment unless you are sure about doing this.