Friday 24 January 2014

Association Form

An association form is filled out by the user when the workflow is first added to the list, they can be built using either InfoPath or .NET. This Post is going to focus on the latter. Generally I avoid InfoPath, not because of deployment, but because I avoid investing my time into products that are on their way out.

Add a new Item to your Workflow, In the solution explorer select your workflow and add a new item to it. With your workflow selected hit (ctrl+shift+a) or

now with your "Add New Item" pane open select the "Workflow Association Form (Farm Solution only)" option.

Open up your workflow elements file, and take a look at the new AssociationURL attribute added to the workflow.

it should have a relative Url to the newly added form

<?xml version="1.0" encoding="utf-8" ?>

<!-- Customize the text in square brackets.
Remove brackets when filling in, e.g.
Name="[NAME]" ==> Name="MyWorkflow" -->

<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  <Workflow
     Name="Computer Access Workflow"
     Description="My SharePoint Workflow"
     Id="6582541c-eb11-4904-9d35-1394b463f158"
     CodeBesideClass="POC_WorkFlow.CA_Workflow.CA_Workflow"
     CodeBesideAssembly="$assemblyname$"
     AssociationUrl="_layouts/POC_WorkFlow/CA_Workflow/CA_AssociationForm.aspx">
    <Categories/>
    <MetaData>
      <AssociationCategories>List</AssociationCategories>
      <!-- Tags to specify InfoPath forms for the workflow; delete tags for forms that you do not have -->
      <!--<Association_FormURN>[URN FOR ASSOCIATION FORM]</Association_FormURN>
       <Instantiation_FormURN>[URN FOR INSTANTIATION FORM]</Instantiation_FormURN>
      <Task0_FormURN>[URN FOR TASK (type 0) FORM]</Task0_FormURN>
      <Task1_FormURN>[URN FOR TASK (type 1) FORM]</Task1_FormURN>-->
      <!-- Modification forms: create a unique guid for each modification form -->
      <!--<Modification_[UNIQUE GUID]_FormURN>[URN FOR MODIFICATION FORM]</Modification_[UNIQUE GUID]_FormURN>
      <Modification_[UNIQUE GUID]_Name>[NAME OF MODIFICATION TO BE DISPLAYED AS A LINK ON WORKFLOW STATUS PAGE</Modification_[UNIQUE GUID]_Name>
      -->
      <StatusPageUrl>_layouts/WrkStat.aspx</StatusPageUrl>
    </MetaData>
  </Workflow>
</Elements>

Now with that ready, if you deploy your Project and associated your workflow with a list you will get the following:

Pretty simple, but lets add some functionality to our form, open up the association forms aspx page. The form I'm working on requires specifying a primary and secondary AMANDA admin.

<%@ Assembly Name="$SharePoint.Project.AssemblyFullName$" %>
<%@ Assembly Name="Microsoft.Web.CommandUI, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Import Namespace="Microsoft.SharePoint" %>
<%@ Import Namespace="Microsoft.SharePoint.ApplicationPages" %>
<%@ Register Tagprefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register Tagprefix="Utilities" Namespace="Microsoft.SharePoint.Utilities" Assembly="Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register Tagprefix="asp" Namespace="System.Web.UI" Assembly="System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" %>

<%@ Page Language="C#"
    DynamicMasterPageFile="~masterurl/default.master"
    AutoEventWireup="true"
    Inherits="POC_WorkFlow.CA_Workflow.CA_AssociationForm"
    CodeBehind="CA_AssociationForm.aspx.cs" %>

<asp:Content ID="Main" ContentPlaceHolderID="PlaceHolderMain" runat="server">

    <div>
        <div style="width:300px; display:inline-block; margin-bottom:20px"">
            Primary Amanda Admin<br />
            <SharePoint:PeopleEditor id="PrimaryAmandaAdmin_PPL" runat="server" IsValid="true"
                AllowEmpty="false" Height="20px" Width="200px" AllowTypeIn="true" MultiSelect="false" />
        </div>
        <div style="width:300px; display:inline-block; margin-bottom:20px">
            Secondary Amanda Admin<br />
            <SharePoint:PeopleEditor id="SecondaryAmandaAdmin_PPL" runat="server" IsValid="true"
                AllowEmpty="false" Height="20px" Width="200px" AllowTypeIn="true" MultiSelect="false" />
        </div>
    </div>


    <div style ="margin-top:50px;">
        <asp:Button ID="AssociateWorkflow" runat="server" OnClick="AssociateWorkflow_Click" Text="Associate Workflow" />
   
        <asp:Button ID="Cancel" runat="server" Text="Cancel" OnClick="Cancel_Click" />
    </div>
</asp:Content>

<asp:Content ID="PageTitle" ContentPlaceHolderID="PlaceHolderPageTitle" runat="server">
    Computer Access Association Form
</asp:Content>

<asp:Content ID="PageTitleInTitleArea" runat="server" ContentPlaceHolderID="PlaceHolderPageTitleInTitleArea">
        Computer Access Association Form

</asp:Content>

I use the people editor to get the auto complete functionality, always helpful.

with the UI complete create a serializable class called AssociationData, now since we are going to use xml as our transportation format there is no need for the serializable attribute.

public class AssociationData
{
    public string PrimaryAmandaAdmin{ get; set; }
    public string SecondaryAmandaAdmin { get; set; }
       
    public AssociationData(){ }

    public AssociationData(string primaryAmandaAdmin)
    {
        this.PrimaryAmandaAdmin = primaryAmandaAdmin;
    }

    public AssociationData(string primaryAmandaAdmin, string secondaryAmandaAdmin)
        : this(primaryAmandaAdmin)
    {
        this.SecondaryAmandaAdmin = secondaryAmandaAdmin;
    } 
}

For a class to be deserializable it requires an empty constructor, with our association class complete, open up the codebehind for the association form. By default the codbehind will have a private function GetAssociationData(). This is where you are going to get the data from your form and return it as a serialized xml class. At that point, thank Microsoft for doing the hard part for you.

// This method is called when the user clicks the button to associate the workflow.
private string GetAssociationData()
{
    string xml = string.Empty;
    string PrimaryAmandaAdmin = string.Empty;
    string SecondaryAmandaAdmin = string.Empty;

    if(PrimaryAmandaAdmin_PPL.Accounts.Count > 0)
        PrimaryAmandaAdmin = PrimaryAmandaAdmin_PPL.Accounts[0].ToString();

    if(SecondaryAmandaAdmin_PPL.Accounts.Count > 0)
        SecondaryAmandaAdmin = SecondaryAmandaAdmin_PPL.Accounts[0].ToString();

    AssociationData data = string.IsNullOrEmpty(SecondaryAmandaAdmin) ?
        new AssociationData(PrimaryAmandaAdmin) : new AssociationData(PrimaryAmandaAdmin, SecondaryAmandaAdmin);
        
    XmlSerializer xmlSerializer = new XmlSerializer(typeof(AssociationData));

    using (StringWriter writer = new StringWriter())
    {
        xmlSerializer.Serialize(writer, data);
        xml =  writer.ToString();
    }
           
    // TODO: Return a string that contains the association data that will be passed to the workflow. Typically, this is in XML format.
    return xml;

}

Basically what we did was grabbed our Admin values from our form. we then created an instance of our AssociationData class, we then used the StringWriter with the Serializer to convert our AssociationData class into xml, if you want to really check it out put a breakpoint into your code after we populate the xml variable and take a look for yourself.

<?xml version="1.0" encoding="utf-16"?>
<AssociationData xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <PrimaryAmandaAdmin>DOMAIN\smithj</PrimaryAmandaAdmin>
  <SecondaryAmandaAdmin>DOMAIN\doej</SecondaryAmandaAdmin>

</AssociationData>

Now that we have our association form complete, we are successfully setting variables for our workflow at the time that we are attaching our workflow to a list. This lets us share variables between all workflows on said list. Ideal for variables that change once in a while, such as Contract agreements: ie Interest rates, or administrators, contacts, things of that nature. When they do change all you have to do is deactivate your workflow, then re-add it with the new association data.

In my previous post I talked about attaching a workflow from the event receiver, which is great for testing and even production if you want to minimize manual setup, but the problem is now that you have an association form the association values have to be set. To pull this off go back into your event receiver and pass your default values from code rather then your form. Let's do just that open up your event receiver and take a look.

public override void FeatureActivated(SPFeatureReceiverProperties properties)
{
    Guid workflowID = new Guid("6582541c-eb11-4904-9d35-1394b463f158");
    SPSite site =properties.Feature.Parent as SPSite;
    if (site != null)
    {
        SPWeb web = site.RootWeb;
        // OTB List for Tasks
        SPList taskList = web.Lists["Workflow Tasks"];

        // OTB Hidden List
        SPList historyList = web.Lists["Workflow History"];

        // List our workflow gets attached to
        SPList caList = web.Lists["List1"];

        SPWorkflowTemplate wfTemplate = web.WorkflowTemplates[workflowID];
        SPWorkflowAssociation wfAssociation =
            SPWorkflowAssociation.CreateListAssociation(wfTemplate, "Computer Access", taskList, historyList);

        // TODO add Association Data XML

        wfAssociation.AllowManual = true;
        wfAssociation.AutoStartCreate = true;
        wfAssociation.AutoStartChange = true;

        caList.WorkflowAssociations.Add(wfAssociation);
        wfAssociation.Enabled = true;
    }

}

now add the following line of code where the TODO is

wfAssociation.AssociationData = @"<?xml version='1.0' encoding='utf-16'?><AssociationData xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xmlns:xsd='http://www.w3.org/2001/XMLSchema'><PrimaryAmandaAdmin>DOMAIN\user1</PrimaryAmandaAdmin><SecondaryAmandaAdmin>DOMAIN\user2</SecondaryAmandaAdmin></AssociationData>";

notice that it's just the xml from before when we serialized our AssociationData class. Now a better approach would probably be to create the class using constructors and serialize just as you did before.