Manage WebParts of Page Layouts through a feature: Resolving the "AllUsersWebPart"-pain – Part 2

2009-05-26 UPDATE: I've uploaded now a fully Visual Studio Project called "1stQuad's Page Layout WebParts Configurator" containing the feature described below. Please download it from here.

Before we start, please make sure that you've read and prepared everything posted in Part 1.

OK, so now – finally – to the code in the file "PageLayoutsWebPartConfiguratorFeatureReceiver.cs". I am using a custom logger, so I've commented out all logging calls - maybe you can adjust them to your logging-mecanism or simply delete them.

Let's start with the feature receiver and the obligatory overrides from which we only need "FeatureActivated":

using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Web.UI.WebControls.WebParts;
using System.Xml;
using System.Xml.XPath;
using Microsoft.SharePoint;
using Microsoft.SharePoint.WebPartPages;

namespace YourNameSpace.Receivers
{
    public class PageLayoutsWebPartConfiguratorFeatureReceiver : SPFeatureReceiver
    {
        private SPFeatureReceiverProperties _properties;

        /// <summary>
        /// Constructor
        /// </summary>
        public PageLayoutsWebPartConfiguratorFeatureReceiver() { }

        /// <summary>
        /// Applies web parts to publishing page layouts based on configuration and webpart definition files.
        /// </summary>       
        /// <param name="properties">SPFeatureReceiverProperties</param>
        public override void FeatureActivated(SPFeatureReceiverProperties properties)
        {
            try
            {
                //Logger.Write(this, "Featurea activation started.", System.Diagnostics.TraceEventType.Information, TraceEventCategory.FeatureReceiver);

                _properties = properties;
                SPSecurity.CodeToRunElevated elevatedAppliedConfigurations = new SPSecurity.CodeToRunElevated(ApplyConfiguration);
                SPSecurity.RunWithElevatedPrivileges(elevatedAppliedConfigurations);

            }
            catch (Exception ex)
            {
                //Logger.Write(this, "Feature Activation Failed", ex.ToString(),
                //   System.Diagnostics.TraceEventType.Error, TraceEventCategory.FeatureReceiver, new string[] { TraceEventCategory.Error });
                throw ex;
            }
            //Logger.Write(this, "Feature Activation Successful", System.Diagnostics.TraceEventType.Information, TraceEventCategory.FeatureReceiver);
        }

        /// <summary>
        /// No operation
        /// </summary>       
        /// <param name="properties">SPFeatureReceiverProperties</param>
        public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
        {
            /* no op */
        }

        /// <summary>
        /// No operation
        /// </summary>
        /// <param name="properties"></param>
        public override void FeatureInstalled(SPFeatureReceiverProperties properties)
        {
            /* no op */
        }

        /// <summary>
        /// No operation
        /// </summary>
        /// <param name="properties"></param>
        public override void FeatureUninstalling(SPFeatureReceiverProperties properties)
        {
            /* no op */
        }

As you can see, we run the "ApplyConfiguration" method with elevated priviledges. Let's have a look now at the "ApplyConfiguration" method:

        private SPWeb ElevatedWebContext { get; set; }
        private SPSite ElevatedSiteContext { get; set; }

        /// <summary>
        /// Applies the WebPart configuration to page layouts. Runs with elevated priviledges.
        /// </summary>
        private void ApplyConfiguration()
        {
            this.ElevatedSiteContext = new SPSite(((SPSite)_properties.Feature.Parent).Url);
            this.ElevatedWebContext = this.ElevatedSiteContext.OpenWeb("/");

            try
            {

                foreach (XPathNavigator pageLayoutConfiguration in LoadConfigurationFile().CreateNavigator()
                    .Select("configuration/pageLayout"))
                {

                    string pageLayoutFilename = pageLayoutConfiguration.GetAttribute("filename", "");

                    //Logger.Write(this, string.Format("Processing page layout '{0}'.", pageLayoutFilename),
                    //    System.Diagnostics.TraceEventType.Information, TraceEventCategory.FeatureReceiver);

                    // Load the SPListItem that represents the page layout.
                    SPListItem pageLayoutListItem = LoadPageLayoutListItem(pageLayoutFilename, true);

                    // Check out the page Layout
                    pageLayoutListItem.File.CheckOut();

                    try
                    {
                        // Get the limitedWebPartManager of the page layout file.
                        using (SPLimitedWebPartManager wpManager = pageLayoutListItem.File.GetLimitedWebPartManager(PersonalizationScope.Shared))
                        {
                            foreach (XPathNavigator webPartConfiguration in pageLayoutConfiguration.Select("webPart"))
                            {
                                this.ProcessWebPartConfiguration(wpManager, webPartConfiguration, pageLayoutListItem);
                            }

                            this.DeleteRedundantWebParts(wpManager, pageLayoutConfiguration, pageLayoutListItem);
                        }

                        // Update the list item.
                        pageLayoutListItem.Update();

                        // Check in and publish the page layout.
                        string fileOperationMessage = _properties.Definition.GetTitle(System.Threading.Thread.CurrentThread.
                            CurrentCulture) + ": WebPart configuration.";
                        pageLayoutListItem.File.CheckIn(fileOperationMessage);
                        pageLayoutListItem.File.Publish(fileOperationMessage);
                        pageLayoutListItem.File.Approve(fileOperationMessage);

                        // "Uncustomize/ghost" the page layout.
                        pageLayoutListItem.File.RevertContentStream();
                    }
                    catch
                    {
                        // Make sure that the file is not checked out anymore.
                        pageLayoutListItem.File.UndoCheckOut();
                        throw;
                    }
                }
            }
            catch (Exception ex)
            {
                //Logger.Write(this, "Unexpected error during processing of configurations:" + ex.Message, ex.ToString(),
                //   System.Diagnostics.TraceEventType.Error, TraceEventCategory.FeatureReceiver, new string[] { TraceEventCategory.Error });
                throw;
            }
            finally
            {
                this.ElevatedWebContext.Dispose();
                this.ElevatedSiteContext.Dispose();
            }
        }

 As you can see, I'm processing the WebParts in two steps per page layout: Apply all webparts that are in the configuration file and then delete the WebParts that are rendunant (they were once in a configuration but are no longer).

Here is the helper method that loads the configuration file:

       /// <summary>
        /// Loads the configuration file containing page layout WebParts' configuration.
        /// </summary>
        /// <returns>The configuration file.</returns>
        private XPathDocument LoadConfigurationFile()
        {
            string configFilepath = _properties.Definition.RootDirectory + "
\configuration.xml";

            XmlTextReader configReader;
            XPathDocument configDoc;

            try
            {
                configReader = new XmlTextReader(new FileStream(configFilepath, FileMode.Open, FileAccess.Read));
                configDoc = new XPathDocument(configReader);
            }
            catch (Exception ex)
            {
                throw new ApplicationException("Could not load configuration file 'configuration.xml' or invalid XML-Syntax.", ex);
            }
            return configDoc;
        }

And the helper method that gets an instance of the SPListItem that is the current page layout:

        /// <summary>
        /// Loads the SPListItem-instance of the page layout which is being configured.
        /// </summary>
        /// <param name="filename">The filename of the page layout.</param>
        /// <param name="ensureNotCheckedOut">If true ensures that the page layout file/listitem is not checked out.</param>
        /// <returns></returns>
        private SPListItem LoadPageLayoutListItem(string filename, bool ensureNotCheckedOut)
        {
            SPListItem pageLayoutListItem = ElevatedWebContext.GetListItem("_catalogs/masterpage/" + filename);
            if (pageLayoutListItem == null)
            {
                throw new ApplicationException(string.Format("Page layout '{0}' could not be found in masterpage-library.", filename));
            }

            if (ensureNotCheckedOut)
            {
                // Check checkout-status of page-layout: If checked out, we cannot proceed.
                if (pageLayoutListItem.File.CheckOutStatus != SPFile.SPCheckOutStatus.None)
                {
                    throw new ApplicationException(string.Format("Page layout '{0}' is checked out and configuration cannot be processed", filename));
                }
            }
            return pageLayoutListItem;
        }

No to the complicated part of the solution: Processing the WebPart configuration per page layout. There are two possibilities: Either the WebPart is already present (then we want to update all WebPart-Properties) or is is not (then we want to add it):

        /// <summary>
        /// Processes a webPart-configuration node on a page layout. This will result in either the addition of a WebPart to the
        /// page layout or the (properties-)update of an existing WebPart.
        /// </summary>
        /// <param name="wpManager">The SPLimitedWebPartManager of the page layout.</param>
        /// <param name="webPartConfiguration">The webPart-configuratin node of the page layout.</param>
        /// <param name="pageLayoutListItem">The SPListItem-instance of the page layout.</param>
        private void ProcessWebPartConfiguration(SPLimitedWebPartManager wpManager, XPathNavigator webPartConfiguration,
            SPListItem pageLayoutListItem)
        {
            string errorMsg = null;
            string webPartFilename = webPartConfiguration.GetAttribute("filename", "");
            string webPartFilepath = _properties.Definition.RootDirectory + "
\WebParts\" +
                webPartConfiguration.GetAttribute("filename", "");
            string webPartUniqueID = webPartConfiguration.GetAttribute("ID", "").ToLower();
            string webPartZoneID = webPartConfiguration.GetAttribute("zoneID", "");
            int webPartZoneIndex = Convert.ToInt32(webPartConfiguration.GetAttribute("index", ""));

            //Logger.Write(this, string.Format("Processing webPart '{0}'.", webPartFilename),
            //    System.Diagnostics.TraceEventType.Information, TraceEventCategory.FeatureReceiver);

            // Import the webpart into memory by loading the webpart definition file
            FileStream wpFileStream = new FileStream(webPartFilepath, FileMode.Open, FileAccess.Read);
            XmlTextReader wpXmlReader = new XmlTextReader(wpFileStream);
            System.Web.UI.WebControls.WebParts.WebPart configWebPart = wpManager.ImportWebPart(wpXmlReader, out errorMsg);
            if (!string.IsNullOrEmpty(errorMsg))
            {
                throw new ApplicationException(string.Format("Error importing webPart '{0}': {1}", webPartFilename,
                    errorMsg));
            }
            else
            {
                // Check if webpart already exists – must get the real id from the web
                string webPartInternalID = pageLayoutListItem.Properties["PageLayoutWebPart_" + webPartUniqueID] as string;
                if (!string.IsNullOrEmpty(webPartInternalID) && wpManager.WebParts[webPartInternalID] != null)
                {
                    // The WebPart is already present on the page layout – we need to update it's properties now
                    //Logger.Write(this, string.Format("WebPart '{0}' is present on page layout '{1}'. Updating…",
                    //    webPartFilename, pageLayoutListItem.File.Name), System.Diagnostics.TraceEventType.Verbose,
                    //    TraceEventCategory.FeatureReceiver);

                    System.Web.UI.WebControls.WebParts.WebPart layoutWebPart = wpManager.WebParts[webPartInternalID];

                    PropertyInfo[] pinProperties = configWebPart.GetType().GetProperties(
                        BindingFlags.Public | BindingFlags.Instance);

                    foreach (PropertyInfo pinProperty in pinProperties)
                    {
                        if (pinProperty.CanWrite)
                        {
                            layoutWebPart.GetType().GetProperty(pinProperty.Name).SetValue(layoutWebPart,
                                pinProperty.GetValue(configWebPart, null), null);
                        }
                    }

                    wpManager.SaveChanges(layoutWebPart);

                    //Logger.Write(this, string.Format("WebPart '{0}' on page layout '{1}' updated.",
                    //    webPartFilename, pageLayoutListItem.File.Name), System.Diagnostics.TraceEventType.Verbose,
                    //    TraceEventCategory.FeatureReceiver);
                }
                else
                {
                    // The WebPart is either not registered on the SPWeb or it has been removed from the page layout.
                    if (!string.IsNullOrEmpty(webPartInternalID))
                    {
                        // Must be an orphaned registration in the SPWeb, so take it away.
                        pageLayoutListItem.Properties.Remove("PageLayoutWebPart_" + webPartUniqueID);
                    }

                    // The WebPart is already present on the page layout – we need to update it's properties now
                    //Logger.Write(this, string.Format("WebPart '{0}' not on page layout '{1}'. Adding…", webPartFilename,
                    //    pageLayoutListItem.File.Name), System.Diagnostics.TraceEventType.Verbose, TraceEventCategory.FeatureReceiver);

                    wpManager.AddWebPart(configWebPart, webPartZoneID, webPartZoneIndex);

                    // Register the WebPart's UniqueID provided in the configuration
                    pageLayoutListItem.Properties.Add("PageLayoutWebPart_" + webPartUniqueID, configWebPart.ID);

                    //Logger.Write(this, string.Format("WebPart '{0}' added to page layout '{1}'.", webPartFilename,
                    //    pageLayoutListItem.File.Name), System.Diagnostics.TraceEventType.Information, TraceEventCategory.FeatureReceiver);

                }
            }
        }

In the last method, we are deleting redundant WebParts from the Page Layout so we have again a clean state for both, WebParts and Properties of the List Item:

        /// <summary>
        /// Deletes redundant WebParts that are registered on the page layout but not (anymore) in the configuration file.
        /// </summary>
        /// <param name="wpManager">The SPLimitedWebPartManager of the page layout.</param>
        /// <param name="pageLayoutConfiguration">The whole configuratin node of the page layout.</param>
        /// <param name="pageLayoutListItem">The SPListItem-instance of the page layout.</param>
        private void DeleteRedundantWebParts(SPLimitedWebPartManager wpManager, XPathNavigator pageLayoutConfiguration,
            SPListItem pageLayoutListItem)
        {
            // Loop through all pageLayoutItemProperties to find WebParts that are not anymore in the configuration
            List<string> pageLayoutItemPropertiesToDelete = new List<string>();
            foreach (string pageLayoutItemPropertyKey in pageLayoutListItem.Properties.Keys)
            {
                if (pageLayoutItemPropertyKey.StartsWith("PageLayoutWebPart_"))
                {
                    string webPartUniqueID = pageLayoutItemPropertyKey.Substring(pageLayoutItemPropertyKey.IndexOf("_") + 1);
                    if (pageLayoutConfiguration.SelectSingleNode("webPart[translate(@ID, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz')='"
                        + webPartUniqueID + "']") == null)
                    {
                        //Logger.Write(this, string.Format("WebPart with ID '{0}' on page layout '{1}' is redundant. Deleting",
                        //     webPartUniqueID, pageLayoutListItem.File.Name), System.Diagnostics.TraceEventType.Verbose,
                        //     TraceEventCategory.FeatureReceiver);

                        wpManager.DeleteWebPart(wpManager.WebParts[pageLayoutListItem.Properties[pageLayoutItemPropertyKey] as string]);
                        pageLayoutItemPropertiesToDelete.Add(pageLayoutItemPropertyKey);

                        //Logger.Write(this, string.Format("Redundant WebPart with ID '{0}' on page layout '{1}' deleted.",
                        //     webPartUniqueID, pageLayoutListItem.File.Name), System.Diagnostics.TraceEventType.Information,
                        //     TraceEventCategory.FeatureReceiver);
                    }
                }
            }
            foreach (string pageLayoutItemPropertyToDelete in pageLayoutItemPropertiesToDelete)
            {
                pageLayoutListItem.Properties.Remove(pageLayoutItemPropertyToDelete);
            }
        }

Here you go – compile the solution (give that you've replaced all placeholders with your namespaces, assembly names, publickeytokens) and deploy it to your SharePoint. Activate the feature and create a new page out of the page layouts you've configured with WebParts. Want to add more, less or differently configured WebParts? Change your feature or also only the configuration file (don't forget IISReset to force MOSS to read the 12-hive feature files again), deactivate and activate your feature – there you go.

A small last comment on this series of posts: The code is testes in a rather complex environment and seems to be working pretty well. I've adjusted (mainly made it "anonymous") my original code so maybe there might be a chance for some bugs. So please take it "AS IS", I cannot nor want to guarantee any support while implementing this solution – and of course it is not "SharePoint standard" Wink But for sure it ended some of my nightmares – i hope it does for you too.

 

 

Leave a Reply