In SharePoint 2010, if a page layout contains web parts, then when you change the page layout via the Ribbon while editing the page, then the page will have web parts of both the previous and the new page layouts.
Update: An alternate, and potentially more comprehensive, approach to handle this is added at the end of the post.
This is observed with SP1 and August 2011 CU, and occurred to both custom and OOTB page layouts.
A similar issue is when you reactivate the feature that provisions the custom page layout. In this case, the web parts are duplicated on the custom page layout itself. Waldek Mastykarz has blogged about a solution for this case: http://blog.mastykarz.nl/preventing-provisioning-duplicate-web-parts-instances-on-feature-reactivation/.
Back to the problem at hand. I tried everything in my custom page layout and could not stop the web parts from being duplicated when the user changes the page layout. I was inspired by Waldek’s solution above and eventually solved the problem by adding a code-behind for the page layout. In the code-behind I detect when the ChangePageLayout event is just about to occur. I then removes all the web parts defined in the current page layout from the page. Below is a walkthrough of the code.
First create a public class and inherit from PublishingLayoutPage (you will need a reference to Microsoft.SharePoint.Publishing.dll). Override the OnLoad method and add the code as shown below.
public class MyLayoutPage : PublishingLayoutPage { protected override void OnLoad(EventArgs e) { base.OnLoad(e); if (SPContext.Current.FormContext.FormMode != SPControlMode.Display && this.IsCurrentPostBackChangePageLayoutEvent) { var currentPageLayoutWebParts = GetCurrentPageLayoutWebParts(); RemoveMatchingWebPartsFromCurrentPage(currentPageLayoutWebParts); //We do not need to save the page as SharePoint will save the page as part of the ChangePageLayoutEvent. } } }
Add the code below for the this.IsCurrentPostBackChangePageLayoutEvent property. This was the tricky part to figure out. The SharePoint object model does not have any mean to hook into this event. However, as part of the ASP.NET framework, a form parameter is included in the HttpRequest to identify the postBack event.
private bool IsCurrentPostBackChangePageLayoutEvent { get { return this.Request["__EVENTARGUMENT"] == "ChangePageLayoutEvent"; } }
Add the code below for the GetCurrentPageLayoutWebParts() method. Essentially we get the current page layout of the page, get the list item and ASPX file that represent the page layout, and return the web parts defined on that page layout. The only messy thing here is we are returning a Dictionary of web parts, where the value is the ZoneID of the web part. This is further explained in the in-code comment.
/// <summary> /// Returns a dictionary of web parts defined in the page layout of the current page. The key is the web part object. /// The value is the Zone ID of the web part. /// </summary> /// <remarks>We need to return a dictionary here because we also want to return the Zone ID of the web part for comparison later. /// The WebPart class we are using is System.Web.UI.WebControls.WebPart, and this does not expose the ZoneID property. The /// Zone property of the WebPart for some reason is always null.</remarks> /// <returns></returns> private Dictionary<WebPart, string> GetCurrentPageLayoutWebParts() { var webPartDictionary = new Dictionary<WebPart, string>(); var currentPageLayoutValue = this.CurrentPageLayout; if (String.IsNullOrEmpty(currentPageLayoutValue)) { return webPartDictionary; } //The page layout value will be of the format [absoluteUrl], [content type]. We will need to get the //URL, and use that to retrieve the page layout list item in the catalog. var currentPageLayoutUrl = currentPageLayoutValue.Split(new[] { ", " }, StringSplitOptions.RemoveEmptyEntries)[0]; try { //Official MS guidance is not to call Dispose() on SPSite.RootWeb SPListItem currentPageLayout = SPContext.Current.Site.RootWeb.GetListItem(currentPageLayoutUrl); var webPartManager = currentPageLayout.File.GetLimitedWebPartManager(PersonalizationScope.Shared); var webParts = webPartManager.WebParts.OfType<WebPart>().ToList(); webParts.ForEach(w => webPartDictionary.Add(w, webPartManager.GetZoneID(w))); if (webPartManager.Web != null) { webPartManager.Web.Dispose(); } webPartManager.Dispose(); return webPartDictionary; } catch (FileNotFoundException e) { //TODO: Log this return webPartDictionary; } } private string CurrentPageLayout { get { try { return SPContext.Current.ListItem["PublishingPageLayout"] as string; } catch (ArgumentException) { //This will occur if the page does not have a page layout associated, e.g. when we open //the page layout itself (as oppose to the content page). return String.Empty; } } }
Add the code below for the RemoveMatchingWebPartsFromCurrentPage() method. The comparison is by web part’s Title, Type and Zone ID.
/// <summary> /// Web parts are matched on Title, Type and Zone ID. /// </summary> /// <param name="webPartsToMatch">A dictionary of the web parts to match (i.e. to find and delete on the current page). /// The key is the web part object. The value is the Zone ID of the web part.</param> private void RemoveMatchingWebPartsFromCurrentPage(Dictionary<WebPart, string> webPartsToMatch) { var webPartManager = SPContext.Current.File.GetLimitedWebPartManager(PersonalizationScope.Shared); foreach (WebPart toMatchWebPart in webPartsToMatch.Keys) { var matches = webPartManager.WebParts.OfType<WebPart>().Where( w => w.Title == toMatchWebPart.Title && w.GetType() == toMatchWebPart.GetType() && webPartManager.GetZoneID(w) == webPartsToMatch[toMatchWebPart]); foreach (var match in matches.ToList()) { webPartManager.DeleteWebPart(match); } } if (webPartManager.Web != null) { webPartManager.Web.Dispose(); } webPartManager.Dispose(); }
Lastly we will need to hook up our code-behind class to our page layout. For all the custom page layouts, locate the <%@ Page %> directive and change the Inherits attribute from the OOTB class to our class:
<%@ Page language="C#" Inherits="[fullNamespace].[className], [strongAssemblyName]" meta:progid="SharePoint.WebPartPage.Document" %>
That’s it. Go ahead and deploy your solution.
——————————–
Update: There is one drawback with the above method, and that is it won’t work if the user changes from an OOTB page layout to a new page layout. Obviously this is because our code-behind is not hooked up to the OOTB page layouts.
Another way to handle this, which will also work for the OOTB page layouts, is to attach an event receiver to the Pages library and detect the ChangePageLayout event there. With this approach you do not need code-behind for the page layouts. You however will need to attach this event receiver to all existing and new sites – which may (or not) be easier for you.
The code above can be adapted to work inside an event receiver so I won’t go over it again. I will highlight a few key points:
- The event to perform the work is the ItemUpdating event. When the user changes the page layout, SharePoint automatically saves the page.
- While you can compare properties.BeforeProperties[“PublishingPageLayout”] and properties.AfterProperties[“PublishingPageLayout”] to see if the page layout has changed, you should still rely on the PostBack event argument (like we did for the code-behind) as your best check. This is because the event still occurs (and the web parts still get duplicated) if the user changes to the current page layout – and when this happens the BeforeProperties and AfterProperties will be the same!
- To check the PostBack event argument you will need to access HttpContext.Current. This is accessible only within the constructor of the event receiver. Create an instance variable and set it to HttpContext.Current so you can reuse it afterward. See code below.
- HttpContext.Current will sometime not be available at all, e.g. when the item is being updated via a console app, or when the web is being created. Therefore, you should also check the Before/AfterProperties as a fall back.
The code below illustrates the above points:
public class PagesLibraryItemEventReceiver : SPItemEventReceiver { private readonly HttpContext _httpContext; public PagesLibraryItemEventReceiver() : base() { _httpContext = HttpContext.Current; } //Other code omitted... private bool IsPageLayoutChanging(SPItemEventProperties properties) { if (_httpContext != null) { return _httpContext.Request["__EVENTARGUMENT"] == "ChangePageLayoutEvent"; } /*The page layout value is 1 of 2 formats: * http://server/_catalogs/masterpage/mypagelayout1.aspx, Content Page 1 * /_catalogs/masterpage/mycontentpagelayout2.aspx, Content Page 2 * * We therefore will take a substring starting from the last '/' and compare the 2 values. * */ var currentPageLayout = properties.BeforeProperties["PublishingPageLayout"] as string ?? String.Empty; var newPageLayout = properties.AfterProperties["PublishingPageLayout"] as string ?? String.Empty; if (!String.IsNullOrEmpty(currentPageLayout)) { currentPageLayout = currentPageLayout.Substring(currentPageLayout.LastIndexOf('/') + 1); } if (!String.IsNullOrEmpty(newPageLayout)) { newPageLayout = newPageLayout.Substring(newPageLayout.LastIndexOf('/') + 1); } return !currentPageLayout.Equals(newPageLayout, StringComparison.InvariantCultureIgnoreCase); } }
Hope this helps!
This looks along the right lines for what I’m after – but there seems to be a few bugs (IsPageLayoutChanging doesn’t return a bool at all paths for example). If you can take a look and e-mail me daniel.mcnulty@gmx.com I’d seriously appreciate it; been stuck on this problem for a few hours now.
Thanks.
Ignore that, the IsPageLayoutChanging method is fine.
Hi Daniel – we have implemented this approach in 2 systems and it is working fine so let me know if you can’t get it working.
Hi Bernado,
There is one drawback in the solution using PublishingLayoutPage and it is :
If the user modifies any web part and then RemoveMatchingWebPartsFromCurrentPage deletes it from Page that will throw an error as the Page will try to save properties of no longer existing web part.
In that case EventReceiver seemed to be the correct approach.
Regards,
Milen
Pingback: Curia Damiano blog | SharePoint: preventing duplicate web parts