In Sitecore, the MVC framework provides a “default” model type that does some very necessary work.
To support the view, Sitecore’s default model type Sitecore.Mvc.Presentation.RenderingModel provides you access to
- The “current” item – passed via a datasource or the context item if no datasource is used
- The PageContext item (which may be different if a datasource is used)
- A reference to the Rendering used by the model
In some cases, this is very sufficient – but there are occasions where a developer might want to add a property to the model to remove code blocks that would otherwise have to be placed within the view.
Sitecore does allow you to create a custom model class – as long as you implement Sitecore.Mvc.Presentation.IRenderingModel, and support the above 3 properties, you can create a model from scratch.
Rather than writing a custom model from scratch every time I needed one, I decided to use Generics to accomplish the task.
Step 1 – Writing a CustomItem class
If you are familiar with Sitecore, you have certainly seen the Sitecore.Data.Items.CustomItem class. This abstract type is a good foundation to use to write a custom domain item or DTO based on a Sitecore Item type in your project. Lots of Sitecore custom modules such as the CustomItemGenerator use this base type when creating custom item classes.
Writing a custom item is easy – simply support a constructor that takes a Sitecore.Data.Item as an argument, and you’re ready to go. Here’s a sample CustomItem to support a Breadcrumb view:
using System; using System.Collections.Generic; using Sitecore.Data; using Sitecore.Data.Items; using Sitecore.Diagnostics; using MyStuff.Web.Extensions; namespace MyProject.Web.Models.Items.Navigation { public class BreadcrumbItem : CustomItem { /// <summary> /// Constructor to build a NavigableItem from a Sitecore Item /// </summary> /// <param name="item"></param> protected BreadcrumbItem(Item item) : base(item) { CrumbStack = new Stack<Item>(); Item cur = item; while (cur != null && !cur.IsDerived(new ID(TemplateIds.WebsiteFolder))) { CrumbStack.Push(cur); cur = cur.Parent; } } public Stack<Item> CrumbStack { get; private set; } } }
Step 2 – Creating a Model base type that handles generics
First, the interface:
using Sitecore.Data.Items; using Sitecore.Mvc.Presentation; namespace MyProject.Web.Models { public interface ICustomItemRenderingModel<out T> : IRenderingModel where T : CustomItem { T CustomItem { get; } Item Item { get; } Item PageItem { get; } Rendering Rendering { get; } } }
The important part of this is the <out T> modifier – in C# 4, this allows you to use covariance … in simple terms, this allows you to use a derived type instead of a specific type as the generic type. In this case, T can be of any type that is derived from CustomItem.
Now the implementation
using System; using System.Reflection; using Sitecore.Data.Items; using Sitecore.Mvc.Presentation; namespace MyProject.Web.Models { public class CustomItemRenderingModel&lt;T&gt; : ICustomItemRenderingModel&lt;T&gt; where T : CustomItem { private Rendering _rendering; private T _customItem; private Item _pageItem; private Item _item; public void Initialize(Rendering rendering) { _rendering = rendering; } public T CustomItem { get { if (_customItem != null) return _customItem; // find the constructor and call it Type type = typeof(T); ConstructorInfo constructor = type.GetConstructor(BindingFlags.NonPublic | BindingFlags.CreateInstance | BindingFlags.Instance, null, new[] { typeof(Item) }, null); if (constructor == null) return null; _customItem = (T)constructor.Invoke(new object[] { _rendering.Item }); return _customItem; } } public Item PageItem { get { return _pageItem ?? (_pageItem = PageContext.Current.Item); } } public Item Item { get { return _item ?? (_item = _rendering.Item); } } public virtual Rendering Rendering { get { return _rendering; } } } }
Step 3 – Using the Generic Type
This really should be the easy part, but not so much. To use your new CustomItem / Generic model, you simply add the model statement to the view
@model MyProject.Web.Models.CustomItemRenderingModel<MyProject.Web.Models.Navigation.BreadcrumbItem>
and then of course in Sitecore create a Model item in the Layouts folder that declares the type. Note that you have to use the alternate syntax to declare the type in your model item:
MyProject.Web.Models.CustomItemRenderingModel`1[MyProject.Web.Models.Items.Navigation.BreadcrumbItem],MyAssembly.Web
Step 4 – Replacing the Model Locator within Sitecore to support Generics
As of Sitecore 6.6 update 5, this step is required because Sitecore will fail to create the Model with the Generics declaration. Once you have narrowed it down a bit, you have to do 2 things to support models with generics:
First, create your own model locator and override the method object GetModelFromTypeName(string typeName, string model, bool throwOnTypeCreationError)
using System; using System.Text.RegularExpressions; using Sitecore.Mvc.Helpers; using Sitecore.Mvc.Presentation; namespace MyProject.Web.Models { public class CustomModelLocator : ModelLocator { private const string GenericsPattern = @"`d[.+]"; protected override object GetModelFromTypeName(string typeName, string model, bool throwOnTypeCreationError) { Type type = Regex.IsMatch(typeName, GenericsPattern, RegexOptions.Singleline) ? Type.GetType(typeName) : TypeHelper.GetType(typeName); if (type == null) { if (!throwOnTypeCreationError) return null; throw new InvalidOperationException(Sitecore.StringExtensions.StringExtensions.FormatWith("Could not locate type '{0}'. Model reference: '{1}'", (object)typeName, (object)model)); } object @object = TypeHelper.CreateObject(type, new object[0]); if (@object != null || !throwOnTypeCreationError) return @object; throw new InvalidOperationException(Sitecore.StringExtensions.StringExtensions.FormatWith("Could not create a model object of type '{0}'. Model reference: '{1}'", (object)typeName, (object)model)); } } }
You can tell Sitecore to use your model locator by adding this method to the <initialize> pipeline
using MyProject.Web.Models; using Sitecore; using Sitecore.Mvc.Configuration; using Sitecore.Pipelines; namespace MyProject.Web.Pipelines { [UsedImplicitly] public class SetModelLocator { public virtual void Process(PipelineArgs args) { MvcSettings.RegisterObject&lt;Sitecore.Mvc.Presentation.ModelLocator&gt;(() =&gt; new CustomModelLocator()); } } }
Just patch that method into the pipeline (I did it as the first processor in the pipeline) and now you’re all set.
I followed all the steps but still sitecore is not able to locate the generic model.
Please explain Step3 in detail
MyProject.Web.Models.CustomItemRenderingModel`1[MyProject.Web.Models.Items.Navigation.BreadcrumbItem],MyAssembly.Web
Tell me the parameters I have to mention for step 3.