LibrarySites.Banner

Determine Models from Views with the Sitecore ASP.NET CMS

This blog post describes a prototype for an mvc.getModel pipeline that determines the view to instantiate from the compiled view in addition to allowing a field in the view definition item to specify a model definition item in MVC solutions that use the Sitecore ASP.NET web Content Management System (CMS) and Experience Platform (XP).

It always bothered me that developers would need to create model definition items and select them in view definition items. I do not see the model definition item adding value in this context. The view specifies the model; why not just use that? Using an mvc.getModel pipeline processor based on this untested mvc.getModel prototype, it appears that you can.

namespace Sitecore.Sharedsource.Mvc.Pipelines.Response.GetModel
{
  using System;
  using System.Web.Compilation;
 
  using Sitecore.Data;
  using Sitecore.Data.Items;
  using Sitecore.Diagnostics;
  using Sitecore.Mvc.Pipelines.Response.GetModel;
 
  public class GetFromView : GetModelProcessor
  {
    private string GetPathFromLayout(
      Database db,
      ID layoutId)
    {
      Item layout = db.GetItem(layoutId);
   
      if (layout != null)
      {
        return layout["path"];
      }
 
      return null;
    }
 
    public override void Process(GetModelArgs args)
    {
      Log.Info(this + " : Process()", this);
      if (args.Result != null)
      {
        Log.Info(this + " : Process() : result populated; return", this);
        return;
      }
 
      if (args.Rendering.RenderingType != "Layout"
        && args.Rendering.RenderingType != "View" 
        && args.Rendering.RenderingType != "r")
      {
        Log.Info(this + " : wrong rendering type; return : " + args.Rendering.RenderingType, this);
        return;
      }
 
      string path = args.Rendering.RenderingItem.InnerItem["path"];
 
      if (string.IsNullOrWhiteSpace(path) && args.Rendering.RenderingType == "Layout")
      {
        path = this.GetPathFromLayout(args.PageContext.Database, new ID(args.Rendering.LayoutId));
      }
 
      if (string.IsNullOrWhiteSpace(path))
      {
        Log.Info(this + " : no path; return", this);
        return;
      }
 
      Type compiledViewType = BuildManager.GetCompiledType(
        path);
      Type baseType = compiledViewType.BaseType;
 
      if (baseType == null || !baseType.IsGenericType)
      {
        Log.Error(string.Format(
          "View {0} compiled type {1} base type {2} does not have a single generic argument.",
          args.Rendering.RenderingItem.InnerItem["path"],
          compiledViewType,
          baseType), this);
        return;
      }
 
      var modelType = baseType.GetGenericArguments()[0];
 
      if (modelType == typeof(object))
      {
        // When no @model is set, the result is a ViewPage<object>
        throw new Exception(string.Format(
          "View '{0}' needs a @model directive.",
          args.Rendering.RenderingItem.InnerItem["path"]));
      }
 
//      args.Result = args.ModelLocator.GetModel(modelType.ToString(), false /*throwIfNotFound*/); // exception
//      args.Result = args.ModelLocator.GetModelFromTypeName(modelType.ToString(), false /*throwIfNotFound*/); // protectred
      args.Result = Activator.CreateInstance(modelType);
      Assert.IsNotNull(args.Result, "args.Result");
    }
  }
}

Remember that when Sitecore invokes the mvc.getModel pipeline for a layout, it passes the device definition item rather than the layout definition item. From the layout definition item or the view definition item we can get the path to the view file. We can pass that to BuildManager.GetCompiledType to get the generic type that represents the page, for which the type specified by the @model directive in the .cshtml file is the first generic argument. The ModelLocator protects the required method or throws an exception, so we can use Activator.CreateInstance to instantiate our type. If it implements IRenderingModel, the InitializeModel pipeline later in the mvc.getModel pipeline will call its Initialize() method. Finally, remember that many MVC pipelines do not abort, requiring processors to check data context (args.Result) before determining whether to proceed.

You can use a web.config include file to enable this mvc.getModel pipeline processor:

<configuration xmlns:patch="https://www.sitecore.com/xmlconfig/">
  <sitecore>
    <pipelines>
      <mvc.getModel>
        <processor patch:before="processor[1]" type="Sitecore.Sharedsource.Mvc.Pipelines.Response.GetModel.GetFromView,Sitecore.Sharedsource"/>
      </mvc.getModel>
    </pipelines>
  </sitecore>
</configuration>

Of course that could leave us with this old barnacle:

Compiler Error Message: CS1061: 'System.Web.Mvc.HtmlHelper<Sitecore.Sharedsource.Models.MyModel>' does not contain a definition for 'Sitecore' and no extension method 'Sitecore' accepting a first argument of type 'System.Web.Mvc.HtmlHelper<Sitecore.Sharedsource.Models.MyModel>' could be found (are you missing a using directive or an assembly reference?)

This appears to require this little gem (which should really cache the SitecoreHelper…):

namespace Sitecore.Sharedsource.Mvc
{
  using System.Web.Mvc;
 
  using Sitecore.Mvc.Helpers;
 
  public static class HtmlHelperExtensions
  {
    public static SitecoreHelper Sitecore<TModel>(this HtmlHelper<TModel> htmlHelper) where TModel : class
    {
      return new SitecoreHelper(htmlHelper);
    }
  }
}

This requires a new /configuration/system.web.webPages.razor/host/pages/namespaces/add element to the web.config file in the root subdirectory for .cshtml files:

<add namespace="Sitecore.Sharedsource.Mvc" />

Resources