Infopath: Create a pseudo dialog box

In some of my projects I need to display a confirmation dialog box when a user performs an action. For example, I have a form that allows a user uploads an attachment and the file will be stored in a document library instead of embedded in the form, something quite similar to the one shown on this blog. But I also want to allow the user to remove the attachment by clicking a remove button that will remove the file from the respective document library. So I want to display a confirmation box each time the remove button clicked, otherwise user might accidently remove the attachment.

Unfortunately, Infopath web based form doesn’t support confirmation action on a button, so this post will show a simple trick how to display a confirmation message.

The top section is in my case the URL section (it can be anything). In my project the URL will store the location of the uploaded document in the document library. The bottom section is the pseudo confirmation dialog box (I called it dialog section) which by default is hidden. The dialog section has a boolean attribute called ShowDlg. By default the value is 0 (false), when ShowDlg = 1, the dialog section will be shown and the top section is hidden. The remove button on the right has an Action rule that set the value of the ShowDlg. The Ok and Cancel buttons in the dialog sections will hide the dialog section, additionaly the Ok button can perform some code behind operation such as in my case removing the file in the document library specified by the URL section. Below is the complete rule of the implementation:
Remove button:
   set field @ShowDlg = 1

Ok Button
  set field @ShowDlg = 0
  -- do code behind thing if required

Cancel Button
  set field @ShowDlg = 0

Dialog Section
  Formatting:
     Hide this control when @ShowDlg = 0
Url Section
  Formatting
     Hide this control when @ShowDlg = 1

SharePoint 2010 dynamic approvers for OOB Approval Workflow part 2

This Article Series:

  1. SharePoint 2010 dynamic approvers for OOB Approval Workflow part 1
  2. SharePoint 2010 dynamic approvers for OOB Approval Workflow part 2

In previous post in this series, I have explained the 2 approaches to have dynamics approvers for OOB Approval workflow. Responding to on a quite number of requests, in this post I’ll show some sample codes how to accomplish them. Let’s assume we have an one stage, serial approval workflow and we maintain its approvers in a list’s multivalues user field called Approvers as shown below:

The approval workflow would get the approvers from the field’s values. The first thing that we need to have is a routine to convert the SPFieldUserValueCollection into the Approver xml, the xml format is used by the OOB Approval Workflow I explained in my previous post in this series. The end result of the approval xml for this example would be as shown below. As seen in the xml that there would be 3 approvers, the first 2 are SharePoint Groups and the last approver is a user.

 <d:Assignment xmlns:d="http:=//schemas.microsoft.com/office/infopath/2009/WSSList/dataFields">
   <d:Assignee>
     <pc:Person xmlns:pc="http:=//schemas.microsoft.com/office/infopath/2007/PartnerControls">
       <pc:AccountId>Approvers 1</pc:AccountId>
       <pc:AccountType>SharePointGroup</pc:AccountType>
     </pc:Person>
     <pc:Person xmlns:pc="http:=//schemas.microsoft.com/office/infopath/2007/PartnerControls">
       <pc:DisplayName>Approvers 2</pc:DisplayName>
       <pc:AccountId>Approvers 2</pc:AccountId>
       <pc:AccountType>SharePointGroup</pc:AccountType>
     </pc:Person>
     <pc:Person xmlns:pc="http:=//schemas.microsoft.com/office/infopath/2007/PartnerControls">
      <pc:DisplayName>Daniel Laksana</pc:DisplayName>
      <pc:AccountId>AUSDOM\aulaksanada</pc:AccountId>        
      <pc:AccountType>User</pc:AccountType>
     </pc:Person>
   </d:Assignee>
   <d:Stage>0</d:Stage>   
  <d:AssignmentType>Serial</d:AssignmentType>  
</d:Assignment>

Firstly, I created a constant class that contains all the xml templates used in the code as shown below. The first template is for the assignment element, the second template is for each person element. This approach might not be sufficient if we more than 1 stages.

class Constant
{
  public const string WorkflowAssignmentTemplate = @"
            <d:Assignment xmlns:d=""http://schemas.microsoft.com/office/infopath/2009/WSSList/dataFields"">
                 <d:Assignee>{0}</d:Assignee><d:Stage>0</d:Stage><d:AssignmentType>Serial</d:AssignmentType>
            </d:Assignment>";

  public const string IfsUserXmlTemplate = @"
        <pc:Person xmlns:pc=""http://schemas.microsoft.com/office/infopath/2007/PartnerControls"">
           <pc:DisplayName>{0}</pc:DisplayName><pc:AccountId>{1}</pc:AccountId><pc:AccountType>{2}</pc:AccountType>
        </pc:Person>";

 public const string AddAssignmentsElement =
    @"<d:Assignments xmlns:d=""http://schemas.microsoft.com/office/infopath/2009/WSSList/dataFields"">{0}</d:Assignments>";
}

Next steps is the actual code to convert the SPUserFieldValueCollection to the assignment xml, this part of code will call another routine that convert its SPUserFieldValue to the Person elements part of the xml.

//an extension method to serialize SPFieldUserValueCollection to the assignment xml
public static string ToWorkflowAssignmentXml(this SPFieldUserValueCollection users)
{
   StringBuilder sb = new StringBuilder();
   users.ForEach(u => sb.Append(u.ToInfopathUserXml()));
   return string.Format(Constant.WorkflowAssignmentTemplate, sb.ToString());
}
//An extension method for serializing SPFieldUserValue to the Person element
//It also determines the user type either User, SecurityGroup or SharePointGroup
public static string ToInfopathUserXml(this SPFieldUserValue userField)
{
  string groupType = string.Empty;
  if (userField.User != null)
  {
    groupType = userField.User.IsDomainGroup ? "SecurityGroup" : "User";
  }
  else groupType = "SharePointGroup";

 if (groupType == "SharePointGroup")
 {
  return string.Format(Constant.IfsUserXmlTemplate, 
    userField.LookupValue, userField.LookupValue, groupType);
 }
 return string.Format(Constant.IfsUserXmlTemplate, 
   userField.User.Name, userField.User.LoginName, groupType);
}

Let’s apply the code above to my 2 approaches from previous post. First go to any library or list that require the dynamic approver workflow, and setup and approval workflow with no approvers as shown below.

  • Code behind approach. We are using StartWorkflow of the SPWorkflowManager, and we replace the eventData argument with our xml. The code below shows how it works.
    //This routine will construct the workflow association data given the approvers data.
    //The approver data is stored in a list's multi users field called Approvers. As shown at the first picture at the beginning of this post.
    public static string ConstructApproversXml(string associationData, SPListItem approverItem)
    {
      string approversXml = (approverItem["Approvers"] as SPFieldUserValueCollection).ToWorkflowAssignmentXml();
      int startIndex = associationData.IndexOf("<d:Assignment>");
      int lastIndex = associationData.IndexOf("</d:Approvers>");
      string assignmentElementString = associationData.Substring(startIndex, lastIndex - startIndex);
      return associationData.Replace(assignmentElementString, approversXml);
    }
    

    We use the routine above, and pass it to the SPWorkflowManager.StartWorklow as shown below. The workflow will use the approvers defined in the the approverItem. Code below show how to use the routine above. Note. ApproverItem and CurrentItem can be different, as you can have the approvers list separated from the list that has the workflow association.

    //pass the workflow assocition and the approver Item to get the approverXml
    //list and approverItem instantiation are not shown
    var approverXml = WorkflowApproverHelper.ConstructApproversXml(list.WorkflowAssociations[0].AssociationData, approverItem);
    
    //call startWorkflow by passing the item in which the workflow run, the wf association
    //and the approverXml above. 
    //site, list, and currentItem instantiations are not shown. 
    site.WorkflowManager.StartWorkflow(currentItem, list.WorkflowAssociations[0], approverXml);
    				
    				
    
  • Wokflow Approach. This approach requires creating a custom activity as shown in this post. The is a little change in the assignment xml to have it works in a workflow. The Assignment elements needs to be enclosed inside Assignments (with s) element. This short code below adding the parent Assignments element.
    public static string ConstructApproverXmlForWorkflow(SPFieldUserValueCollection userValues)
    {
      return string.Format(Constant.AddAssignmentsElement, userValues.ToWorkflowAssignmentXml());
    }
    

    To use the code above you can change the line 29 of my post here with 2 lines of code below.

     var users = val[approverFieldName] as SPFieldUserValueCollection
     this.ReturnValue = WorkflowApproverHelper.ConstructApproverXmlForWorkflow(users);
    

In this post I have shown you how to implement dynamic workflow’s approvers for OOB SharePoint approval workflow. The sample code above is for one stage, serial approver. But you can extend easily for more complex workflow such as more stages of serial and parallel approvals. You can find the source code here.

SharePoint 2010: Perform complex operation in the WorkflowCompleted Event Receiver

In some of my projects I need to perform complex operations after the SharePoint Approval Workflow finished such as perform complex site provisioning when the status is approved or send an email when status is rejected. In SharePoint 2010 I can use workflow event receivers, one of them is WorkflowCompleted event. It has SPWorkflowEventProperties as its parameter. There is an issue with the parameter is that some its properties are null such as ActivationProperties & AssociationData. So there is no simple way to retrieve SPListItem on which the workflow runs.

Luckily, the InstanceId property is not null, and we can use this property to get the workflow task item from the Workflow Tasks list. And from the workflow task item we can extract listId, itemId and even the workflow name. Below are the internal name of the columns in the workflow task item that I use to retrieve the list item info:

  • WorkflowInstanceID (used this one in the CAML query)
  • WorkflowListId (GUID of the List on which the workflow associated)
  • WorkflowItemId (List item id on which the workflow runs)
  • WorkflowName (Workflow association name as also the workflow field name)

This code shows how to extract the workflow status information and run an operation based on the status during WorkflowCompleted event. Note: the status is based on SharePoint 2010 Approval Workflow Status.

public class SiteRequestListItemEvent : SPWorkflowEventReceiver
{
  //CAML query to retrieve task item based by WF instanceId
  private const string WfInstanceQuery = @"<Where><Eq>
          <FieldRef Name='WorkflowInstanceID'/><Value Type='GUID'>{0}</Value>
          </Eq></Where>";

  private const string WorkflowTaskList = "Workflow Tasks";

  public override void WorkflowCompleted(SPWorkflowEventProperties properties)
  {
    using (SPSite site = new SPSite(properties.WebUrl))
    using (SPWeb web = site.OpenWeb())
    {
      //Query Workflow Task List
      SPList wfTaskList = web.Lists[WorkflowTaskList];
      var items = wfTaskList.GetItems(new SPQuery()
      {
        Query = string.Format(WfInstanceQuery, properties.InstanceId.ToString())
      });
      if (items.Count > 0)
      {

        Guid listId = new Guid(items[0]["WorkflowListId"].ToString());
        int itemId = (int)items[0]["WorkflowItemId"];
        string workflowField = items[0]["WorkflowName"] as string;

        //Get the List item on which the workflow run
        SPListItem item = web.Lists[listId].GetItemById(itemId);

        //get the workflow status
        int status = Convert.ToInt32(item[workflowField]);

        //do something when approved
        if (status == 16)
        {
          //do something such as provision a site, apply permission, etc.
        }
      }

    }

  }

}

Below are the internal value sof the SPWorkflowStatus. SharePoint 2010 Approval workflow add some additional statuses such as Approved, Cancelled & Reject:

  • NotStarted = 0
  • FailedOnStart = 1
  • InProgress = 2
  • ErrorOccurred = 3
  • StoppedByUser = 4
  • Completed = 5
  • FailedOnStartRetrying = 6
  • ErrorOccurredRetrying = 7
  • ViewQueryOverflow = 8
  • Cancelled = 15
  • Approved = 16
  • Rejected = 17

Dynamic list form with ECMAScript Client Object Model

Client Object model is one of my favorite features in SharePoint 2010. It allows us to build rich features or interactive UI fairly quickly using Javascript and SharePoint Designer. In this post I will show how to use ECMAScript Client OM that performs custom operation in a custom Display Form. I will extend the custom form of  the Announcement List I created in my previous post to include additional operation.  In the list I added 2 more fields as shown below:

  1. Type: Yes/No, DisplayName: Read Flag, Internal Name: ReadFlag
  2. Type: User field (multi value), Display Name: Have Read, Internal Name: HaveRead

Then I added a button called ‘I have read’. The button will show whenever the ReadFlag field equal to ‘Yes’ and HaveRead doesn’t contain current user. And whenever a user click the button, I will add the current user into the HaveRead userfield collection and update the list item. There wouldn’t any managed code involve to implement this. Ok let’s get started with the detail implementation. I will reuse the same data form webpart and the its xlst from my previous post.

  1. On the dataform webpart I will add a new parameter binding to get the current user as shown below.
       <ParameterBinding Name="UserID" Location="CAMLVariable" DefaultValue="CurrentUserName"/>
    
  2. To use this variable in our xsl, we need to add UserID in the global parameter as shown below at line 3.
       <xsl:param name='ManualRefresh'></xsl:param>
      <xsl:param name='dvt_firstrow'>1</xsl:param>
      <xsl:param name="UserID"/>
    
  3. As I mentioned before that the button will be shown when ReadFlag = true and HaveRead doesn’t contain current user. The code below shows how to implement this.
    .
    <xsl:variable name="currentUserHasRead">
         <xsl:value-of select="contains(@HaveRead,$UserID)"/>
    </xsl:variable>
    ----------------------
    <xsl:if test="(@ReadFlag = '1')  and $currentUserHasRead = 'false'">
       <button class="ms-ButtonHeightWidth" id="IHaveRead" onclick="SetHaveRead({@ID});return false">
          I have read
      </button>
    </xsl:if>
    

    The first part of the xsl is creating a boolean variable which equals to true if current user is already added into HaveRead userfield collection. The 2nd part of the xsl is adding a html button inside an xsl if clause. The onclick button is calling a javascript function SetHaveRead and passing the itemId into the function. The ‘return false’ is added to avoid the button to perform postback

  4. Below is the client script that adding the current user to the HaveRead user field collection.
    var item;
    var ctx;
    var user;
    var notifyId;
    var $Id;
    
    function SetHaveRead(itemId) {
        notifyId = SP.UI.Notify.addNotification("Executing", true);
        $Id = itemId;
        ctx = SP.ClientContext.get_current();
        var announList = ctx.get_web().get_lists().getByTitle("Announcements");
        item = announList.getItemById(itemId);
        user = ctx.get_web().get_currentUser();
        ctx.load(item);
        ctx.load(user);
        ctx.executeQueryAsync(Function.createDelegate(this, this.doUpdate),
       Function.createDelegate(this, this.OnFailed));
    }
    
    function doUpdate(sender, args) {
        var users = item.get_item('HaveRead');
        if (!users)
            users = new Array();
    
        users.push(SP.FieldUserValue.fromUser(user.get_loginName()));
    
        ctx = SP.ClientContext.get_current();
        var announList = ctx.get_web().get_lists().getByTitle("Announcements");
        item = announList.getItemById($Id);
        item.set_item('HaveRead', users);
        item.update();
    
        ctx.load(item);
        ctx.executeQueryAsync(Function.createDelegate(this, this.UpdateSuccess),
        Function.createDelegate(this, this.OnFailed));
    }
    
    function UpdateSuccess(sender, args) {
        $("#IHaveRead").css("display", "none");
        SP.UI.Notify.removeNotification(notifyId);
    }
    
    function OnFailed(sender, args) {
        alert('Request failed. ' + args.get_message() + '\n' + args.get_stackTrace());
        SP.UI.Notify.removeNotification(notifyId);
    }
    

    This is a brief explanation about the code. The first method is the one called by the button, in this one it grabs current item & current user using Client API. In doUpdate method, it retrieves the HaveRead field value, if it is empty, initialize an array finally using SP.FieldUserValue.fromUser to get retrieve the UserFieldValue from the current user and push it into the array, then performs item update operation. When updateSuccess, the button is hidden using jquery. During the operation the (Executing) notification will be shown as shown in 2nd pic on this post. This msdn article contains javascript Client OM API.

  5. Finally we need to add the javascript reference to the page.

In conclusion DataForm webpart & Javascript Client OM is a powerfull combination to perform sharepoint customization fairly quickly.

You can find the xsl here.

Infopath List Form – hide/disable fields based on SharePoint group membership

As we know in SharePoint 2010 we can customize list form using Infopath 2010. With Infopath 2010 we will get some advantages of more flexible look and feel, data validation/rules as well as web services invocation in the form of data connection without writing any single of code as in fact Infopath list form does not support code behind.

In this post I will show how to use Infopath data connection to build rules for hiding/disabling fields based on the SharePoint group membership of current user. In my case I would like to disable the Approval field as shown below for any users that are not member of  a SharePoint group called Form Administrator.

infopath_approval_field

Here is my steps to achieve the goal:

  1. The first very step obviously is clicking the Customize Form on the ribbon to edit the form in Infopath Designer. I wouldn’t go to any detail of how to build a list form using Infopath 2010 as there are already a lot of posts about the topic for example this msdn article.

  2. Adding 2 received from SOAP Web Services data connections to the forms, again there are a lot of articles about this topic, so I wouldn’t go into any detail such as this one.
    • GetUserProfileByName in http://<server-name>/_vti_bin/UserProfileService.asmx. Tick Automatically retrieve data when the form opened.
    • GetGroupCollectionFromUser in http://server-name/_vti_bin/UserGroup.asmx (put a sample value, of an existing account such as contoso\account_name). Untick automatically retrieve data when the form opened.

  3. Save the form as source files as we are going to modify the xml schema of the GetGroupCollectionFromUser data connection. The reason why we need to do this is that the generated data connection doesn’t display the returned fields. As shown in the picture  that the dataFields  children elements only contain userLoginName which is not correct.
    To save the form as sources go to Files -> Publish as shown below then  click Export Source Files on the next window. The exported files would look like in the picture. It consists of infopath definition file (manifest.xsf), xml schemas of all data connections (.xsd files), some xml files that contain sample data of the data connections and an xsl file (view.xsl) used for form rendering. In our case we only focus on GetGroupCollectionFormUser.xsd (the xml schema for GetGroupCollectionFromUser data connection).

  4. Modify the GetGroupCollectionFromUser1.xsd, make sure close the Infopath Designer before editing. I found this approach from Sumit’s SharePoint blog, he explained how to correct the xsd in the ‘Correcting the xsd for the Return Fields’ section in his post. To summarize his approach basically, we need to add this below type definition
    <s:complexType name="GetGroupCollectionFromUserType">
        <s:sequence>
          <s:element minOccurs="0" maxOccurs="1" name="userLoginName" type="s:string"/>
          <s:element minOccurs="0" maxOccurs="1" name="Groups">
            <s:complexType>
              <s:sequence>
                <s:element maxOccurs="unbounded" name="Group" >
                  <s:complexType>
                    <s:attribute name="ID" type="s:unsignedShort"></s:attribute>
                    <s:attribute name="Name" type="s:string"></s:attribute>
                    <s:attribute name="Description" type="s:string"></s:attribute>
                    <s:attribute name="OwnerID" type="s:unsignedByte"></s:attribute>
                    <s:attribute name="OwnerIsUser" type="s:string"></s:attribute>
                  </s:complexType>
                </s:element>
              </s:sequence>
            </s:complexType>
          </s:element>
        </s:sequence>
      </s:complexType>
    

    below this line (at the 2nd line of the file)

    <s:import namespace="http://www.w3.org/2001/XMLSchema"></s:import>
    

    Then find this below

      <s:element name="GetGroupCollectionFromUser">
         <s:complexType>
           <s:sequence>
    	   <s:element minOccurs="0" maxOccurs="1" name="userLoginName" type="s:string">
               </s:element>
    	 </s:sequence>
          </s:complexType>
        </s:element>
    

    Replace it with this:

      <s:element name="GetGroupCollectionFromUser" type="tns:GetGroupCollectionFromUserType">
     </s:element>
    
  5. Create a rule to invoke GetGroupCollectionFromUser web services method. As the method is not invoked when the form is opened, we need to invoke the method after the GetUserProfileByName invocation finished. To do this, right click on the manifest.xsf and choose Design. Choose GetUserProfileByName data connection and apply rules on the PropertyData field as display in the picture.

    The rule condition is Name=”AccountName”, rule type is Action. The rule actions are:

    1. Set field’s value: the userLoginName of the GetGroupCollectionFromUser’s queryFields to the Property value AccountName from the GetUserProfileByName.  This blog shows how to set the property value (please follow steps 7 – 10)
    2. Query for data: GetGroupCollectionFromUser. (no need screen shot for this :) )

  6. Create a formatting rule for the field (in my case approval field) that we want to disable/hide if current user is not member of a SharePoint Group. Put a condition as below, where The Form Administrator is the SharePoint Group Name.

    And the any occurrence & number of occurrence of Name is the SharePoint groups’ name retrieved from the dataFields of GetGroupCollectionFromUser data connection below, in other words the rule check if any of the groups’ name is Form Administrator or no SharePoint groups found associated with the current user, if condition met, disable the field.

  7. And finally publish the form

Please feel free to give me feedback if there is better approach.

SharePoint 2010 – Add custom list form to existing list

In this post I will show you how to add a custom list form to existing list using SharePoint 2010 object model. Before we get into how, I would like to put some backgrounds about why I would do this:

  • I want to have a nicer display form (not in a form format) for an existing list such as anouncement list.
  • I also want to add a web part to the custom display form such as social comment web part.
  • And I want to be able to activate this display form as part of a feature.

This is my custom announcement display form looks like. As we can see that the list form web part has been replaced with a dataform webpart with custom rendering and also I added a social comment web part after the dataform web part. This can be done easily using SharePoint Designer. When creating a custom form, SharePoint Designer would add a DataForm web part instead of a ListForm web part to the aspx file. Interestingly, if we delete the dataform webpart, the aspx file would loose its function as a form. So we should modify the xlst of the dataform web part instead of replacing it.

To achieve the same result using codebehind, we need to do this followings:

1. Provision an aspx file and set it as a form page. As shown in the code below. In SharePoint 2007 we can’t achieve this as SPTemplateFileType.FormPage is new in SharePoint 2010.

  //the list's forms are located in the list's rootfolder
  SPFolder root = list.RootFolder;
  string customFormUrl = webUrl.LocalPath + "/" + root.Url + "/" + formName;
  //provision a new list form
  SPFile file = root.Files.Add(customFormUrl, SPTemplateFileType.FormPage);

2.Provision a webpart that implements IListWebPart such as ListFormWebPart, DataFormWebPart, DataViewWebPart, etc. Then we need to set the two properties of the web part: ListId, the List GUID, and PageType, this will determine what type of the form either display, new or edit form. My observation shows that without an IListWebPart and settings those 2 properties the aspx page wouldn’t be provisioned as a form. This below code excerpt shows how to provision the webpart and set those 2 properties (line 18, 19)

 //Provision IList Web Part in this case DataForm web part onto the new form
 SPLimitedWebPartManager limitedWebPartManager = file.GetLimitedWebPartManager(PersonalizationScope.Shared);
IListWebPart part = null;

//set specific dataform webpart properties
//for query (Parameter bindings & DataSources),
DataFormWebPart webPart = new DataFormWebPart();
webPart.ParameterBindings = ;
SPDataSource ds = new SPDataSource(); //the detail spdatasource initialization not shown
webPart.DataSources.Add(ds);

//for rendering XslLink
webPart.XslLink = webUrl.LocalPath + xslUrl;

//set the IListWebPart Propertes required for the form
part = webPart;
string str2 = list.ID.ToString("B").ToUpper(CultureInfo.InvariantCulture);
part.ListId = list.ID;
part.PageType = PAGETYPE.PAGE_DISPLAYFORM;

3. In the XSL don’t forget to add FormToolbar control as this would initialize the form’s ribbon, without it the form’s ribbon wouldn’t be displayed. The easiest way to get the right xsl is modifying the one created by SharePoint Designer when we provision a custom form via SharePoint Designer.

4. Lastly, set the list Default form. SPList has these properties: DefaultDisplayFormUrl, DefaultEditFormUrl and DefaultNewFormUrl. In my case I would set the DefaultDisplayFormUrl.

  list.DefaultDisplayFormUrl = customFormUrl;

With this approach we can provision custom forms on any list using code behind. This source code contains an example of an implementation of the approach by provisioning a custom display form for announcement list through a List Event receiver.

Related Post:
Dynamic list form with ECMAScript Client Object Model

SharePoint 2010: Custom Provider – cross site collection navigation

Recently I get a request to have a consistent top navigation across site collection, in which I need to retrieve the navigation items from a site collection to be used in different site collections (in the same web application).  As it is known that SharePoint is using Provider framework to build its navigation, and for Publishing site it is using PortalSiteMapProvider that provides additional caching mechanism. The issue with the provider is that it cannot live cross site collection boundary.

In this post, I would like to share how to build the custom navigation provider utilizing the PortalSiteMapProvider from different site collection (in this post I call it source site collection), I provide the source code at the end of this post. Let’s get started:

  1. We need to create a custom HttpHandler (ashx) that hosts the source site collection’s PortalSiteMapProvider. The ashx is deployed to layouts directory. This excellent post shows how to build the custom HttpHandler, in fact the source code is based on the post with some modification. Basically the handler will build JSON object tree based on the navigation structure of the source site collection.
  2. Then create a custom SiteMap provider that calls the ashx created at step 1. The provider will call the handler using WebRequest with the user credential, and deserialize the received JSON into a object structure used by the provider.
  3. Replace the implementation of OOB site map data source with our own. In v4.master the navigation data sources (SiteMapDataSource) are enclosed in delegate controls, below are the delegate controls in the master page
    Navigation Location Delegate Ctl ID
    Top navigation TopNavigationDataSource
    Quick launch QuickLaunchDataSource

    With this knowledge, we can replace the OOB data source with my own implementation. Below is the definition in my elements.xml to replace the top navigation data source with our own implementation.

    <Control Sequence="30"
       Id="TopNavigationDataSource"
       ControlClass="System.Web.UI.WebControls.SiteMapDataSource"
       ControlAssembly="System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
       <Property Name="ID">topSiteMap</Property>
       <Property Name="SiteMapProvider">SymprgNavigationProvider</Property>
       <Property Name="EnableViewState">false</Property>
       <Property Name="ShowStartingNode">true</Property>
     </Control>
    

    You can see that I replaced the top navigation data source implementation to use my custom provider: SymprgNavigationProvider. And also make sure the control sequence number is less than the OOB publishing control defined in Navigation feature (14 hive/TEMPLATE/FEATURES/Navigation /NavigationSiteSettings.xml- the OOB Publishing sequence number is 50).  Put this elements.xml to a web scoped feature. By activating the feature we can replace the top navigation.

  4. The last step is adding the custom navigation handler to the web.config inside siteMap > providers element as shown below. The custom provider (Symprg.NavProvider.NavigationProvider) has two custom attributes:
    • sourceUrl: the source site collection url that provides the navigation items
    • depth: the depth of the navigation, by default is 1 if not specified.
      <siteMap defaultProvider="CurrentNavigation" enabled="true">
        <providers>
           ---------------------
           <add name="SymprgNavigationProvider" type="Symprg.NavProvider.NavigationProvider, Symprg.NavProvider, Version=1.0.0.0, Culture=neutral, PublicKeyToken=d7699941b48f79bc" sourceUrl="http://aSiteCollection" depth="2"/>
        </providers>
      </siteMap>
    

You can find the source code here.

SharePoint 2010 dynamic approvers for OOB Approval Workflow part 1

This Article Series:

  1. SharePoint 2010 dynamic approvers for OOB Approval Workflow part 1
  2. SharePoint 2010 dynamic approvers for OOB Approval Workflow part 2

Microsoft has improved SharePoint’s Approval Workflow in SharePoint 2010 that it is now customizable where its completion condition, behavior of individual task and behavior of overall task processes can be changed on design time using SharePoint Designer 2010. The customization allow us to build more complex approval workflows such as updating list items/documents during the approval process, modify the permissions of the item, modify the email content, etc. This video shows you how to customize OOB Approval workflow.

The approvers of the worklow are defined on the Association or Initiation form. But in some cases we might want to have approvers determine dynamically based on the meta data of the workflow’s item. There are 2 approaches to inject approvers into the Approval Workflow:

  1. Inject the approvers to the Association Data from code behind. This approach can be achieved by starting the workflow in the code behind by calling StartWorkflow method of the SPWorkflowManager class and passing Association data parameter containing the dynamic approvers.
  2. Pass the approvers into the Approvers parameter of the Approval Workflow  during design time (in SharePoint Designer 2010). The picture shows the Approvers parameter.

To be able to inject the approvers data we need to know its data format. As we know, the approvers of the workflow are defined in its association form on the Approvers field as picture below. The association form is an Infopath form so that the approvers field on the form are represented by an xml data. Below is the xml data of the approvers (Approvers xml) as on the picture.

<d:Approvers
   xmlns:d="http://schemas.microsoft.com/office/infopath/2009/WSSList/dataFields"
   xmlns:pc="http://schemas.microsoft.com/office/infopath/2007/PartnerControls">
     <d:Assignment>
       <d:Assignee>
         <pc:Person>
           <pc:DisplayName>Daniel Laksana</pc:DisplayName>
           <pc:AccountId>aDomain\danielLogin</pc:AccountId>
           <pc:AccountType>User</pc:AccountType>
         </pc:Person>
         <pc:Person>
           <pc:DisplayName>Northwind Members</pc:DisplayName>
           <pc:AccountId>Northwind Members</pc:AccountId>
           <pc:AccountType>SharePointGroup</pc:AccountType>
         </pc:Person>
       </d:Assignee>
       <d:Stage xsi:nil="true" />
       <d:AssignmentType>Serial</d:AssignmentType>
      </d:Assignment>
      <d:Assignment>
       <d:Assignee>
         <pc:Person>
           <pc:DisplayName>aDomain\aGroup</pc:DisplayName>
           <pc:AccountId>aDomain\aGroup</pc:AccountId>
           <pc:AccountType>SecurityGroup</pc:AccountType>
         </pc:Person>
         <pc:Person>
           <pc:DisplayName>Northwind Visitors</pc:DisplayName>
            <pc:AccountId>Northwind Visitors</pc:AccountId>
            <pc:AccountType>SharePointGroup</pc:AccountType>
         </pc:Person>
         </d:Assignee>
          <d:Stage xsi:nil="true" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" />
         <d:AssignmentType>Parallel</d:AssignmentType>
     </d:Assignment>
  </d:Approvers>

There are 3 possible values of the AccountType:

  1. User
  2. SharePointGroup
  3. SecurityGroup for Active Directory group.

To inject dynamic approvers to the workflow, we can transform any user data from SPUser, SPFieldUserValue , UserProfile or any other data types to the xml format and passing it as part of the Association data of the workflow. You can find the sample implementation in the 2nd post of this series. Referring back to the 2 approaches I mentioned in the beginning of this post:

  1. With the code behind approach, StartWorkflow method of the SPWorklowManager, we can replace the Approvers part of the AssociationData property of the SPWorkflowAssociation parameter.
  2. For the workflow approach, we need to build a custom activity that has output format the same as the Approvers xml, and place the activity before the Approval workflow as shown in the picture.  I posted an article about the custom activity here.  The output xml must exclude the Approvers element as shown below as an example:
<d:Assignments xmlns:d=""http:="//schemas.microsoft.com/office/infopath/2009/WSSList/dataFields">
  <d:Assignment xmlns:d="http:=//schemas.microsoft.com/office/infopath/2009/WSSList/dataFields">
    <d:Assignee>
       <pc:Person
         xmlns:pc="http:=//schemas.microsoft.com/office/infopath/2007/PartnerControls">
        <pc:DisplayName>Approvers 1</pc:DisplayName>
        <pc:AccountId>Approvers 1</pc:AccountId>
        <pc:AccountType>SharePointGroup</pc:AccountType>
      </pc:Person>
       <pc:Person
         xmlns:pc="http:=//schemas.microsoft.com/office/infopath/2007/PartnerControls">
        <pc:DisplayName>Approvers 2</pc:DisplayName>
        <pc:AccountId>Approvers 2</pc:AccountId>
        <pc:AccountType>SharePointGroup</pc:AccountType>
      </pc:Person>
    </d:Assignee>
    <d:Stage>0</d:Stage>
    <d:AssignmentType>Serial</d:AssignmentType>
  </d:Assignment>
</d:Assignments>

With these mechanisms we can have dynamics approvers  for OOB Approval Workflow.

Microsoft SharePoint Workflow, Infopath, ASP.NET

Add custom commands on Infopath browser form ribbon

In Infopath 2010 browser form, the commands are shown on Ribbon, so from usability point of view, it would be great if we have our custom commands reside in the Ribbon.  The OOB Ribbon are provisioned by a SharePoint  feature called IPFSSiteFeatures, one of the xml files in it contains the Ribbon definition, IPFSRibbon.xml. And the command handlers are defined in javascript file, Core.js, in <hive 14>\TEMPLATE|LAYOUTS\INC

Infopath Custom Command

In the picture, we can see I have added a custom command called Complete. The  command will display a javascript prompt and then perform a postback where the code behind run a server operation. In my case it is set the field on the form, extract some data in the form into custom lists for reporting and send emails to interested party.

Below is the ribbon definition for the Complete command. The most important part is the Location, I put my custom command into the first Ribbon group (Ribbon.ContextualTabs.InfoPathHomeTab.FormAction). The command handler is a javascript function defined in CommandUIHandlers. And also make sure to provision this custom action as site feature (Scope=Web) as this custom command will be added to any Form libraries in the site.

<CustomAction Id="IFCustomRibbon" Location="CommandUI.Ribbon">
    <CommandUIExtension>
      <CommandUIDefinitions>
         <CommandUIDefinition Location="Ribbon.Tabs.InfoPathHomeTab.FormActions.Controls._children">
             <Button Id="Ribbon.Tabs.Lcms.FormActions.Controls.Complete"
               Sequence="70" Command="CompleteForm"
               Image16by16="/_layouts/$Resources:core,Language;/images/formatmap16x16.png" Image16by16Top="-48" Image16by16Left="-48"
              Image32by32="/_layouts/$Resources:core,Language;/images/formatmap32x32.png" Image32by32Top="0" Image32by32Left="-96"
              LabelText="Complete"
              TemplateAlias="o2"
              ToolTipTitle="Complete"
              ToolTipDescription="$Resources:CcaLcms,cui_desc_CompleteForm;"/>
        </CommandUIDefinition>
      </CommandUIDefinitions>
   <CommandUIHandlers>
       <CommandUIHandler Command="CompleteForm" CommandAction="javascript:CompleteForm();
          function CompleteForm(){
             if(confirm('This will complete the form and send notification to interested party'))
             {
               var $command = $get('FormControl_V1_I1_B29');
               $command.click();
             }
          }
        "/>
      </CommandUIHandlers>
    </CommandUIExtension>
  </CustomAction>

The Command handler is mapped to a javascript function. In it it will display a confirmation to users and then perform a postback. The trick to do a postback is on the Infopath form we have a hidden button that runs custom code during on click event. As it is hidden the button wouldn’t be displayed on the browser form but its html representation (<input>) can be still be found in form’s html.

To have the button callable in javascript we need to get the button Id. This can be achieved by using IE Developer Tool or Firebug to inspect the browser form and get the id of the html input tag that represents the button. In this example The html input tag would have attributes: buttonid=FormComplete and value=Complete. The
FormControl_V1_I1_B29 in the javascript above is the input tag id of the Complete button.

With the mechanism we basically can execute any Infopath buttons from our ribbon’s custom commands, such as run a workflow, open a modal dialog, etc. Though there is a catch that the buttons id tend to change if we change the structure of the form, but if we put the hidden buttons on the very top of the form, we can avoid the changes of the ids when the form structure changes.

Additionally we also can call the submit and close from javascript. I got this following functions from Core.js mentioned at the beginning of the article. You can dig the Core.js to find more functions.

   var a = IPFSRibbon_GetFocusedFormControlId();
   IPFSRibbon_HandleButtonClick(a, 'submit');  //this will submit the form
   IPFSRibbon_HandleButtonClick(a, 'close');   //this will close the form
   var navigateUrl = CurrentFormData_UrlToNavigateToOnClose(a);

NOTE. You must delete the browser’s cache files every time deploying the changes of the ribbon definition (CustomAction).

Microsoft SharePoint 2010, Infopath 2010, .NET

Infopath Form redirect to a Custom FormServer page

In one of my project I need to use a custom FormServer.aspx. The reason I created a custom FormServer page was to be able to inject ribbon commands into the browser form and also handle the XmlFormView’s NotifyHost event. I didn’t do much modification of the OOB FormServer.aspx apart from adding a Delegate control and handle the XmlFormView’s OnNotifyHost event. The delegate control would contains some logic for the ribbon as well as the implementation of the OnNotifyHost event.

As by default a browser form will be opened by FormServer.aspx, I need to redirect the form to my custom FormServer page. There are some suggestions here such as:

  • Create a custom link: this is not applicable for me as users would go to the Form Library and click on the form, another reason is there is workflow on the form and any link on the email would use the OOB FormServer page.
  • Modify the serverfiles.xml: I dont want to modify any OOB xml as it would be overwritten during upgrade.
  • Create http module: this solution is clean, but this is not applicable in my case.

Another solution is simply redirect the form during OnLoading event. This approach has a catch though as the server redirection (Response.Redirect or Server.Transfer) would not work as during the OnLoading event as the HTTP Response already sent to the client. So the only way to perform redirection is using javascript redirection.

Below is the part of the code to perform the redirection. The redirection is performed by OOB javascript function called STSNavigate. On line 6, I injected a style sheet to hide the ribbon otherwise there would be flickering on the ribbon as the ribbon html would be displayed before the script run.

if (!CustomFormPage)
{
  Page page = HttpContext.Current.Handler as Page;
  request = request.Replace("_layouts/FormServer.aspx", "_layouts/Custom/FormServer.aspx");
  request = request.Contains("Source=") ? request : request += "&Source=" + SPContext.Current.Web.Url;
  HttpContext.Current.Response.Write("<style>#RibbonWrapper{display:none;}</style>");
  page.ClientScript.RegisterStartupScript(page.GetType(), "redirect",
       "STSNavigate('" + request + "');", true);
}

Please feel free to suggest a better approach.

SharePoint 2010, Infopath 2010