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);
                        }
                    }                
                }
            }
        }
    }
}