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!