Arquitectura, buenas prácticas y desarrollo sobre la nueva herramienta de Microsoft SharePoint 2016

Office 365: Aprovisionando el Branding

Sharepoint_Branding_SmallEn la entrada de la semana anterior vimos como se podía aprovisionar los Displays Templates en un SharePoint OnPremise. A raíz de varias preguntas que me han realizado, creo que es la hora de abordar el tratamiento del aprovisionamiento de los sitios alojados en SharePoint Online. En este primer post vamos a ver como podemos aprovisionar los aspectos relativos al branding dentro de SharePoint Online.

Introducción

La personalización es uno de los aspectos que más requerimientos tienen los clientes a la hora de adoptar la plataforma, quieren que su Site Collection tenga por lo menos su identidad corporativa  (logo, tamaño/fuente de letra, colores,etc…) Todo esto es posible con SharePoint Online, aunque hay que tener ciertos aspectos a la hora  de realizar esta customización (todos estos aspectos los podemos aprender aprender y conocer dentro del proyecto GitHub Patterns and Practices ) Aunque la idea inicial y recomendada por Microsoft es limitar la customización de la site collection a la minima expresión y realizar esta customización en la APP correspondiente, vamos a ver como poder aplicar un poco de diseño a nuestro SharePoint Online.

Como opinión personal, creo que estos dos aspectos son distintos: por un lado aplicar estilo a un SharePoint Online debe de ser posible y por otro lado la customización excesiva y vinculada a la versión, es más un desarrollo a medida y por lo tanto nuestra forma de enfocar este proyecto debería de ser utilizando el modelo APP.

Manos a la obra

El primer paso que es definir que artefactos vamos a desplegar:

  • Hojas de estilo, ficheros Javascript, imagenes
  • Master Page
  • PageLayouts

Despliegue de Ficheros

En soluciones OnPremise el despliegue de ficheros era de una forma muy sencilla haciendo uso de Modulos desde Visual Studio, indicarle la ruta y activar la feature. Para realizar esto en SharePoint Online, en primer lugar tendríamos que utilizar soluciones SandBox que están deprecated. El que estén deprecated no significa que no se puedan utilizar, pero si que es conveniente no utilizarlas por un motivo principal: no sabemos si en futuras versiones tendremos compatibilidad.

Descartada la opción Sandbox, nos quedaría utilizar PowerShell o bien utilizar el modelo de objetos de Cliente (CSOM). Si estuviéramos en un entorno OnPremises no tendríamos ninguna  duda  con PowerShell tenemos una API muy rica, junto con toda la potencia del lenguaje  pero desgraciadamente en SharePoint Online los cmdlets de PowerShell están bastante limitados. La diferencia entre los Cmdlets que hay en Online en OnPremise es 700/30.  Por lo que la única opción que disponemos es de utilizar CSOM. Una practica muy habitual es crear los propios Cmdlet haciendo uso de CSOM y de esta forma disponer de un gran número de comandos PowerShell para facilitar la vida en el desarrollo en SharePoint Online.

Para subir un fichero, lo que vamos a realizar es por un lado comprobar si la ruta donde vamos a alojar el fichero existe, comprobar que si el fichero existe no esta desprotegido, subir el fichero y posteriormente publicarlo y aprobarlo. Para ello utilizaremos un función como la siguiente:

public static void UploadFile(ClientContext clientContext, string name, string folder, string path)
        {
            var web = clientContext.Web;
            var filePath = web.ServerRelativeUrl.TrimEnd(Program.trimChars) + "/" + path + "/";
           Console.WriteLine("Uploading file {0} to {1}{2}", name, filePath, folder);
            EnsureFolder(web, filePath, folder);
            CheckOutFile(web, name, filePath, folder);
            var uploadFile = AddFile(web.Url, web, "Branding\\Files\\", name, filePath, folder);
            CheckInPublishAndApproveFile(uploadFile);
        }

La definición de la función EnsureFolder es la siguiente

private static void EnsureFolder(Web web, string filePath, string fileFolder)
        {
            if (string.IsNullOrEmpty(fileFolder))
            {
                return;
            }

            var lists = web.Lists;
            web.Context.Load(web);
            web.Context.Load(lists, l => l.Include(ll => ll.DefaultViewUrl));
            web.Context.ExecuteQuery();

            ExceptionHandlingScope scope = new ExceptionHandlingScope(web.Context);
            using (scope.StartScope())
            {
                using (scope.StartTry())
                {
                    var folder = web.GetFolderByServerRelativeUrl(string.Concat(filePath, fileFolder));
                    web.Context.Load(folder);
                }

                using (scope.StartCatch())
                {
                    var list = lists.Where(l => l.DefaultViewUrl.IndexOf(filePath, StringComparison.CurrentCultureIgnoreCase) >= 0).FirstOrDefault();

                    ListItemCreationInformation newFolder = new ListItemCreationInformation();
                    newFolder.UnderlyingObjectType = FileSystemObjectType.Folder;
                    newFolder.FolderUrl = filePath.TrimEnd(Program.trimChars);
                    newFolder.LeafName = fileFolder;

                    ListItem item = list.AddItem(newFolder);
                    web.Context.Load(item);
                    item.Update();
                }

                using (scope.StartFinally())
                {
                    var folder = web.GetFolderByServerRelativeUrl(string.Concat(filePath, fileFolder));
                    web.Context.Load(folder);
                }
            }

            web.Context.ExecuteQuery();
        }

La función CheckOutFile tendría la siguiente definición:

  private static void CheckOutFile(Web web, string fileName, string filePath, string fileFolder)
        {
            var fileUrl = string.Concat(filePath, fileFolder, (string.IsNullOrEmpty(fileFolder) ? string.Empty : "/"), fileName);
            var temp = web.GetFileByServerRelativeUrl(fileUrl);

            web.Context.Load(temp, f => f.Exists);
            web.Context.ExecuteQuery();

            if (temp.Exists)
            {
                web.Context.Load(temp, f => f.CheckOutType);
                web.Context.ExecuteQuery();

                if (temp.CheckOutType != CheckOutType.None)
                {
                    temp.UndoCheckOut();
                }

                temp.CheckOut();
                web.Context.ExecuteQuery();
            }
        }

La función de Añadir el fichero:

private static File AddFile(Web web, string filePath, string fileName, string serverPath, string serverFolder)
        {
            var fileUrl = string.Concat(serverPath, serverFolder, (string.IsNullOrEmpty(serverFolder) ? string.Empty : "/"), fileName);
            var folder = web.GetFolderByServerRelativeUrl(string.Concat(serverPath, serverFolder));

            FileCreationInformation spFile = new FileCreationInformation()
            {
                Content = System.IO.File.ReadAllBytes(filePath + fileName),
                Url = fileUrl,
                Overwrite = true
            };
            var uploadFile = folder.Files.Add(spFile);
            web.Context.Load(uploadFile, f => f.CheckOutType, f => f.Level);
            web.Context.ExecuteQuery();

            return uploadFile;
        }

La definición de CheckIn PublishAndApproveFile:

      private static void CheckInPublishAndApproveFile(File uploadFile)
        {
            if (uploadFile.CheckOutType != CheckOutType.None)
            {
                uploadFile.CheckIn("Updating branding", CheckinType.MajorCheckIn);
            }

            if (uploadFile.Level == FileLevel.Draft)
            {
                uploadFile.Publish("Updating branding");
            }

            uploadFile.Context.Load(uploadFile, f => f.ListItemAllFields);
            uploadFile.Context.ExecuteQuery();

            if (uploadFile.ListItemAllFields["_ModerationStatus"].ToString() == "2") // SPModerationStatusType.Pending
            {
                uploadFile.Approve("Updating branding");
                uploadFile.Context.ExecuteQuery();
            }

Despliegue de MasterPage

Para desplegar las masterPage que hemos diseñado en primer lugar subiremos la master dentro de la carpeta de MasterPage. A continuación protegemos y publicamos el fichero y por último establecemos esta MasterPage como página maestra del sitio. Para ello (y utilizando gran parte de las funciones anteriores) utilizamos un código similar al siguiente:

public static void UploadMasterPage(ClientContext clientContext, string name, string folder)
        {
            var web = clientContext.Web;
            var lists = web.Lists;
            var gallery = web.GetCatalog(116);
            clientContext.Load(lists, l => l.Include(ll => ll.DefaultViewUrl));
            clientContext.Load(gallery, g => g.RootFolder.ServerRelativeUrl);
            clientContext.ExecuteQuery();

            Console.WriteLine("Uploading and applying {0} to {1}", name, web.ServerRelativeUrl);

            var masterPath = gallery.RootFolder.ServerRelativeUrl.TrimEnd(new char[] { '/' }) + "/";

            EnsureFolder(web, masterPath, folder);
            CheckOutFile(web, name, masterPath, folder);

            var uploadFile = AddFile(web.Url, web, "Branding\\MasterPages\\", name, masterPath, folder);

            SetMasterPageMetadata(web, uploadFile);
            CheckInPublishAndApproveFile(uploadFile);

            //store the currently used master pages so we can switch back upon deactivation
            var allWebProperties = web.AllProperties;
            allWebProperties["OriginalMasterUrl"] = web.MasterUrl;
            allWebProperties["CustomMasterUrl"] = web.CustomMasterUrl;

            var masterUrl = string.Concat(masterPath, folder, (string.IsNullOrEmpty(folder) ? string.Empty : "/"), name);
            web.MasterUrl = masterUrl;
            web.CustomMasterUrl = masterUrl;
            web.Update();
            clientContext.ExecuteQuery();
        }

La función «SetMasterPageMetadata tiene la siguiente definición:

private static void SetMasterPageMetadata(Web web, File uploadFile)
        {
            var parentContentTypeId = "0x010105"; // Master Page
            var gallery = web.GetCatalog(116);
            web.Context.Load(gallery, g => g.ContentTypes);
            web.Context.ExecuteQuery();

            var contentTypeId = gallery.ContentTypes.FirstOrDefault(ct => ct.StringId.StartsWith(parentContentTypeId)).StringId;
            var item = uploadFile.ListItemAllFields;
            web.Context.Load(item);

            item["ContentTypeId"] = contentTypeId;
            item["UIVersion"] = Convert.ToString(15);
            item["MasterPageDescription"] = "Violin master page";
            item.Update();
            web.Context.ExecuteQuery();
        }

Desplegando los PageLayouts

Para desplegar los pageLayouts, realizaremos una función similar al despliegue de las Master pero modificando los valores del PageLayout.

  public static void UploadPageLayout(ClientContext clientContext, string name, string folder, string title, string publishingAssociatedContentType)
        {
            var web = clientContext.Web;
            var lists = web.Lists;
            var gallery = web.GetCatalog(116);
            clientContext.Load(lists, l => l.Include(ll => ll.DefaultViewUrl));
            clientContext.Load(gallery, g => g.RootFolder.ServerRelativeUrl);
            clientContext.ExecuteQuery();

            Console.WriteLine("Uploading page layout {0} to {1}", name, clientContext.Web.ServerRelativeUrl);

            var masterPath = gallery.RootFolder.ServerRelativeUrl.TrimEnd(Program.trimChars) + "/";

            EnsureFolder(web, masterPath, folder);
            CheckOutFile(web, name, masterPath, folder);

            var uploadFile = AddFile(web.Url, web, "Branding\\PageLayouts\\", name, masterPath, folder);

            SetPageLayoutMetadata(web, uploadFile, title, publishingAssociatedContentType);
            CheckInPublishAndApproveFile(uploadFile);
        }

La función «SetPageLayoutMetadata» tiene la siguiente definición:

private static void SetPageLayoutMetadata(Web web, File uploadFile, string title, string publishingAssociatedContentType)
        {
            var parentContentTypeId = "0x01010007FF3E057FA8AB4AA42FCB67B453FFC100E214EEE741181F4E9F7ACC43278EE811"; //Page Layout
            var gallery = web.GetCatalog(116);
            web.Context.Load(gallery, g => g.ContentTypes);
            web.Context.ExecuteQuery();

            var contentTypeId = gallery.ContentTypes.FirstOrDefault(ct => ct.StringId.StartsWith(parentContentTypeId)).StringId;
            var item = uploadFile.ListItemAllFields;
            web.Context.Load(item);

            item["ContentTypeId"] = contentTypeId;
            item["Title"] = title;
            item["PublishingAssociatedContentType"] = publishingAssociatedContentType;

            item.Update();
            web.Context.ExecuteQuery();
        }

Conclusión

Poco a poco tenemos que ir viendo todas las posibilidades que hay en Office 365 para poder realizar los desarrollos y requerimientos de los clientes. Office 365 es la evolución natural de nuestro SharePoint tradicional, cada vez tenemos más herramientas y utilidades para que las diferencias entre Online y OnPremise se acorten. Está claro que en la nube NUNCA vamos a tener las FARM Solutions pero esto desde mi punto de vista no es algo que limite a la plataforma sino más bien al contrario, la enriquece y hace utilizar cada herramienta en lo que realmente es su fuerte.

En este post hemos visto como podemos aplicar branding a nuestros desarrollos de una forma relativamente simple. En futuros post veremos como podemos aprovisionar el resto de artefactos en Office 365.

Referencias

http://channel9.msdn.com/Blogs/Office-365-Dev/Applying-Branding-to-SharePoint-Sites-with-an-App-for-SharePoint-Office-365-Developer-Patterns-and-P

https://github.com/OfficeDev/PnP

mm

Sobre Adrián Díaz

Adrián Díaz es Ingeniero Informático por la Universidad Politécnica de Valencia. Es MVP de Microsoft en la categoría Office Development desde 2014, MCPD de SharePoint 2010, Microsoft Active Profesional y Microsoft Comunity Contribuitor 2012. Cofundador del grupo de usuarios de SharePoint de Levante LevaPoint. Lleva desarrollando con tecnologías Microsoft más de 10 años y desde hace 3 años está centrado en el desarrollo sobre SharePoint. Actualmente es Software & Cloud Architect Lead en ENCAMINA.
Esta entrada ha sido publicada en Office 365 y etiquetada como , , . Enlace permanente .
Suscríbete a Desarrollando sobre SharePoint

Suscríbete a Desarrollando sobre SharePoint

Recibe todas las actualizaciones semanalmente de nuestro blog

You have Successfully Subscribed!

ENCAMINA, piensa en colores