Switching Layout Provider

July 03, 2016 in #Sitecore #Angular | | | Share on Google+

While working on a project that integrates AngularJS with Sitecore we were looking for ways to make the pages easily editable by content editors (using the Experience Editor) while keeping the HTML clean for the Angular views.

The problem is that in order to get the page properly rendered (including correct styling) we need a full-fledged HTML layout, but when we load that same page from Angular we want just the HTML of the renderings, no <head>, no <body>.

After thinking about it for some time I thought "wouldn't it be nice if Sitecore could provide us with a different layout for the Experience Editor and normal page rendering?" and that was how I came up with the idea to just write something that allows just that and integrates into the Sitecore pipeline.

At first I thought it would be possible to just overwrite/extend one small part of the httpRequestBegin pipeline (specifically the LayoutResolver) and that does work. Unless you use MVC because when you do, the page is constructed in an entirely different way.

Back to the drawing board! Luckily I quickly stumbled upon the BuildPageDefinition pipeline in the Sitecore.Mvc assembly which does as it says. It will create (render) the entire page.

After some trial and error I found out that I can get the current layout after the LayoutResolver part of the pipeline and then just assign it to the context and load it back in the BuildPageDefinition part. Luckily this means that the logic isn't split but that the MVC part just reads and loads from the context which we've set earlier.

using System;  
using System.Collections.Generic;  
using System.Linq;  
using System.Text;  
using System.Threading.Tasks;  
using System.Web;  
using Sitecore.Data.Fields;  
using Sitecore.Data.Items;  
using Sitecore.Diagnostics;  
using Sitecore.Pipelines.HttpRequest;  
using Sitecore.Web;

namespace Boondoggle.Sc.Pipelines.SwitchingLayout  
{
    public class SwitchingLayoutResolver : HttpRequestProcessor
    {
        public override void Process(HttpRequestArgs args)
        {
            Assert.ArgumentNotNull(args, "args");

            if (Sitecore.Context.Item == null)
                return;

            var layoutItem = GetQueryStringLayout() ?? Sitecore.Context.Item.Visualization.Layout;
            if (layoutItem != null && layoutItem.InnerItem.TemplateID.ToString() == "{2C87C6FE-F615-4E92-B321-1BAE3D488B0D}")
            {
                Tracer.Info("Found SwitchingLayout");

                ReferenceField layoutField = null;
                if (Sitecore.Context.PageMode.IsExperienceEditor)
                {
                    layoutField = (ReferenceField)layoutItem.InnerItem.Fields["EditingLayout"];
                }
                else
                {
                    layoutField = (ReferenceField)layoutItem.InnerItem.Fields["NormalLayout"];
                }

                if (layoutField != null)
                {
                    var specificLayoutItem = layoutField.TargetItem;
                    if (specificLayoutItem != null)
                    {
                        Tracer.Info(string.Format("[{0}] Setting SwitchingLayout to {1}", Sitecore.Context.PageMode.IsExperienceEditor ? "Editing" : "Normal", specificLayoutItem.ID.ToString()));

                        var newLayout = new LayoutItem(specificLayoutItem);
                        Sitecore.Context.Page.FilePath = newLayout.FilePath;
                        args.Context.Items.Add("SwitchingLayoutGuid", specificLayoutItem.ID.ToGuid());
                    }
                }
            }
        }

        /// <summary>
        /// Gets the layout.
        /// 
        /// </summary>
        /// 
        /// <returns>
        /// The layout.
        /// </returns>
        private static LayoutItem GetQueryStringLayout()
        {
            string queryString = WebUtil.GetQueryString("sc_layout");
            if (string.IsNullOrEmpty(queryString))
                return null;

            return Sitecore.Context.Database.GetItem(queryString);
        }
    }
}

As you can see here we just check if the current page has a layout that uses our specific Switching Layout and if it does we start the magic: If ExperienceEditor then load that layout, if not load the other.
Easy huh?

And like I said, the MVC part is no rocket science either (once you know where to look, thank god we have dotPeek for that!)

using System;  
using System.Collections.Generic;  
using System.Linq;  
using System.Text;  
using System.Threading.Tasks;  
using System.Web;  
using System.Xml.Linq;  
using Sitecore.Diagnostics;  
using Sitecore.Mvc.Pipelines.Response.BuildPageDefinition;  
using Sitecore.Mvc.Presentation;

namespace Boondoggle.Sc.Pipelines.SwitchingLayout  
{
    public class ProcessXmlBasedSwitchingLayoutDefinition : ProcessXmlBasedLayoutDefinition
    {
        protected override Rendering GetRendering(XElement renderingNode, Guid deviceId, Guid layoutId, string renderingType, XmlBasedRenderingParser parser)
        {
            var rendering = base.GetRendering(renderingNode, deviceId, layoutId, renderingType, parser);
            try
            {
                if (!String.IsNullOrWhiteSpace(renderingType) && renderingType == "Layout")
                {
                    if (HttpContext.Current.Items.Contains("SwitchingLayoutGuid"))
                    {
                        var switchingLayoutId = (Guid)HttpContext.Current.Items["SwitchingLayoutGuid"];

                        Tracer.Info("Setting XmlBasedLayoutDefinition SwitchingLayout to " + switchingLayoutId.ToString());
                        rendering.LayoutId = switchingLayoutId;
                    }
                }
            }
            catch (Exception ex)
            {
                Tracer.Error("Unable to process XmlBasedLayoutDefinition SwitchingLayout");
            }

            return rendering;
        }
    }
}

Now all that's left is to combine the two and patch them into our pipelines, like this:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:set="http://www.sitecore.net/xmlconfig/set/" >  
  <sitecore>
    <pipelines>

      <httpRequestBegin>
        <processor type="Boondoggle.Sc.Pipelines.SwitchingLayout.SwitchingLayoutResolver, Boondoggle.Sc"
                   patch:after="processor[@type='Sitecore.Pipelines.HttpRequest.LayoutResolver, Sitecore.Kernel']"/>
      </httpRequestBegin>

      <mvc.buildPageDefinition>
        <processor type="Boondoggle.Sc.Pipelines.SwitchingLayout.ProcessXmlBasedSwitchingLayoutDefinition, Boondoggle.Sc"
                   patch:instead="processor[@type='Sitecore.Mvc.Pipelines.Response.BuildPageDefinition.ProcessXmlBasedLayoutDefinition, Sitecore.Mvc']"/>
      </mvc.buildPageDefinition>

    </pipelines>
  </sitecore>
</configuration>  

Now, when you test it you will notice something...
You can't choose a Switching Layout as your layout on any item!

Of course that control only knows about Sitecore's standard layout types. Now, let's try to find where that logic is set. It's an XML control so it'll probably be in the sitecore/shell on the filesystem.

Bingo, the DeviceEditor is the one we need. Thank god Sitecore made it easy for us to overwrite this specific part of the layout. Downside is that we'll have to overwrite the entire xml control instead of just "patching" in our changes. This is something you'll have to take into account when upgrading Sitecore versions.

I've created a new xml in the sitecore/shell/Override/DeviceEditor folder named DeviceEditor.xml. We do like things to be consistent!

<?xml version="1.0" encoding="utf-8" ?>  
<control xmlns:def="Definition" xmlns="http://schemas.sitecore.net/Visual-Studio-Intellisense">  
  <DeviceEditor>
    <FormDialog Icon="People/24x24/pda.png" Header="Device Editor"
      Text="Set the layouts, controls and placeholders for this device." OKButton="OK">

      <Stylesheet>

        .ie8 .scDialogContentContainer {
        overflow: hidden !important;
        }

        .scVerticalTabstrip .scTabContent {
        background: transparent;
        border: none;
        position:relative;
        }

        .scConditionContainer{
        background-color: #446693;
        color: White;
        float: left;
        left: 20px;
        position: absolute;
        text-align: center;
        top: 0px;
        padding: 1px 3px;
        }

        .scTestContainer{
        background-color: #934444;
        color: White;
        float: left;
        left: 20px;
        position: absolute;
        text-align: center;
        top: 0px;
        padding: 1px 3px;
        }

        .scLongConditionContainer
        {
        left:16px;
        padding: 1px 2px;
        }

        .optionButtons {
        position: absolute;
        top: 0;
        right: 0;
        text-align: right;
        white-space: normal;
        }

        .optionButtons .scButton{
        width: 100%;
        margin: 0;
        display: block;
        }

        #Renderings, #Placeholders {
        margin-right: 110px;
        }

        #Renderings > div:hover, #Placeholders > div:hover {
        background-color: #E3E3E3;
        cursor: pointer;
        }

        .ie .lang_ja_jp #Renderings {
        margin-right: 140px;
        }

      </Stylesheet>

      <CodeBeside Type="Sitecore.Shell.Applications.Layouts.DeviceEditor.DeviceEditorForm,Sitecore.Client"/>
      <DataContext ID="LayoutDataContext" DataViewName="Master" Root="{75CC5CE4-8979-4008-9D3C-806477D57619}" Filter="Contains('{A87A00B1-E6DB-45AB-8B54-636FEC3B5523},{1163DA83-B2EF-4381-BF09-B2FF714B1B3F},{3A45A723-64EE-4919-9D41-02FD40FD1466},{A87A00B1-E6DB-45AB-8B54-636FEC3B5523},{239F9CF4-E5A0-44E0-B342-0F32CD4C6D8B},{93227C5D-4FEF-474D-94C0-F252EC8E8219},{2C87C6FE-F615-4E92-B321-1BAE3D488B0D}', @@templateid)"/>

      <VerticalTabstrip>
        <Tab ID="LayoutTab" Header="Layout">
          <TreePicker ID="Layout" DataContext="LayoutDataContext" SelectOnly="true" AllowNone="true" Width="100%"/>
        </Tab>

        <Tab ID="ControlsTab" Header="Controls">
          <div class="scStretch">
            <Scrollbox ID="Renderings" Padding="0px" />
            <div class="optionButtons">
              <Button Header="Add" Click="device:add" style="margin-bottom: 6px"/>
              <Button Header="Edit" ID="btnEdit" Click="device:edit" style="margin-bottom: 6px"/>
              <Button ID="Personalize" Header="Personalize" Click="device:personalize" style="margin-bottom: 6px"/>
              <Button Header="Change" ID="btnChange" Click="device:change" style="margin-bottom: 6px"/>
              <Button Header="Remove" ID ="btnRemove" Click="device:remove" style="margin-bottom: 14px"/>
              <Button ID="Test" Header="Test" Click="device:test" style="margin-bottom: 14px"/>
              <Button Header="Move Up" ID ="MoveUp" Click="device:sortup" style="margin-bottom: 6px"/>
              <Button Header="Move Down" ID="MoveDown" Click="device:sortdown" style="margin-bottom: 6px"/>

            </div>
          </div>
        </Tab>

        <Tab ID="PlaceholdersTab" Header="Placeholder Settings">
          <div class="scStretch">
            <Scrollbox ID="Placeholders" Padding="0px"/>
            <div class="optionButtons">
              <Button Header="Add" Click="device:addplaceholder" style="margin-bottom: 6px"/>
              <Button Header="Edit" ID="phEdit" Click="device:editplaceholder" style="margin-bottom: 6px"/>
              <Button Header="Remove" ID="phRemove" Click="device:removeplaceholder" style="margin-bottom: 14px"/>
            </div>
          </div>
        </Tab>

      </VerticalTabstrip>

    </FormDialog>
  </DeviceEditor>
</control>  

The only part I've changed is that I've added our template's ID ({2C87C6FE-F615-4E92-B321-1BAE3D488B0D}) in the DataSource mentioned in the XML file.

Now let's test it again.
Victory! It all works perfectly fine. Now we can easily edit our pages while the rendering stays relatively clean. We can even add an Experience-Editor specific CSS file (or JS file) which will help us with the look and feel of our components.

In a next post we'll look more into other Angular'specific customizations we've done.

Github link: https://github.com/fverswijver/Sitecore.Switching.Layout

July 03, 2016 in #Sitecore #Angular | | | Share on Google+