Getting a Sitecore Model From The cshtml (View)

As most of you will no doubt be aware, I am an advocate of using an ORM (you decide – I personally like Glass Mapper For Sitecore) in your Sitecore solutions. A little while back the magnificent John West did a great post on a proof of concept for getting a model from the .cshtml rather than defining it in Sitecore. I thought this was awesome and since then I have been experimenting with it as an idea.

I have kept this example based on vanilla Sitecore, however, would take very minor modification to use with an ORM.

Original Version

The original version had a couple of issues (sorry John), in terms of being production ready immediately – as stated by him, it is a proof of concept and this was expected.

The main one was that it began to complain if the layout did not have a model specified, in many cases in my code, I do not have a layout with a model since it usually only contains some boilerplate html and a few @Html.Sitecore().Placeholder(“whatever”).

The next issue was that the getting of the compiled cshtml was not the fastest (don’t get me wrong, its not bad at all – but I have just spent months getting performance out of Sitecore and want that to continue). In my final version under test there is about 500 requests / min additional gained and 5 – 10% cpu performance. Compared to the gains I have managed to get elsewhere, this is trivial, but every little helps right ?

My Version

Ok, so here we have my version, as with John’s original, this code is supplied as is, no warranty etc etc..

I will quickly run through:

Model Caching

To overcome the slower performance of the getting of the compiled type, I introduced a cache manager. I abstracted this away in some structures of their own. This will allow me to add / get from a thread safe cache at runtime. I have opted for the ConcurrentDictionary as a mechanism to achieve this though there are other options.


using System;

namespace RenderingsTest.Code
{
    public interface IModelCacheManager
    {
        void Add(string path, Type modelType);

        Type Get(string path);

        string GetKey(string path);
    }
}


In implementation, I needed a way to also ensure that if files changed on disk, that the cache would be invalidated as a consequence. I did some experimenting and settled on the file system watcher class. I *believe* that since the underlying cache is thread safe, that the FileSystemWatcher will work quite nicely with it.

Also note that to prevent costly calls to Server.MapPath for every run through the pipeline, I cache a map between path (from Sitecore) and full path (on disk) to prevent this having to be re-called.


using System;
using System.Collections.Concurrent;
using System.IO;
using System.Web;

namespace RenderingsTest.Code
{
    public class ModelCacheManager : IModelCacheManager
    {
        public static FileSystemWatcher FileSystemWatcher { get; private set; }

        public static ConcurrentDictionary<string, Type> CachedTypes { get; private set; }

        public static ConcurrentDictionary<string, string> CachedKeys { get; private set; }

        static ModelCacheManager()
        {
            CachedKeys = new ConcurrentDictionary<string, string>();
            CachedTypes = new ConcurrentDictionary<string, Type>();
            FileSystemWatcher = new FileSystemWatcher(HttpContext.Current.Server.MapPath("/"))
            {
                Filter = "*.cshtml",
                IncludeSubdirectories = true
            };

            FileSystemWatcher.Changed += FileSystemWatcher_Changed;
            FileSystemWatcher.Deleted += FileSystemWatcher_Changed;
            FileSystemWatcher.Renamed += FileSystemWatcher_Renamed;
            FileSystemWatcher.EnableRaisingEvents = true;
        }

        private static void FileSystemWatcher_Renamed(object sender, RenamedEventArgs e)
        {
            Type outType;
            CachedTypes.TryRemove(e.OldFullPath, out outType);
        }

        private static void FileSystemWatcher_Changed(object sender, FileSystemEventArgs e)
        {
            Type outType;
            CachedTypes.TryRemove(e.FullPath, out outType);
        }

        public virtual void Add(string path, Type modelType)
        {
            CachedTypes.TryAdd(path, modelType);
        }

        public virtual Type Get(string path)
        {
            return CachedTypes.ContainsKey(path) ? CachedTypes[path] : null;
        }

        public string GetKey(string path)
        {
            string realPath;
            if (CachedKeys.ContainsKey(path))
            {
                realPath = CachedKeys[path];
            }
            else
            {
                realPath = HttpContext.Current.Server.MapPath(path);
                CachedKeys.TryAdd(path, realPath);
            }

            return realPath;
        }
    }
}

The pipeline processor
As with all things Sitecore, we will simply patch it into the pipeline

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <mvc.getModel>
        <processor patch:before="processor[1]" type="RenderingsTest.Code.GetModelFromView,RenderingsTest"/>
      </mvc.getModel>
    </pipelines>
  </sitecore>
</configuration>

I used a null model in order to ensure that I have dealt with the object through the cache meaning I don’t attempt to re-get the model for objects that do not have an @model declaration at the top.

The processor therefore looks like this:


using System;
using System.Web.Compilation;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Mvc.Pipelines.Response.GetModel;

namespace RenderingsTest.Code
{
    public class GetModelFromView : GetModelProcessor
    {
        private readonly IModelCacheManager modelCacheManager;

        public GetModelFromView() : this(new ModelCacheManager())
        {
            
        }

        public GetModelFromView(IModelCacheManager modelCacheManager)
        {
            this.modelCacheManager = modelCacheManager;
        }

        public override void Process(GetModelArgs args)
        {
            if (!IsValidForProcessing(args))
            {
                return;
            }

            string path = GetViewPath(args);

            if (string.IsNullOrWhiteSpace(path))
            {
                return;
            }

            string cacheKey = modelCacheManager.GetKey(path);
            Type modelType = modelCacheManager.Get(cacheKey);

            if (modelType == typeof (NullModel))
            {
                // The model has been attempted before and is not useful
                return;
            }

            // The model type hasn't been found before or has been cleared.
            if (modelType == null)
            {
                modelType = GetModel(args, path);

                modelCacheManager.Add(cacheKey, modelType);

                if (modelType == typeof (NullModel))
                {
                    // This is not the type we are looking for
                    return;
                }
            }
            
            // If you are using an orm, replace this line with an orm specific implementation.
            args.Result = Activator.CreateInstance(modelType);
            Assert.IsNotNull(args.Result, "args.Result");
        }

        private string GetPathFromLayout(
            Database db,
            ID layoutId)
        {
            Item layout = db.GetItem(layoutId);

            return layout != null
                ? layout["path"]
                : null;
        }

        private string GetViewPath(GetModelArgs args)
        {
            string path = args.Rendering.RenderingItem.InnerItem["path"];

            if (string.IsNullOrWhiteSpace(path) && args.Rendering.RenderingType == "Layout")
            {
                path = GetPathFromLayout(args.PageContext.Database, new ID(args.Rendering.LayoutId));
            }
            return path;
        }

        private Type GetModel(GetModelArgs args, string path)
        {
            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 typeof(NullModel);
            }

            Type proposedType = baseType.GetGenericArguments()[0];
            return proposedType == typeof (object) 
                ? typeof (NullModel) 
                : proposedType;
        }

        private static bool IsValidForProcessing(GetModelArgs args)
        {
            if (args.Result != null)
            {
                return false;
            }

            if (!String.IsNullOrEmpty(args.Rendering.RenderingItem.InnerItem["Model"]))
            {
                return false;
            }

            return args.Rendering.RenderingType == "Layout" ||
                   args.Rendering.RenderingType == "View" ||
                   args.Rendering.RenderingType == "r";
        }
    }

    public class NullModel
    {
        
    }
}

Happy Mapping & once again, thank you John West 😀

Advertisements

4 thoughts on “Getting a Sitecore Model From The cshtml (View)

  1. Pingback: Glass V4 – Model From View | CardinalCore

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s