Overriding .xml and .xaml.xml files part of Sitecore shell structure

Sitecore Shell page UI layout is stored in two file formats. In other words, the layout can be in one of either formats. This blog article covers two parts:

1. Overriding .xml file

2. Overriding .xaml.xml file


Overriding .xml file

Under the hood, UI elements like presentation details are stored as xml and then loaded by Sitecore.Client dll classes. So, a picture first as to how it is all composed - the purple ones are xml files while the blue ones are classes:

In case of presentation details, the layout structure is stored in layoutdetails.xml, within the layout, there are parent containers and the container gets passed to add fields (again, stored as LayoutFieldDevice.xml) and the filled container is passed back to the layout view.

Now, some background:

I recently came across a requirement in Sitecore Slack channel wherein someone wanted the Edit and CopyTo options in Presentation Details to be hidden for users other than admin. Note that Sitecore allows role-based restriction only till the ribbon buttons and not fine-grained field-level restriction as this.

If this were a client requirement, I would have said that the best option would be to deny access to presentation details in ribbon altogether but the developer side in me wanted to take this requirement offline and see under-hood how-to accomplish  just making these links disappear in shared layout for users other than admin!

So, here are the steps:

1. Add LayoutDetails.xml to <website root>\sitecore\shell\override\Applications\Content Manager\Dialogs\LayoutDetails.

Note: the way I found LayoutDetails.xml has the presentation details is picking one element through browser inspect and searching through the file system of the deployed website!

Also, this Hishaam's blog told me that you could override layout details as per your wish.

2. Add a simple change to the xml file in above location, open presentation details to find to be effective: 


Note: I've used Sitecore 10.1.0 instance

3. Next, perform the usual shenanigans of creating an empty asp.net project, adding a class library that will hold the custom presentation details logic, publish and check if all is good:


4. In the override LayoutDetails.xml, update the codebeside assembly and new class as follows and check the end-result:


5. The new code doesn't inherit the existing base code hence the above end-result of presentation details displayed blank so, add nuget package reference to Sitecore 10.1.0 in the project, ensure Sitecore and other DLLs are set to "Do not copy" since I don't want to disturb my website bin folder during code publish. Then ensure the existing custom class inherits from Sitecore.Shell.Applications.ContentManager.Dialogs.LayoutDetails.LayoutDetailsForm:


6. End-result after publish:


7. Now, the next step is to put dotpeek to use. I pulled all code from Sitecore.Client LayoutDetailsForm class using dotpeek to my new LayoutDetailsForm class: 


The most important part here is this code since it passes the container name to LayoutGridBuilder for building the grid and I have customized the name to CustomLayoutGridBuilder to  add my logic:

private void RenderLayoutGridBuilder(string layoutValue, Sitecore.Web.UI.HtmlControls.Control renderingContainer)
        {
            string str = renderingContainer.ID + "LayoutGrid";
            CustomLayoutGridBuilder layoutGridBuilder = new CustomLayoutGridBuilder()
            {
                ID = str,
                Value = layoutValue,
                EditRenderingClick = "EditRendering(\"$Device\", \"$Index\")",
                EditPlaceholderClick = "EditPlaceholder(\"$Device\", \"$UniqueID\")",
                OpenDeviceClick = "OpenDevice(\"$Device\")",
                CopyToClick = "CopyDevice(\"$Device\")"
            };
            renderingContainer.Controls.Clear();
            layoutGridBuilder.BuildGrid((System.Web.UI.Control)renderingContainer);
            if (!Context.ClientPage.IsEvent)
                return;
            SheerResponse.SetOuterHtml(renderingContainer.ID, (System.Web.UI.Control)renderingContainer);
            SheerResponse.Eval("if (!scForm.browser.isIE) { scForm.browser.initializeFixsizeElements(); }");
        }

8. Similarly, I built the CustomLayoutGridBuilder  code from LayoutGridBuilder using dotpeek:


I just did some dirty coding here to differentiate the admin user from other users, the best option here is to limit by role:

XmlControl child = new XmlControl();

            if (Sitecore.Context.User.Profile.UserName.ToLowerInvariant() == "sitecore\\admin")
            {
                child = string.IsNullOrEmpty(this.OpenDeviceClick) ? Resource.GetWebControl("LayoutFieldDeviceReadOnly") as XmlControl : Resource.GetWebControl("LayoutFieldDevice") as XmlControl;

            }
            else
            {
                child = string.IsNullOrEmpty(this.OpenDeviceClick) ? Resource.GetWebControl("LayoutFieldDeviceReadOnly") as XmlControl : Resource.GetWebControl("CustomLayoutFieldDevice") as XmlControl;
            }

9. I then ensured that the LayoutDevice.xml is copied over to \sitecore\shell\override\Applications\Layouts\DeviceEditor


Note that I created the CustomLayoutFieldDevice.xml which is nothing but a copy of LayoutFieldDevice.xml. In case of former, I removed the Edit and CopyTo links so that I can use it to toggle in the if condition.


Desired end-result:

User apart from admin:


Note that in the above case, the user can still click other parts like Sample layout and get to add components and this is not an ideal customization that too when Sitecore provides security options/roles to restrict users but this is just to prove how such deep changes can be accomplished using xml and code!

Admin user:

Intact and clickable:



Github code

Overriding .xaml.xml Files

Now, if the layout is in a .xaml.xml file, overriding is not so easy unless you add a patch as covered by this useful blog

Note that there are so many files with .xaml.xml extension under shell so this concept is very useful.


You could get a ysod (as below) after adding the above patch file,  just make these couple of minor fixes to this app_config\include\zzz patch:

XamlSharpOverride.config:

<?xml version="1.0" encoding="utf-8" ?>

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

  <sitecore>

    <xamlsharp>

      <sources>

        <source type="Sitecore.Web.UI.XamlSharp.Xaml.XamlFileControlSource,Sitecore.Kernel" patch:before="*">

          <watchers hint="list:AddWatcher">

            <watcher type="Sitecore.Web.UI.XamlSharp.Xaml.XamlFileWatcher,Sitecore.Kernel">

              <folder>/sitecore/shell/override</folder>

              <filter>*.xaml.xml</filter>

              <codefilter>*.xaml.xml.cs</codefilter>

              <includesubdirectories>true</includesubdirectories>

            </watcher>

          </watchers>

        </source>

      </sources>

    </xamlsharp>

  </sitecore>

</configuration>

Once you add the above patch, you can add override files for .xaml.xml file as follows and they will get reflected in the corresponding UI:



Server Error in '/' Application.


'patch' is an undeclared prefix. Line 5, position 93.

Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.

Exception Details: System.Xml.XmlException: 'patch' is an undeclared prefix. Line 5, position 93.

Source Error:

An unhandled exception was generated during the execution of the current web request. Information regarding the origin and location of the exception can be identified using the exception stack trace below.


Stack Trace:

[XmlException: 'patch' is an undeclared prefix. Line 5, position 93.]
   System.Xml.XmlTextReaderImpl.Throw(Exception e) +91
   System.Xml.XmlTextReaderImpl.LookupNamespace(NodeData node) +180
   System.Xml.XmlTextReaderImpl.AttributeNamespaceLookup() +77
   System.Xml.XmlTextReaderImpl.ParseAttributes() +1471
   System.Xml.XmlTextReaderImpl.ParseElement() +878
   System.Xml.XmlTextReaderImpl.ParseElementContent() +552
   System.Xml.XmlLoader.LoadNode(Boolean skipOverWhitespace) +1025
   System.Xml.XmlLoader.LoadDocSequence(XmlDocument parentDoc) +67
   System.Xml.XmlDocument.Load(XmlReader reader) +137
   Sitecore.Configuration.ConfigPatcher.GetXmlElement(XmlReader reader, String sourceName) +112
   Sitecore.Configuration.ConfigPatcher.ApplyPatch(TextReader patch, String sourceName) +99
   Sitecore.Configuration.ConfigPatcher.ApplyPatch(String filename) +92
   Sitecore.Configuration.ConfigReader.LoadIncludeFiles(ConfigPatcher patcher, IEnumerable`1 files) +150

[ConfigurationException: An error occurred during applying the patch file: C:\inetpub\wwwroot\sc103xyzsc.dev.local\App_Config\Include\zzz\XamlSharpOverride.config]
   Sitecore.Configuration.ConfigReader.LoadIncludeFiles(ConfigPatcher patcher, IEnumerable`1 files) +362
   Sitecore.Configuration.ConfigReader.DoGetConfiguration() +402
   Sitecore.Configuration.ConfigReader.GetConfiguration() +236
   Sitecore.DependencyInjection.ServiceLocator.ConfigureServiceProvider() +13
   Sitecore.DependencyInjection.ServiceLocator.get_ServiceProvider() +513
   Sitecore.DependencyInjection.SitecorePerRequestScopeModule..ctor() +13

[TargetInvocationException: Exception has been thrown by the target of an invocation.]
   System.RuntimeTypeHandle.CreateInstance(RuntimeType type, Boolean publicOnly, Boolean noCheck, Boolean& canBeCached, RuntimeMethodHandleInternal& ctor, Boolean& bNeedSecurityCheck) +0
   System.RuntimeType.CreateInstanceSlow(Boolean publicOnly, Boolean skipCheckThis, Boolean fillCache, StackCrawlMark& stackMark) +142
   System.Activator.CreateInstance(Type type, Boolean nonPublic) +107
   System.RuntimeType.CreateInstanceImpl(BindingFlags bindingAttr, Binder binder, Object[] args, CultureInfo culture, Object[] activationAttributes, StackCrawlMark& stackMark) +1473
   System.Activator.CreateInstance(Type type, BindingFlags bindingAttr, Binder binder, Object[] args, CultureInfo culture, Object[] activationAttributes) +186
   System.Activator.CreateInstance(Type type, BindingFlags bindingAttr, Binder binder, Object[] args, CultureInfo culture) +28
   System.Web.HttpRuntime.CreateNonPublicInstance(Type type, Object[] args) +80
   System.Web.HttpApplication.BuildIntegratedModuleCollection(List`1 moduleList) +234
   System.Web.HttpApplication.GetModuleCollection(IntPtr appContext) +1153
   System.Web.HttpApplication.RegisterEventSubscriptionsWithIIS(IntPtr appContext, HttpContext context, MethodInfo[] handlers) +139
   System.Web.HttpApplication.InitSpecial(HttpApplicationState state, MethodInfo[] handlers, IntPtr appContext, HttpContext context) +168
   System.Web.HttpApplicationFactory.GetSpecialApplicationInstance(IntPtr appContext, HttpContext context) +277
   System.Web.Hosting.PipelineRuntime.InitializeApplication(IntPtr appContext) +369

[HttpException (0x80004005): Exception has been thrown by the target of an invocation.]
   System.Web.HttpRuntime.FirstRequestInit(HttpContext context) +532
   System.Web.HttpRuntime.EnsureFirstRequestInit(HttpContext context) +111
   System.Web.HttpRuntime.ProcessRequestNotificationPrivate(IIS7WorkerRequest wr, HttpContext context) +719



Version Information: Microsoft .NET Framework Version:4.0.30319; ASP.NET Version:4.8.9032.0 




Comments

Popular Posts