Não é possível excluir a página mestra personalizada programaticamente durante a desativação do recurso 2013
Pergunta
Atualmente tenho uma página mestra personalizada que está sendo implantada no escopo do Site.Depois de implantado e o recurso ativado, a página mestra fica disponível para uso.Consigo ativar o recurso e definir a página mestra em sites e os subsites herdam a página mestra corretamente.
O problema ocorre quando tento desativar o recurso.
Uma vez desativado, tenho um receptor de eventos que encontra todos os sites que possuem a página mestra do recurso definida e a define de volta para a página mestra padrão.Isso também inclui a configuração da página mestra em sites herdados.
Isso funciona bem, mas depois de concluído, tento excluir a página mestra da galeria de páginas mestras e ocorre um erro informando que ela ainda está em uso.Quando verifico os sites por meio da GUI, todos eles foram transferidos para Seattle e os sites herdados ainda estão herdando do pai, então tudo parece bem nesse sentido.
Quando eu entro no Conteúdo e Estrutura do conjunto de sites e procurar as páginas relacionadas à página mestra, ainda há um relacionamento com o _DeviceChannelMappings.aspx páginas para todos os sites que herdam a página mestra do pai.Veja abaixo:
Não consegui encontrar uma maneira de remover programaticamente esse relacionamento e, por isso, não consigo excluir a página mestra da biblioteca de catálogos.
Se eu for manualmente para o conjunto de sites raiz na GUI e verificar o Redefina todos os subsites para herdar esta configuração de página mestra do site usando a página mestre de Seattle, os relacionamentos desaparecerão e o arquivo poderá ser excluído.
Qualquer ajuda seria apreciada.Aqui está meu código atual para a desativação do recurso:
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)
{
}
}
}
}
Solução 2
Não sei se esta é a melhor maneira de corrigir esse problema, mas consegui resolvê-lo editando manualmente o __DeviceChannelMappings.aspx no Desativação de recurso.Isso liberará os mapeamentos para a página mestra personalizada e permitirá que ela seja excluída.
Aqui está o código final:
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)
{
}
}
}
}
Outras dicas
Parece que sua página mestra está sendo usada por canais de dispositivos em seus sites.A Microsoft não fornece uma API pública para visualizar ou modificar as páginas mestras configuradas para um canal de dispositivo.Dito isso, em meu livro (SharePoint 2013 WCM Advanced Cookbook: http://tinyurl.com/lutktay), tenho alguns exemplos de como visualizar e modificar as páginas mestras configuradas para canais de dispositivos.
Aqui está o código de exemplo C# para visualizar os canais do dispositivo:
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();
}
}
}
E aqui está o código de exemplo C# para configurar os canais do dispositivo:
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();
}
}
}
Com esses dois exemplos, você terá o que precisa para obter os canais de dispositivos de cada site e garantir que eles não estejam usando sua página mestra.