Thursday, October 27, 2011

Invoke SharePoint Workflow Programmatically

There's some restricted SharePoint sites and content in your SharePoint environment, and users can only interact with secure SharePoint resources through custom webpart(s). A typical example is performance review system. For simplicity let's say each department has a site or subsite to store the review data. You create a SPList for each year's review like PerformanceReview-2011, PerformanceReview-2012, etc. Only the management team members of a department can have direct access to the department performance review site. Every employee can input the data and respond manager's question/review through a custom webpart. Usually RunWithElevatedPrivileges is used inside the WebPart to get through the permission issue for SPList content update as code below:
    SPSecurity.RunWithElevatedPrivileges(delegate()
    {
        using (SPSite site = new SPSite(siteID))
        {
            using (SPWeb web = site.OpenWeb(webID))
            {
                SPList list = web.Lists.TryGetList(listName);
                //Update List Item ...
            }
        }
    });
A workflow associated with the List is supposed to automatically kick off doing some extra stuff such as assigning task to corresponding managers. The problem is that the execution context of above code is system account, and updating List Item with System account won't trigger the workflow. So you have to start the workflow manually inside the WebPart:
    // Start a workflow manually since system account won't trigger workflow automatically
    private static void StartWorkflow(SPListItem listItem, string workflowName)
    {
        try
        {
            var listWorkflowAssociations = listItem.ParentList.WorkflowAssociations;
            foreach (SPWorkflowAssociation wfAssoication in listWorkflowAssociations)
            {
                if (wfAssoication.Name == workflowName)
                {
                    listItem.Web.Site.WorkflowManager.
                      StartWorkflow(listItem, wfAssoication, wfAssoication.AssociationData, true);
                    break;
                }
            }
        }
        catch (Exception ex)
        {
            // Exception handling
        }
    }
Bear in mind that the Associator, Initiator and Current User inside Workflow context all are system account with such implementation. You have to use People field defined in List Item to reference the original user.

Friday, October 21, 2011

Event Receivers Issue After Migration From SharePoint 2007 to SharePoint 2010

We have a SharePoint 2007 site created by Reservations Template (part of WSS 3.0 Application Templates). The site is used for company-wide meeting room booking. After migrating to SharePoint 2010 (database-attachment approach) the room-booking system was not working properly. Same resource can be doubly booked and sometimes you can't book some resources while they show as available in the UI.

After some digging I found the problem was caused by the Event Receiver associated with the Reservations List. Originally in SharePoint 2007 the Reservations List has registered the Event Receivers with assembly of "ReservationEventHandler, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c". That assembly registration became "ReservationEventHandler, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" after migration for some reason. To fix the problem, we simply correct the List's event registry:
static void FixReservationEventReceivers(SPSite site, string webUrl, string listName)
{
    using (SPWeb web = site.OpenWeb(webUrl))
    {
        bool hasAddingEvent = false, hasDeletingEvent = false, hasUpdatingEvent = false;
        SPList list = web.Lists.TryGetList(listName);
        List<SPEventReceiverDefinition> invalidReceivers = new List<SPEventReceiverDefinition>();
        foreach (SPEventReceiverDefinition item in list.EventReceivers)
        {
            if (item.Assembly.Contains("ReservationEventHandler") && item.Assembly.Contains("Version=14.0.0.0"))
                invalidReceivers.Add(item);
            if (item.Assembly.Contains("ReservationEventHandler") && item.Type == SPEventReceiverType.ItemAdding)
                hasAddingEvent = true;
            if (item.Assembly.Contains("ReservationEventHandler") && item.Type == SPEventReceiverType.ItemDeleting)
                hasDeletingEvent = true;
            if (item.Assembly.Contains("ReservationEventHandler") && item.Type == SPEventReceiverType.ItemUpdating)
                hasUpdatingEvent = true;
        }
        foreach (var item in invalidReceivers)
        {
            item.Delete();
        }

        string assemblyName = "ReservationEventHandler, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c";
        string className = "Microsoft.SharePoint.ApplicationTemplates.ReservationEventHandler";
        if (!hasAddingEvent)
            list.EventReceivers.Add(SPEventReceiverType.ItemAdding, assemblyName, className);
        if (!hasDeletingEvent)
            list.EventReceivers.Add(SPEventReceiverType.ItemDeleting, assemblyName, className);
        if (!hasUpdatingEvent)
            list.EventReceivers.Add(SPEventReceiverType.ItemUpdating, assemblyName, className);
    }
}
You may wonder what those event receivers do with the List item. The explanation is lengthy but not worthy knowing to be honest. The template was not well designed and implemented in my opinion. I spent quite a bit of time to customize and optimize the template. Actually we don't need those event receivers anymore after my modification and the performance of the booking system has been improved significantly since then (hundreds of thousands of records in our Reservations List). I may write some notes about that later.

Monday, October 17, 2011

XsltListViewWebPart Custom View and XSL

SharePoint 2010 introduces a new XsltListViewWebPart that is inherited from DataFormWebPart. One great feature in XsltListViewWebPart is that you can define query and fields easily and clearly. Following XML declaration demonstrates how to set a XsltListViewWebPart to show open tasks assign to current user, and link to external custom XSL to display the items.
<WebPartPages:XsltListViewWebPart runat="server" IsIncluded="True" GhostedXslLink="main.xsl" 
    ListDisplayName="Tasks" NoDefaultStyle="TRUE" ViewFlag="8" Title="Tasks" PageType="PAGE_NORMALVIEW"  
    Default="FALSE" DisplayName="Open Tasks" DetailLink="Lists/Tasks" __markuptype="vsattributemarkup" 
    partorder="3"  viewcontenttypeid="0x" >
    <XmlDefinition>
        <View MobileView="TRUE" Type="HTML" DisplayName="All Tasks" Url="Lists/Tasks/AllItems.aspx" 
            Level="1" BaseViewID="1" ContentTypeID="0x" ImageUrl="/_layouts/images/issues.png">
            <Query>
                <OrderBy>
                    <FieldRef Name="Status" Ascending="TRUE"/>
                </OrderBy>
                <GroupBy></GroupBy>
                <Where>
                    <And>
                        <Eq>
                            <FieldRef Name="AssignedTo"/><Value Type="Integer"><UserID/></Value>
                        </Eq>
                        <Neq>
                            <FieldRef Name="Status"/><Value Type="Text">completed</Value>
                        </Neq>
                    </And>
                </Where>
            </Query>
            <ViewFields>
                <FieldRef Name="LinkTitleNoMenu"/>
                <FieldRef Name="Status"/>
                <FieldRef Name="Priority"/>
                <FieldRef Name="DueDate"/>
                <FieldRef Name="PercentComplete"/>
            </ViewFields>
            <RowLimit Paged="TRUE">30</RowLimit>
            <Toolbar Type="Freeform" />
        </View>
    </XmlDefinition>
    <XslLink>Site Assets/XSL/OpenTask.xsl</XslLink>
    <parameterbindings>
        <ParameterBinding Name="dvt_sortdir" Location="Postback;Connection"/>
        <ParameterBinding Name="dvt_sortfield" Location="Postback;Connection"/>
        <ParameterBinding Name="dvt_startposition" Location="Postback" DefaultValue=""/>
        <ParameterBinding Name="dvt_firstrow" Location="Postback;Connection"/>
        <ParameterBinding Name="OpenMenuKeyAccessible" Location="Resource(wss,OpenMenuKeyAccessible)"/>
        <ParameterBinding Name="open_menu" Location="Resource(wss,open_menu)"/>
        <ParameterBinding Name="select_deselect_all" Location="Resource(wss,select_deselect_all)"/>
        <ParameterBinding Name="idPresEnabled" Location="Resource(wss,idPresEnabled)"/>
        <ParameterBinding Name="NoAnnouncements" Location="Resource(wss,noXinviewofY_LIST)"/>
        <ParameterBinding Name="NoAnnouncementsHowTo" Location="Resource(core,noXinviewofY_DEFAULT)"/>
        <ParameterBinding Name="AddNewAnnouncement" Location="Resource(wss,addnewitem)"/>
        <ParameterBinding Name="MoreAnnouncements" Location="Resource(wss,moreItemsParen)"/>
        <ParameterBinding Name="UserID" Location="CAMLVariable" DefaultValue="CurrentUserName"/>
    </parameterbindings>
</WebPartPages:XsltListViewWebPart>
The custom XSL does three things:

1. Replace priority string from "(2) Normal" by "Normal"
2. Replace default empty message "There are no items to show in this view of the {ListName} list. To add a new item, click 'New'." by "No item"
3. Replace the SP2010 Add New Item icon to old SP2007 icon

The XSL source:
<xsl:stylesheet xmlns:x="http://www.w3.org/2001/XMLSchema" xmlns:d="http://schemas.microsoft.com/sharepoint/dsp" 
                version="1.0" exclude-result-prefixes="xsl msxsl ddwrt" 
                xmlns:ddwrt="http://schemas.microsoft.com/WebParts/v2/DataView/runtime" 
                xmlns:asp="http://schemas.microsoft.com/ASPNET/20" 
                xmlns:__designer="http://schemas.microsoft.com/WebParts/v2/DataView/designer" 
                xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
                xmlns:msxsl="urn:schemas-microsoft-com:xslt" 
                xmlns:SharePoint="Microsoft.SharePoint.WebControls" 
                xmlns:ddwrt2="urn:frontpage:internal" 
                xmlns:o="urn:schemas-microsoft-com:office:office">
    <xsl:include href="/_layouts/xsl/main.xsl"/>
    <xsl:include href="/_layouts/xsl/internal.xsl"/> 

    <xsl:template name="FieldRef_body.Priority" match="FieldRef[@Name='Priority']" mode="body">
                <xsl:param name="thisNode" select="."/>
                    <xsl:choose>
                      <xsl:when test="starts-with($thisNode/@*[name()=current()/@Name], '(1) ')">
              <xsl:value-of select="substring-after($thisNode/@*[name()=current()/@Name], '(1) ')"/>
                      </xsl:when>
            <xsl:when test="starts-with($thisNode/@*[name()=current()/@Name], '(2) ')">
              <xsl:value-of select="substring-after($thisNode/@*[name()=current()/@Name], '(2) ')"/>
            </xsl:when>
            <xsl:when test="starts-with($thisNode/@*[name()=current()/@Name], '(3) ')">
              <xsl:value-of select="substring-after($thisNode/@*[name()=current()/@Name], '(3) ')"/>
            </xsl:when>
                    </xsl:choose>
    </xsl:template>
  
  <xsl:template name="EmptyTemplate">
    <tr>
      <td class="ms-vb" colspan="99">No item</td>
    </tr>
  </xsl:template>

  <xsl:template name="Freeform" >
    <xsl:param name="AddNewText" />
    <xsl:param name="ID" />
    <xsl:variable name="Url">
      <xsl:choose>
        <xsl:when test="List/@TemplateType='119'">
          <xsl:value-of select="$HttpVDir" />/_layouts/CreateWebPage.aspx?List=
          <xsl:value-of select="$List" />&amp;RootFolder=
          <xsl:value-of select="$XmlDefinition/List/@RootFolder" />
        </xsl:when>
        <xsl:when test="$IsDocLib">
          <xsl:value-of select="$HttpVDir" />/_layouts/Upload.aspx?List=
          <xsl:value-of select="$List" />&amp;RootFolder=
          <xsl:value-of select="$XmlDefinition/List/@RootFolder" />
        </xsl:when>
        <xsl:otherwise>
          <xsl:value-of select="$ENCODED_FORM_NEW" />
        </xsl:otherwise>
      </xsl:choose>
    </xsl:variable>
    <xsl:variable name="HeroStyle">
      <xsl:choose>
        <xsl:when test="Toolbar[@Type='Standard']">display:none</xsl:when>
        <xsl:otherwise></xsl:otherwise>
      </xsl:choose>
    </xsl:variable>
    <xsl:if test="$ListRight_AddListItems = '1' and (not($InlineEdit) or $IsDocLib)">
      <table id="Hero-{$WPQ}" width="100%" cellpadding="0" cellspacing="0" border="0" style="{$HeroStyle}">
        <tr>
          <td colspan="2" class="ms-partline">
            <img src="/_layouts/images/blank.gif" width="1" height="1" alt="" />
          </td>
        </tr>
        <tr>
          <td class="ms-addnew" style="padding-bottom: 3px">
            <img src="/_layouts/images/rect.gif" alt="Add a new item" />
            <xsl:text disable-output-escaping="yes" ddwrt:nbsp-preserve="yes">&amp;nbsp;</xsl:text>
            <xsl:choose>
              <xsl:when test="List/@TemplateType = '115'">
                <a class="ms-addnew" id="{$ID}-{$WPQ}" href="{$Url}" 
                   onclick="javascript:NewItem2(event, &quot;{$Url}&quot;);javascript:return false;" target="_self">
                  <xsl:value-of select="$AddNewText" />
                </a>
              </xsl:when>
              <xsl:otherwise>
                <a class="ms-addnew" id="{$ID}" href="{$Url}" 
                   onclick="javascript:NewItem2(event, &quot;{$Url}&quot;);javascript:return false;" target="_self">
                  <xsl:value-of select="$AddNewText" />
                </a>
              </xsl:otherwise>
            </xsl:choose>
          </td>
        </tr>
        <tr>
          <td>
            <img src="/_layouts/images/blank.gif" width="1" height="5" alt="" />
          </td>
        </tr>
      </table>
      <xsl:choose>
        <xsl:when test="Toolbar[@Type='Standard']">
          <script type='text/javascript'>
            if (typeof(heroButtonWebPart <xsl:value-of select="$WPQ" />) != "undefined") {
            <xsl:value-of select="concat(' var eleHero = document.getElementById(&quot;Hero-', $WPQ, '&quot;);')" />
            if (eleHero != null) eleHero.style.display = ""; }
          </script>
        </xsl:when>
        <xsl:otherwise></xsl:otherwise>
      </xsl:choose>
      <xsl:if test="List/@TemplateType = '115'">
        <script type='text/javascript'>
          if (typeof(DefaultNewButtonWebPart <xsl:value-of select="$WPQ" />) != "undefined") {
          <xsl:value-of select="concat(' var eleLink = document.getElementById(&quot;', $ID, '-', $WPQ, '&quot;);')" />
          if (eleLink != null) { DefaultNewButtonWebPart <xsl:value-of select="$WPQ" /> (eleLink); } }
        </script>
      </xsl:if>
    </xsl:if>
  </xsl:template>
</xsl:stylesheet>
Note that linking external XSL in XsltListViewWebPart would have performance impact since a compilation process would occur each time the WebPart is loaded, and the WebPart content can't be cached anymore. So it would be better to use inline XSL if there're many WebParts in a page.

Thursday, October 13, 2011

Using SharePoint WebProvisioned Event to Update Newly Created Site

I've been working on a SharePoint 2010 Site Definition that includes a bunch of Lists and a default page, and the new site (SPWeb) built by the Site Definition should keep old SharePoint 2007 UI because the users are used to the old looks and feels. A DataFormWebPart on the default page shows recent updates for a few different Lists. The problem is that those ListIDs are hard-coded in side DFWP, but ListIDs will be changed when a new site is created. Some tweaks on DFWP, e.g. changing ListID to ListName, could make it reusable, but that only works for single datasource but won't work for multiple datasources (AggregateDataSource). I tried AllUsersWebPart and View approach to provision the WebPart instance to the default page but without luck.

To resolve the problem I use tokens in the DFWP and replace those tokens with real ListIDs after site is created, as following:
<SharePoint:SPDataSource runat="server" DataSourceMode="List" UseInternalName="true" 
            UseServerDataFormat="true" selectcommand="&lt;View&gt;&lt;/View&gt;">
    <SelectParameters><asp:Parameter Name="ListID" DefaultValue="$LIST_TOKEN:{Project Documents}$"/>
    </SelectParameters>
    <DeleteParameters><asp:Parameter Name="ListID" DefaultValue="$LIST_TOKEN:{Project Documents}$"/>
    </DeleteParameters>
    <UpdateParameters><asp:Parameter Name="ListID" DefaultValue="$LIST_TOKEN:{Project Documents}$"/>
    </UpdateParameters>
    <InsertParameters><asp:Parameter Name="ListID" DefaultValue="$LIST_TOKEN:{Project Documents}$"/>
    </InsertParameters>
</SharePoint:SPDataSource>
In above DFWP configuration the "$LIST_TOKEN:{Project Documents}$" token will be replaced by the List/Library ID of "Project Documents". The other issue is that sometimes some WebParts on the default page are automatically closed after the new site is provisioned for some unknown reason. So there're a few issues I need to fix for the Site Definition:

1. Convert SP2010 site back to SharePoint 2007 UI
2. Change master page and css
3. Change DFWP's list Ids on default page
4. Open closed WebParts if exists

The solution is use SPWebEventReceiver to update site properties and the default page:
using System;
using System.Security.Permissions;
using System.Text.RegularExpressions;
using System.Web.UI.WebControls.WebParts;

using Microsoft.SharePoint;
using Microsoft.SharePoint.WebPartPages;

namespace SiteDefinition
{
    /// <summary>
    /// Web Provision Events
    /// </summary>
    public class INGProjectSiteEventReceiver : SPWebEventReceiver
    {
        /// <summary>
        /// Update a site (SPWeb) when it's created
        /// 1. Convert site back to SharePoint 2007 UI
        /// 2. Update master page and css file
        /// 3. Update DFWP's list Ids on default page
        /// 4. Open closed WebParts if exists
        /// </summary>
        public override void WebProvisioned(SPWebEventProperties properties)
        {
            if (properties.Web.WebTemplateId == 11001) // Only applied to targetted template
            {
                properties.Web.UIVersion = 3;
                properties.Web.MasterUrl = properties.Web.Site.RootWeb.MasterUrl;
                properties.Web.AlternateCssUrl = properties.Web.Site.RootWeb.AlternateCssUrl;
                properties.Web.Update();

                UpdateDefaultPageWebPartListIDs(properties.Web);
                OpenClosedWebParts(properties.Web);
            }

            base.WebProvisioned(properties);   
        }

        /// <summary>
        /// Update default page's DataFormWebParts' ListID property with corresponding List ID (GUID)
        /// </summary>
        private void UpdateDefaultPageWebPartListIDs(SPWeb web)
        {

            SPFile defaultPage = web.RootFolder.Files["Default.aspx"];
            if (defaultPage.Exists)
            {
                System.Text.ASCIIEncoding coding = new System.Text.ASCIIEncoding();
                byte[] byteArrayText = defaultPage.OpenBinary();

                if (byteArrayText.Length > 0)
                {
                    string origHtml = coding.GetString(byteArrayText);
                    string newHtml = ReplaceDataSourceToken(origHtml, web);
                    if (!string.IsNullOrEmpty(newHtml))
                    {
                        byte[] newByteArray = coding.GetBytes(newHtml);
                        defaultPage.SaveBinary(newByteArray);
                    }
                }
            }
        }

        /// <summary>
        /// Pre-defined tokens are used inside default page and we need to replace them here:
        /// e.g. "\$LIST_TOKEN:{Tasks}\$" will be replaced with Tasks' List ID (GUID)
        /// </summary>
        private string ReplaceDataSourceToken(string text, SPWeb web)
        {
            string pattern = @"\$LIST_TOKEN:{.*?}\$";
            Match mc = Regex.Match(text, pattern);
            if (mc.Success)
            {
                while (mc.Success)
                {
                    if (!string.IsNullOrEmpty(mc.Value))
                    {
                        string listName = mc.Value.Substring(13, mc.Length - 15);

                        SPList list = web.Lists.TryGetList(listName);
                        if (list != null)
                            text = string.Format("{0}{1}{2}",  
                                text.Substring(0, mc.Index), list.ID.ToString("B"), text.Substring(mc.Index + mc.Length));
                        else
                            text = string.Format("{0}$LIST_NOT_FOUND:[{1}]{2}",
                                text.Substring(0, mc.Index), listName, text.Substring(mc.Index + mc.Length);
                        
                        mc = Regex.Match(text, pattern);
                    }
                }
            }
            return text;
        }

        /// <summary>
        /// Sometimes WebParts are closed automatically and this method opens all closed webParts in the default page
        /// </summary>
        private void OpenClosedWebParts(SPWeb web)
        {
            SPFile defaultPage = web.RootFolder.Files["Default.aspx"];
            if (defaultPage.Exists)
            {
                using (SPLimitedWebPartManager wpManager = 
                    defaultPage.GetLimitedWebPartManager(PersonalizationScope.Shared))
                {
                    foreach (Microsoft.SharePoint.WebPartPages.WebPart wp in wpManager.WebParts)
                    {
                        if (wp.IsClosed)
                        {
                            wpManager.OpenWebPart(wp);
                            wpManager.SaveChanges(wp);
                        }
                    }                
                }
            }
        }
    }
}