This post has been migrated from www.experimentsincode.com, we apologise if some of the images or content is missing

At the Sitecore Bristol User Group last week I was chatting to Nick from True Clarity after his excellent presentation. We moved on to the subject of how I was using Razor views in Sitecore with the Glass.Sitecore.Mapper framework; he referred to the Razor views as essentially being leaf nodes of the presentation layer. I hadn't really thought of visualising the presentation layer in Sitecore as a tree structure but as soon as he said it, it made a lot of sense. This then got me thinking  "why are my Razor views leaf nodes?" can I somehow inject Placeholders into my Razor views? After some hacking and some help from both Kevin Obee and Nick Wesselman on the SDN Forum I have a solution. I will admit that this is a bit experimental but I have tested the solution with the following:
  • Inserting another Razor view into the Placeholder using the Sitecore Layout Designer
  • Configured Placeholder Settings for the Placeholder in Sitecore
  • Dropping a Razor view and Sub-layout into the Placeholder using the Page Editor
  • Deleting a dropped in Razor view and Sub-layout using the Page Editor
If you have download the example project I created for the Glass.Sitecore.Mapper demonstration at the Bristol User Group then this will contain the classes need to get you started and you can modify these to get this example working. The first thing to look at is how a Razor view is constructed, they are similar in setup to the a ASCX page in that they comprise of the Razor template and code-behind class. The code behind might look something like this:
    public class MainContent : RazorControl<HomePage>
    {
        public MainContent()
        {
            Placeholders = new[] { "RazorPlaceholder1","RazorPlaceholder2" };
        }
        protected override void OnLoad(EventArgs e)
        {
            ISitecoreContext context = new SitecoreContext();

            this.Model = context.GetHomeItem<HomePage>();
            base.OnLoad(e);
        }
    }
The class inherits from the RazorControl class (which I explain below) and we pass it the type of model we are using in our Razor view. You can see that within the class constructor I define a couple of Razor placeholders keys and assign the to the Placeholders property, again this will be explained later. When the control loads I use the ISitecoreContext to load my model and assign that to the Model property on the base class. The Razor view looks like this:
@using Glass.Demo.Application.Models.Home
@inherits Glass.Demo.Application.Web.TemplateBase<HomePage>
<div class="user_icon">
    @Html.RenderImage(Model.MainImage, null)
</div>

<div>
     @RenderHolder("RazorPlaceholder1");
</div>
<div class="welcome_block">
    <h1>@(Html.Editable<HomePage>(x=>x.Title, Model))</h1>
    @Model.Text
</div>
<div>
     @RenderHolder("RazorPlaceholder2");
</div>
Notice that in our view we have the RenderHolder method that takes the key of the Sitecore Placeholder we want to render in this position. This method exists on the TemplateBase class which is explained below. This is how simple it is to define the Sitecore Placeholders you want to use in a Razor view and then define where they should render. Now to see how this actually works. Lets start by looking at the RazorControl, this is the control that gets created when Sitecore encounters a Razor layout item (see the RazorRenderingType class in the demo project) :
    public class RazorControl<T> : WebControl, IRazorControl, global::Sitecore.Layouts.IExpandable
    {

        public IEnumerable<string> Placeholders
        {
            get;
            set;
        }

        public string View
        {
            get;
            set;
        }
        public string AssemblyName { get; set; }

        public T Model
        {
            get;
            set;
        }

        public NameValueCollection Form
        {
            get
            {
                return this.Context.Request.Form;
            }
        }

        protected override void DoRender(HtmlTextWriter output)
        {

            var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(View);
            TextReader reader = new StringReader(new StreamReader(stream).ReadToEnd());
            Razor.DefaultTemplateService.Namespaces.Add("Glass.Sitecore.Mapper.FieldTypes");

            try
            {

                using (new global::Sitecore.SecurityModel.SecurityDisabler())
                {
                    var rzControl = this;

                    TemplateModel<T> tModel = new TemplateModel<T>();
                    tModel.RazorControl = this;
                    tModel.Model = Model;

                    string content = Razor.Parse<TemplateModel<T>>(reader.ReadToEnd(), tModel);

                    output.Write(content);
                }
            }
            catch (RazorEngine.Templating.TemplateCompilationException ex)
            {
                throw new Exception(ex.Errors.First().ErrorText, ex);
            }
        }

        public void Expand()
        {
            if (Placeholders != null)
            {
                foreach (var placeHolderName in Placeholders)
                {
                    global::Sitecore.Web.UI.WebControls.Placeholder holder = new global::Sitecore.Web.UI.WebControls.Placeholder();
                    holder.Key = placeHolderName.ToLower();
                    this.Controls.Add(holder);
                }
            }

            this.Controls.Cast<Control>().Where(x => x is global::Sitecore.Layouts.IExpandable)
                .Cast<global::Sitecore.Layouts.IExpandable>().ToList().ForEach(x => x.Expand());
        }
    }
The Expand method is probably the most import method here (thanks to Nick Wesselman for the  information on this). This iterates through the list of Placeholders we defined on our sub-class MainContent creating the actual Placeholder controls. It then iterates through all RazorControl child controls and calls their Expand method, this is important, without it the Placeholders that we just created won't have the correct ContextKey and the ContextKey is used by Sitecore to decided where other rendering components should be placed. Next lets look at the DoRender method, the first few lines simply read the Razor view from the current assembly (all the Razor views are embedded into the actual DLL so there aren't an cshtml files on the server). The next part down is where is starts to get interesting, the RazorControl class has a companion class called TemplateBase, the TemplateBase class is the class that represents the actual cshtml file. The TemplateBase class doesn't know anything about the RazorControl class so we have to somehow pass the RazorControl class and the Model across to the TemplateBase class, we do that using the TemplateModel class:
    public class TemplateModel<T>
    {
        public RazorControl<T> RazorControl
        {
            get;
            set;
        }
        public T Model { get; set; }
    }
This class simple allows us to move information in our RazorControl to our Razor view by passing it into the Razor parser as part of the model. Lets see what the TemplateBase class does with this:
    public class TemplateBase<T>:RazorEngine.Templating.TemplateBase<TemplateModel<T>>
    {
        Html _html;
        public TemplateBase()
        {
            _html = new Html(global::Sitecore.Context.Database);
        }

        public T Model
        {
            get
            {
                return base.Model.Model;
            }
            set
            {
                base.Model.Model = value;
            }
        }

        public Html Html { get { return _html; } }

        public string RenderHolder(string key)
        {
            key = key.ToLower();
            var placeHolder = base.Model.RazorControl.Controls.Cast<Control>()
                .Where(x => x is global::Sitecore.Web.UI.WebControls.Placeholder)
                .Cast<global::Sitecore.Web.UI.WebControls.Placeholder>()
                .FirstOrDefault(x => x.Key == key);

            if (placeHolder == null)
                return "No placeholder with key: {0}".Formatted(key);
            else
            {
                StringBuilder sb = new StringBuilder();
                placeHolder.RenderControl(new HtmlTextWriter(new StringWriter(sb)));
                return sb.ToString();
            }
        }
    }
If you look back at our MainContent.cshtml file shown above we can see that the Razor view inherits from this class:
@using Glass.Demo.Application.Models.Home
@inherits Glass.Demo.Application.Web.TemplateBase<HomePage>
Most of this class is straight forward but the most important method is the RenderHolder method. When you call this method passing in the Placeholder key you want to render it actually goes away and checks the list of Placeholders assigned to the RazorControl. If it finds a Placeholder on the RazorControl that matches the passed in key it then calls the RenderControl method on the Placeholder and returns this to the Razor view. So now that we have this setup I can start to assign Placeholder Settings to the Placeholders and give my editors all the functionality they would expect using Sublayouts but with a rendering system that is much nicer to work with than ASP.NET WebForms (No more hideous control trees). Not only is it awesome how much we can do with Razor in Sitecore but it is also awesome that Sitecore is designed in such an extensible way that it allows us to do this. PS. For those who prefer to use Spark (I have used Spark on a couple of Sitecore projects) then a similar solution should work.
comments powered by Disqus