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!!
Subscribe to:
Post Comments
(
Atom
)
Commented code would be good. Typical Joe code... :-P
ReplyDeleteComments are nothing but deodorant for smelly code.
ReplyDeleteRight, so comment your code
ReplyDeleteI 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.
ReplyDeleteThanks 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.
ReplyDeleteI'm glad I was able to lead you in the right direction.
DeleteThanks 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
ReplyDelete@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
Hey Ram:
DeleteDid 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.
Ram:
DeleteI 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
Thanks Joe for the replies. Let me give a try once again.
DeleteAfter 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