Sitecore SubLayouts rendering on a Sitecore MVC Page (Part 1)


The Idea

At my work there has been a lot excitement around the ability to use MVC with Sitecore. Our business would like to support our eagerness to make the move to MVC but there is a lot of concern about existing the need to rewrite controls that already exist. Although our business folks like the idea of having cleaner markup and no viewstate, they can't afford a productivity/velocity hit while we rewrite controls that are currently functioning perfectly fine. This is a very common issue. Charlie Turano from HedgeHog Development has a blog post explaining how we can extend some of the available Sitecore piplelines and get MVC renderings to render on a WebForms page. While that is really cool stuff I couldn't help but wonder about the other way. Could it be possible to load a SubLayout/UserControl on an MVC View? Let's try it!!

The HtmlHelper Extension Method

So I started with a very simple idea. I can create an HtmlHelper Extension Method to execute just the simple markup. This first pass I will not worry about PostBacks and ViewState. I'm just going to get the markup to render and simply bind statically.
I started with he following code:
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.UI;
using Sitecore.Collections;
using Sitecore.Mvc.Presentation;
using Sitecore.Web;
using Sitecore.Web.UI.WebControls;

namespace BissTalk
{
    public static class SubLayoutMvcAdaperExtensionMethod
    {
        public static IHtmlString RenderSitecoreSubLayout(this HtmlHelper html, 
            string path, string datasource = null, RenderingParameters parameters = null)
        {
            var page = new Sitecore.Web.UI.HtmlControls.Page();
            AddSublayoutToPage(page, path, datasource, parameters);
            var result = ExecutePage(page);
            return html.Raw(result);
        }

        private static void AddSublayoutToPage(Page page, string path, string datasource, 
            IEnumerable<KeyValuePair<string, string>> parameters)
        {
            var dict = new SafeDictionary<string>();
            if (parameters != null)
                parameters.ToList().ForEach(i => dict.Add(i.Key, i.Value));
            var sublayout = new Sublayout
            {
                DataSource = datasource,
                Path = path,
                Parameters = WebUtil.BuildQueryString(dict, true, true)
            };
            var userControl = page.LoadControl(path);
            sublayout.Controls.Add(userControl);
            page.Controls.Add(sublayout);
        }

        private static string ExecutePage(IHttpHandler page)
        {
            var text = new StringWriter();  
            HttpContext.Current.Server.Execute(page, text, true);
            return text.ToString();
        }
    }
}
Now that I have that I can call this method from a razor view page:
@using BissTalk.RequireSc
@using Sitecore.Mvc;
@inherits System.Web.Mvc.WebViewPage

@Html.RenderSitecoreSubLayout("~/MyUserControl.ascx", Html.Sitecore().CurrentRendering.DataSource, 
    Html.Sitecore().CurrentRendering.Parameters)
COOL STUFF!!! Now we can create a SubLayout control that is statically bound on a Sitecore MVC View Rendering. The View rendering is passing down the DataSource Item and any rendering parameters. This is nice, but with this solution you have to create a corresponding view rendering for every single SubLayout we have, and each are the same. That would not only take a long time but it's going to get old quick. So lets make it more dynamic.

Making The Pipleline Do It For Us

The View Rendering we created above is generic except for that darn hard-coded UserControl path. Lets change the razor view and pass the path in as a parameter. That will make it more generic. Here is what that looks like:
@using BissTalk.RequireSc
@using Sitecore.Mvc;
@inherits System.Web.Mvc.WebViewPage

@{
    var path = Html.Sitecore().CurrentRendering.Parameters["sublayout-rendering-path"];
}

@if (!String.IsNullOrEmpty(path))
{
    @Html.RenderSitecoreSubLayout(path, Html.Sitecore().CurrentRendering.DataSource, Html.Sitecore().CurrentRendering.Parameters)
}
Now, we don't want to reference this View Rendering directly because that would just be confusing, especially to our content authors. Lets always reference the SubLayout we want on out page and do a swap in the mvc.buildPageDefinition pipeline. Here's the code for that:
using System;
using Sitecore.Configuration;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Mvc.Pipelines.Response.BuildPageDefinition;
using Sitecore.Mvc.Presentation;

namespace BissTalk.Pipelines
{
    public class GetUserControlRenderer : BuildPageDefinitionProcessor
    {
        public override void Process(BuildPageDefinitionArgs args)
        {
            foreach (var rendering in args.Result.Renderings)
            {
                if (rendering.RenderingItem.TagName == "Sublayout")
                {
                    var id = Settings.GetSetting("Mvc.SublayoutRendering", "{531DCD80-8603-4D69-8F88-F09B63834FDF}");
                    var item = args.PageContext.Database.GetItem(ID.Parse(id));
                    var renderingItem = new RenderingItem(item);
                    var parameterString = item["Parameters"];
                    var path = rendering.RenderingItem.InnerItem["Path"];
                    if (parameterString.Length > 0)
                        parameterString += "&";

                    rendering.Parameters = new RenderingParameters(String.Concat(parameterString, "sublayout-rendering-path=", path));
                    rendering.RenderingItem = renderingItem;
                    rendering.RenderingType = "View rendering";
                }
            }
        }
    }
}
And because I follow Sitecore best practices for configuration file management, I create a patch configuration file and drop it into the~/App_Config/Includes folder. Here is what that looks like:
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <settings>
            <setting name="Mvc.SublayoutRendering" value="{531DCD80-8603-4D69-8F88-F09B63834FDF}" />          
    </settings>
    <pipelines>
      <mvc.buildPageDefinition>
        <processor  type="BissTalk.Pipelines.GetUserControlRenderer, BissTalk" patch:after="*[@type='Sitecore.Mvc.Pipelines.Response.BuildPageDefinition.ProcessXmlBasedLayoutDefinition, Sitecore.Mvc']" />
      </mvc.buildPageDefinition>
    </pipelines>
    <settings>
    </settings>
  </sitecore>
</configuration>

YAY, IT WORKS!!




















And there you have it SubLayout markup rendered on a Sitecore MVC page. You can even update the fields in Page Editor!  Stay tuned until next time when we take this solution and extend it to support ViewState and PostBacks!!


11 comments

11 comments :

  1. Commented code would be good. Typical Joe code... :-P

    ReplyDelete
  2. Comments are nothing but deodorant for smelly code.

    ReplyDelete
  3. I pulled your code into a vanilla sitecore project and still got the "parent Parameter cannot be null" error. I ripped apart Sitecore.Kernel, looking at PageScriptManager.cs, which was throwing the error in the AddBlock function. Seems Sitecore was trying to add some sort of script (I certainly wasn't). Not sure what they are trying to add as the script manager has properties for scripts and stylesheets and both have nothing in them. Regardless, I found properties AddScripts and AddStylesheets. Updating your ExecutePage(IHttpHandler) to ExecutePage(Page) and then settings page.PageScriptManager.AddScripts and page.PageScriptManager.AddStylesheets to false resolved my issue in my vanilla sitecore project. Not sure why you didn't run into this issue.

    ReplyDelete
  4. Thanks for this! Really saved my day. The only problem was in the function RenderSitecoreSublayout you're using the Sitecore.Web.UI.HtmlControls.Page object which caused an error for me. Then I replaced it with System.Web.UI.Page and it worked fine.

    ReplyDelete
    Replies
    1. I'm glad I was able to lead you in the right direction.

      Delete
  5. Thanks for the great article. I have followed your article and tried to call ascx control on sitecore 8.1 MVC request. Added htmlhelper extension and called sublayout in view as below
    @Html.RenderSitecoreSubLayout("~/sitecore modules/Web/MyUsercontrolFolder/MyUserControl.ascx", Html.Sitecore().CurrentRendering.DataSource,
    Html.Sitecore().CurrentRendering.Parameters)

    I am getting below Exception at code line HttpContext.Current.Server.Execute(page, text, true); mentioned in the article.
    Exception: Data at the root level is invalid. Line 1, position 1.

    Stack Trace:


    [XmlException: Data at the root level is invalid. Line 1, position 1.]
    System.Xml.XmlTextReaderImpl.Throw(Exception e) +88
    System.Xml.XmlTextReaderImpl.ParseRootLevelWhitespace() +441
    System.Xml.XmlTextReaderImpl.ParseDocumentContent() +356
    System.Xml.XmlLoader.Load(XmlDocument doc, XmlReader reader, Boolean preserveWhitespace) +160
    System.Xml.XmlDocument.Load(XmlReader reader) +135
    System.Xml.XmlDocument.LoadXml(String xml) +189
    ASP.sitecore_modules_web_yaf_yaf_forum_ascx.Page_Load(Object sender, EventArgs e) +198
    System.Web.UI.Control.OnLoad(EventArgs e) +97
    System.Web.UI.Control.LoadRecursive() +154
    System.Web.UI.Control.LoadRecursive() +251
    System.Web.UI.Control.LoadRecursive() +251
    System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint) +4746


    Please guide me where i am incorrect.
    Thanks in advance.
    Ram

    ReplyDelete
    Replies
    1. Hey Ram:
      Did you read part 2 of the post at http://bisstalk.blogspot.com/2014/10/sitecore-sublayouts-rendering-on_24.html?

      I'm not sure what your issue is because I can't recreate the error but when I took my GitHub project (https://github.com/BissTalk/SitecoreSynergy) and upgraded to MVC 5.2 and reverenced the 8.1 Sitecore.Kernal.dll & Sitecore.Mvc.dll it worked with no issue.

      If you are stuck on this please feel free to private message me with more detail on Sitecore Slack (sitecorechat.slack.com) @joebissol.

      Delete
    2. Ram:
      I Committed the code to Github for Sitecore 8.1 update 2 (https://github.com/BissTalk/SitecoreSynergy). It now uses the Sitecore Nuget Server for dependencies which didn't exists when I originally created the project.

      Joe

      Delete
    3. Thanks Joe for the replies. Let me give a try once again.

      Delete
  6. After I initially left a comment I seem to have clicked on the -Notify me when new comments are added- checkbox and now whenever a comment is added I recieve 4 emails with the same comment. There has to be a means you are able to remove me from that service? Kudos!

    ReplyDelete