4

I currently have a custom master page that is being deployed at the Site scope. Once deployed and the feature is activated it makes the master page available for use. I am able to activate the feature and set master page on sites and subsites inherit the master page properly.

The problem occurs when I attempt to deactivate the feature.

Once deactivated I have a event receiver that finds all sites that have the feature's master page set and sets it back to the default master page. This also includes setting the master page on inheriting sites.

This works fine but after this has completed I attempt to delete the master page from the master page gallery and it errors stating that it is still in use. When I check the sites through the GUI they have all been set back to Seattle and inheriting sites are still inheriting from the parent so everything looks fine on that end.

When I go into the Content and Structure of the site collection and look up the master page related pages there is still a relationship with the _DeviceChannelMappings.aspx pages for all sites that are inheriting it's master page from the parent. See below:

Master Page Relationships

I haven't been able to find a way programmatically to remove this relationship and because of that I am unable to delete the master page from the catalogs library.

If I manually go to the root site collection in the GUI and check the Reset all subsites to inherit this site master page setting using the seattle masterpage the relationships will go away and the file is then able to be deleted.

Any help would be appreciated. Here is my current code for the feature deactivation:

public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
    {
        using (SPSite siteCollection = (SPSite)properties.Feature.Parent)
        {
            string defaultMasterUrl = SPUrlUtility.CombineUrl(siteCollection.ServerRelativeUrl.EndsWith("/") ? siteCollection.ServerRelativeUrl : siteCollection.ServerRelativeUrl + "/" , "_catalogs/masterpage/seattle.master");
            string pulseMasterUrl = SPUrlUtility.CombineUrl(siteCollection.ServerRelativeUrl.EndsWith("/") ? siteCollection.ServerRelativeUrl : siteCollection.ServerRelativeUrl + "/" , "_catalogs/masterpage/pulse.v01.master");
            foreach (SPWeb web in siteCollection.AllWebs)
            {
                Hashtable hash = web.AllProperties;
                if (hash["__InheritsMasterUrl"].ToString() == "True" && !web.IsRootWeb)
                {
                    web.MasterUrl = web.ParentWeb.MasterUrl;
                    web.Update();
                }
                else if (web.MasterUrl == pulseMasterUrl)
                {
                    web.MasterUrl = defaultMasterUrl;
                    web.Update();
                }

                if (hash["__InheritsCustomMasterUrl"].ToString() == "True" && !web.IsRootWeb)
                {
                    web.CustomMasterUrl = web.ParentWeb.CustomMasterUrl;
                    web.Update();
                }
                else if (web.CustomMasterUrl == pulseMasterUrl)
                {
                    web.CustomMasterUrl = defaultMasterUrl;
                    web.Update();
                }
            }

            foreach (SPWeb web in siteCollection.AllWebs)
            {
                try
                {
                    SPFile file = web.GetFile(pulseMasterUrl);
                    if (file.Exists)
                    {
                        file.Delete();
                    }
                    file.Update();
                }
                catch (Exception ex)
                {
                }
            }
        }
    }
2

Looks like your master page is being used by device channels throughout your sites. Microsoft doesn't provide a public API for viewing or modifying the configured master pages for a device channel. With that said, in my book (SharePoint 2013 WCM Advanced Cookbook: http://tinyurl.com/lutktay), I have some samples on how to view and modify the configured master pages for device channels.

Here is the C# sample code for viewing the device channels:

namespace Code6587EN.Ch02.GetDeviceChannelMaps
{
    using Microsoft.SharePoint;
    using System;
    using System.Collections;
    using System.Reflection;

    /// <summary>
    /// Console Application to get the Device Channel mappings for each
    /// Site in a Site Collection
    /// </summary>
    class Program
    {
        static void Main(string[] args)
        {
            // Get the Site Collection in a Using statement
            using (var site = new SPSite("http://sharepoint/sitecollection"))
            {
                // Get the Mappings File type and constructor
                var typeMappingFile = Type.GetType("Microsoft.SharePoint.Publishing.Mobile.MasterPageMappingsFile, Microsoft.SharePoint.Publishing, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c");                
                var consMappingFile = typeMappingFile.GetConstructor(new Type[] {typeof(SPWeb), typeof(bool), typeof(SPWeb)});

                // Iterate through each Site in the Site Collection
                foreach (SPWeb web in site.AllWebs)
                {
                    // Ensure the Site exists
                    if (web.Exists)
                    {
                        // Get the Mapping File for the Site
                        var mappingFile = consMappingFile.Invoke(new object[] { web, false, null });

                        // Output the Default channel details
                        Console.WriteLine("");
                        Console.WriteLine("Site: " + web.Url);
                        Console.WriteLine("Device Channel: Default");
                        Console.WriteLine("Master Page: " + web.CustomMasterUrl);

                        // Get the mappings field from the Mapping File and cast as the IDictionary interface
                        var mappings = (IDictionary)typeMappingFile.GetField("mappings", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(mappingFile);

                        // Iterate through each key in the IDictionary
                        foreach (var key in mappings.Keys)
                        {
                            // Get the Master Page Url property from the mapping object
                            var mappingObject = mappings[key];
                            var masterUrl = (string)mappingObject.GetType().GetProperty("MasterPageUrl", BindingFlags.Instance | BindingFlags.Public).GetValue(mappingObject, null);

                            // Output the Channel details
                            Console.WriteLine("");
                            Console.WriteLine("Site: " + web.Url);
                            Console.WriteLine("Device Channel: " + key);
                            Console.WriteLine("Master Page: " + masterUrl);
                        }

                        // Dispose the Site object
                        web.Dispose();
                    }
                }
            }

            // Wait for a key to be pressed before closing the application
            Console.WriteLine("Press Any Key to Continue...");
            Console.Read();
        }
    }
}

And here is the C# sample code for setting the device channels:

namespace Code6587EN.Ch02.ApplyMasterToChannel
{
    using Microsoft.SharePoint;
    using System;
    using System.Collections;
    using System.Reflection;

    /// <summary>
    /// Console Application to apply a Master Page to a Device Channel
    /// </summary>
    class Program
    {
        static void Main(string[] args)
        {
            // Get the Site Collection in a Using statement
            using (var site = new SPSite("http://sharepoint/sitecollection"))
            {
                // Get the Mappings File type and constructor
                var typeMappingFile = Type.GetType("Microsoft.SharePoint.Publishing.Mobile.MasterPageMappingsFile, Microsoft.SharePoint.Publishing, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c");                
                var consMappingFile = typeMappingFile.GetConstructor(new Type[] {typeof(SPWeb), typeof(bool), typeof(SPWeb)});

                // Get the root Site in a Using statement
                using (var web = site.RootWeb)
                {                   
                    // Get the Mapping File
                    var mappingFile = consMappingFile.Invoke(new object[] { web, false, null });

                    // Get the Mappings and cast to an IDictionary
                    var mappings = (IDictionary)typeMappingFile.GetField("mappings", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(mappingFile);

                    // Set the Master Page Url of the mapping object 
                    mappings["PowerShell"].GetType().GetProperty("MasterPageUrl", BindingFlags.Instance | BindingFlags.Public).SetValue(mappings["PowerShell"], "/_catalogs/masterpage/seattle.master", null);

                    // Set the updated Mappings on the Mappings file
                    typeMappingFile.GetField("mappings", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(mappingFile, mappings);

                    // Get and invoke the Update Single Channel method
                    var updateMethod = typeMappingFile.GetMethod("UpdateSingleChannel", BindingFlags.Instance | BindingFlags.Public, null, new Type[] { typeof(string) }, null);
                    updateMethod.Invoke(mappingFile, new object[] { "PowerShell" });
                }
            }

            // Wait for a key to be pressed before closing the application
            Console.WriteLine("Press Any Key to Continue...");
            Console.Read();
        }
    }
}

With these two samples, you should have what you need to get the device channels for each site and ensure they aren't using your master page.

  • Thanks for the examples. I guess I should mention as well that I am only using the Default device channel and have not specified any custom ones.When I run the console application to view all of the device channels it comes back with all of the sites stating they are using the /_catalogs/masterpage/seattle.master as their master page although within the Content and Structure it still states there's a relationship to that aspx page. When I attempt to run the second console application I get a nullreferenceexception on the mappings["PowerShell"]... line. – dsara May 22 '14 at 22:35
  • PowerShell just happened to be the name of the mapping. If you throw the first sample in a console application (replacing the URL for your site), do any sites show a mapping other than the default? (You might pipe the output so you can read it in notepad "app.exe > output.txt"). – John Chapman May 23 '14 at 13:51
  • I'm able to run the first console application against my site collection successfully and it brings back the Default channel for every site and for every Default channel it returns /_catalogs/masterpage/seattle.master. There are no references to any custom masterpage in the output. If I go into Content and Structure in the web interface on the site collection it still states there is an association with my custom masterpage and _DeviceChannelMappings.aspx for every site that inherits from it's parent, even though I cannot find a reference to the custom masterpage elsewhere. – dsara May 23 '14 at 15:33
  • If you open the device channel mappings file in SharePoint Designer on one of the sites, does it reference your master page in someway? – John Chapman May 23 '14 at 16:33
  • Yeah, on the sites that are having problems this what it looks like: <defaultChannelMapping> <siteMasterUrl token="~sitecollection/_catalogs/masterpage/pulse.v01.master" href="/_catalogs/masterpage/pulse.v01.master" /> <systemMasterUrl token="~sitecollection/_catalogs/masterpage/seattle.master" href="/_catalogs/masterpage/seattle.master" /> <alternateCssUrl token="" href="" /> <themedCssFolderUrl token="" href="" isthemeshared="false" /> </defaultChannelMapping> Is there a way to manually edit these files during a feature deactivation? – dsara May 23 '14 at 18:10
1

I don't know if this is the best way to fix this problem but I was able to resolve the issue by manually editing the __DeviceChannelMappings.aspx on the Feature Deactivation. This will release the mappings to the custom master page and allow it to be deleted.

Here is the final code:

public class PulseMasterPageEventReceiver : SPFeatureReceiver
{
    public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
    {
        using (SPSite siteCollection = (SPSite)properties.Feature.Parent)
        {
            string defaultMasterUrl = SPUrlUtility.CombineUrl(siteCollection.ServerRelativeUrl.EndsWith("/") ? siteCollection.ServerRelativeUrl : siteCollection.ServerRelativeUrl + "/" , "_catalogs/masterpage/seattle.master");
            string pulseMasterUrl = SPUrlUtility.CombineUrl(siteCollection.ServerRelativeUrl.EndsWith("/") ? siteCollection.ServerRelativeUrl : siteCollection.ServerRelativeUrl + "/" , "_catalogs/masterpage/pulse.v01.master");

            foreach (SPWeb web in siteCollection.AllWebs)
            {
                web.AllowUnsafeUpdates = true;
                Hashtable hash = web.AllProperties;
                if (hash["__InheritsMasterUrl"].ToString() == "True" && !web.IsRootWeb)
                {
                    web.MasterUrl = web.ParentWeb.MasterUrl;
                    web.Update();
                }
                else if (web.MasterUrl == pulseMasterUrl)
                {
                    web.MasterUrl = defaultMasterUrl;
                    web.Update();
                }

                if (hash["__InheritsCustomMasterUrl"].ToString() == "True" && !web.IsRootWeb)
                {
                    web.CustomMasterUrl = web.ParentWeb.CustomMasterUrl;
                    web.Update();
                }
                else if (web.CustomMasterUrl == pulseMasterUrl)
                {
                    web.CustomMasterUrl = defaultMasterUrl;
                    web.Update();
                }
                web.AllowUnsafeUpdates = false;
            }

            foreach (SPWeb web in siteCollection.AllWebs)
            {
                string deviceChannelMappings = SPUrlUtility.CombineUrl(web.Url.EndsWith("/") ? web.Url : web.Url + "/", "_catalogs/masterpage/__devicechannelmappings.aspx");
                SPFile dcmFile = web.GetFile(deviceChannelMappings);
                Stream dcmFileStream = dcmFile.OpenBinaryStream();
                Stream dcmFileWrite = new MemoryStream();

                using (StreamWriter sw = new StreamWriter(dcmFileWrite))
                using (StreamReader sr = new StreamReader(dcmFileStream))
                {
                    string line;
                    bool foundCorrection = false;
                    while ((line = sr.ReadLine()) != null)
                    {
                        if (line.Contains("pulse.v01.master"))
                        {
                            foundCorrection = true;
                            line = line.Replace("pulse.v01.master", "seattle.master");
                        }
                        sw.WriteLine(line);
                    }
                    sw.Flush();
                    if (foundCorrection)
                    {
                        if (dcmFile.CheckOutType != SPFile.SPCheckOutType.None)
                            dcmFile.UndoCheckOut();
                        if (dcmFile.RequiresCheckout)
                        {
                            dcmFile.CheckOut();
                            dcmFile.SaveBinary(dcmFileWrite);
                            dcmFile.CheckIn("Updated master page references to default.");
                        }
                        else
                        {
                            dcmFile.SaveBinary(dcmFileWrite);
                        }
                        if (dcmFile.Level == SPFileLevel.Draft)
                        {
                            dcmFile.Publish("Updated master page references to default.");
                        }
                        dcmFile.Update();
                    }
                }
            }

            foreach (SPWeb web in siteCollection.AllWebs)
            {
                try
                {
                    SPFile file = web.GetFile(pulseMasterUrl);
                    if (file.Exists)
                    {
                        file.Delete();
                    }
                    file.Update();
                }
                catch (Exception ex)
                {
                }
            }
        }
    }

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.