Quantcast
Viewing all articles
Browse latest Browse all 21

Use item wrappers as models in Sitecore MVC

I’ve been experimenting with Sitecore 6.6 MVC lately. I love the very clean Razor views. And if you’re used to working with a rich domain model that wraps your Sitecore items, then you’ll hardly ever need a controller rendering (because your view-logic will be on your model). Most of the layouts and sublayouts will be view renderings.

Now, when developing in ASP.NET Web forms, I will usually have a base class for my controls that has a generic type parameter that reflects what type of item it will take as a datasource. This way, I can easily access the right fields in a strong-typed fashion. And I wanted this same setup in MVC.

So, after completing the steps in this blog post, you will be able to make a Razor view like this:

1
2
3
@model UnitTestingDemo.Website.Models.IRenderingModel<UnitTestingDemo.Website.Domain.DomainModel.Page>
<h1>@Html.Sitecore().Field(() => Model.Item.Title)</h1>
@Html.Sitecore().Field(() => Model.Item.TextContent)

Line-by-line explanation:

  1. Declare the model to be a IRenderingModel-derived type (this way, it will be initialized in the same way as the default rendering model). The generic type parameter shows that this particular view will take any item that has the “Page”-template or a template that inherits from it as its datasource.
  2. Use an extension to the SitecoreHelper to reference the “Title” field. It uses a lambda expression, so that the helper method can ensure that the “renderField” pipeline is run; this ensures that among other things, the page-editor support will be preserved.
  3. The same as #2, but for the “Text content” field.

Side-note: I use my own CompiledDomainModel module (CDM) to generate wrapper classes based on Sitecore templates. So the following example code will use it as well. But you can probably easily adapt the following code to work with your mapping/wrapping framework. The code is from a project that I will soon use in a presentation for the SUGNL (hence, the UnitTestingDemo namespace).

The following code is for the RenderingModel. It allows access to the datasource itemwrapper in the type you want. Notice that the “out” keyword is used before the generic type parameter declaration. This means that the type is covariant, so that you can use a base type in your model declaration at the top of the Razor view (e.g. IRenderingModel<PageBase> instead of IRenderingModel<Page>).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
using Sitecore.Mvc.Presentation;
using UnitTestingDemo.Website.Domain;

namespace UnitTestingDemo.Website.Models
{
    /// <summary>
    /// Use this interface as a model for a Razor view if you want to use a CDM wrapper from the view.
    /// </summary>
    /// <typeparam name="T">The type of the wrapper that the model needs to support</typeparam>
    public interface IRenderingModel<out T> : IRenderingModel where T : ItemWrapper
    {
        T Item { get; }
        ItemWrapper PageItem { get; }
        Rendering Rendering { get; }
    }

    /// <summary>
    /// Default CDM rendering model implementation as used by the CreateTypedRenderingModel pipeline processor.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public class RenderingModel<T> : IRenderingModel<T> where T : ItemWrapper
    {
        private Rendering rendering;
        private T item;
        private ItemWrapper pageItem;

        public void Initialize(Rendering rendering)
        {
            this.rendering = rendering;
        }

        /// <summary>
        /// Returns the CDM wrapped item.
        /// This can be the context item, or the item based on the datasource of the rendering.
        /// </summary>
        public virtual T Item
        {
            get
            {
                if (item == null)
                {
                    ItemWrapper typedWrapper = ItemWrapper.CreateTypedWrapper(rendering.Item);
                    if (typedWrapper is T)
                    {
                        item = (T) typedWrapper;
                    }
                }
                return item;
            }
        }

        /// <summary>
        /// The wrapped item for the entire page (from the page context).
        /// </summary>
        public virtual ItemWrapper PageItem
        {
            get
            {
                return pageItem ?? (pageItem = ItemWrapper.CreateTypedWrapper(PageContext.Current.Item));
            }
        }

        public virtual Rendering Rendering
        {
            get { return rendering; }
        }
    }
}

To create the RenderingModel, we need to plug the following in to the “mvc.GetModel” pipeline.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Sitecore.Mvc.Pipelines.Response.GetModel;
using Sitecore.Mvc.Presentation;
using UnitTestingDemo.Website.Domain;
using UnitTestingDemo.Website.Models;

namespace UnitTestingDemo.Website.Pipelines
{
    /// <summary>
    /// This processor can be used in the mvc.GetModel pipeline to resolve the current CDM based model,
    /// from the context item or the datasource (if defined).
    /// The rendering must have the parameter 'typedmodel=1' defined, or else this processor will leave it alone.
    /// </summary>
    public class CreateTypedRenderingModel : GetModelProcessor
    {

        public override void Process(GetModelArgs args)
        {
            if (args.Result == null && "1".Equals(args.Rendering.Parameters["typedmodel"]))
            {
                args.Result = CreateTypedModel(args.Rendering);
            }
        }

        /// <summary>
        /// Uses reflection to create a CDM based model for a rendering
        /// </summary>
        /// <param name="rendering"></param>
        /// <returns></returns>
        protected virtual object CreateTypedModel(Rendering rendering)
        {
            Type targetType = ItemWrapper.GetTypeForTemplateId(rendering.Item.TemplateID);
            var d1 = typeof(RenderingModel<>);
            Type[] typeArgs = { targetType };
            var makeme = d1.MakeGenericType(typeArgs);
            return Activator.CreateInstance(makeme);
        }
    }
}

Hook this code up to the “mvc.GetModel” pipeline using the following configuration (use a config file in the “/App_Config/Include” folder to make a nice patch of this):

1
2
3
4
5
6
7
    <pipelines>
      <mvc.getModel>
        <processor
          patch:before="*[@type='Sitecore.Mvc.Pipelines.Response.GetModel.CreateDefaultRenderingModel, Sitecore.Mvc']"
          type="UnitTestingDemo.Website.Pipelines.CreateTypedRenderingModel, UnitTestingDemo.Website"/>
      </mvc.getModel>
    </pipelines>

Now, when you make a rendering, be sure to add “typedmodel=1″ to the parameters field in Sitecore for that rendering. That way, the “mvc.GetModel” pipeline will know when to create a typed model.

And you’re all set to use the typed model in your Razor views. But there’s one thing left; the lambda syntax support. This works in the same way as the CdmFieldRenderer control from a previous blog post. You can use the following helper class:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
using System;
using System.Linq.Expressions;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Web;
using Sitecore.Collections;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Mvc.Helpers;
using UnitTestingDemo.Website.Domain;

namespace UnitTestingDemo.Website.Util
{
    public static class SitecoreHelperExtensions
    {
        private static readonly Regex ConstNameReplace = new Regex("([A-Z]+[a-z]+)", RegexOptions.Compiled);

        /// <summary>
        /// Render a field by passing an expression that references a CDM class object property.
        /// Using this method ensures typesafe access and preserves web edit functionality.
        /// </summary>
        /// <param name="sitecoreHelper"></param>
        /// <param name="value">An expression that references a CDM class object property</param>
        /// <param name="disableWebEdit"></param>
        /// <param name="parameters"></param>
        /// <returns></returns>
        public static HtmlString Field(
            this SitecoreHelper sitecoreHelper,
            Expression<Func<object>> value,
            bool disableWebEdit = false,
            SafeDictionary<string> parameters = null)
        {
            if (parameters == null)
            {
                parameters = new SafeDictionary<string>();
            }

            var fieldParams = ParamsFor(value);

            if (fieldParams == null || ! fieldParams.HasValue)
            {
                return null;
            }

            HtmlString result = sitecoreHelper.Field(fieldParams.FieldName, fieldParams.Item, new
                {
                    DisableWebEdit = disableWebEdit, Parameters = parameters
                });
            return result;
        }

        private static FieldRendererParams ParamsFor(Expression<Func<object>> func)
        {
            // strip unary expression if needed, like boxing conversion for value types
            Expression valueExpr = func.Body;
            var unaryExpression = valueExpr as UnaryExpression;
            if (unaryExpression != null)
            {
                valueExpr = unaryExpression.Operand;
            }

            var memberExpression = valueExpr as MemberExpression;
            Assert.IsNotNull(memberExpression, "Please enter a valid member expression for CdmFieldRenderer that evalutes to a CDM property");
            if (memberExpression != null && memberExpression.Expression != null)
            {
                // get the name of the property in the CDM class
                string propertyName = memberExpression.Member.Name;

                // get the itemwrapper by invoking the contained expression
                Delegate compiledExpr = Expression.Lambda(memberExpression.Expression).Compile();
                var itemWrapper = compiledExpr.DynamicInvoke() as IItemWrapper;
                if (!string.IsNullOrEmpty(propertyName) && itemWrapper != null)
                {
                    // attempt to determine the name of the constant that holds the field name
                    string constantName = ConstNameReplace.Replace(propertyName, m => (m.Value.Length > 3 ? m.Value : m.Value.ToLower()) + "_");
                    constantName = string.Format("FIELD_{0}", constantName).ToUpperInvariant();
                    if (constantName.LastIndexOf("_", StringComparison.Ordinal) > -1)
                        constantName = constantName.Remove(constantName.LastIndexOf("_", StringComparison.Ordinal), 1);

                    FieldInfo constant = itemWrapper.GetType().GetField(constantName, BindingFlags.Static | BindingFlags.Public | BindingFlags.FlattenHierarchy);
                    if (constant != null)
                    {
                        // set the correct values on the control, so the field can be rendered and is compatible with the page editor
                        return new FieldRendererParams
                        {
                            Item = itemWrapper.Item,
                            FieldName = constant.GetValue(null) as string
                        };
                    }
                    throw new Exception(string.Format("Unable to reflect field-property '{0}' for propertyname '{1}'", constantName, propertyName));
                }
            }
            return new FieldRendererParams();
        }

        #region Nested type: FieldRendererParams

        public class FieldRendererParams
        {
            public Item Item { get; set; }
            public string FieldName { get; set; }

            public bool HasValue
            {
                get
                {
                    if (Item == null || string.IsNullOrEmpty(FieldName))
                        return false;
                    return !string.IsNullOrEmpty(Item[FieldName]);
                }
            }
        }

        #endregion
    }
}

To be able to use this extension without having to import it every time, you can add it to the “/Views/Web.config” file, like this:

1
2
3
4
5
6
7
8
9
<system.web.webPages.razor>
...
<pages pageBaseType="System.Web.Mvc.WebViewPage">
      <namespaces>
        ...
        <add namespace="Sitecore.Mvc" />
        <add namespace="UnitTestingDemo.Website.Util"/>
      </namespaces>
</pages>

I know it takes a little time to setup, but it makes your Razor views very clean and you’ll have code-completion for your field names. If you want, you can compile your Razor views at build-time by editing your MVC project file and setting “<MvcBuildViews>true</MvcBuildViews>”.

Next week, I’ll be doing a presentation on unit testing with test fixtures and I’ll also get into unit testing the Razor view that I mentioned here. The whole project and the slides will be available for download on this blog afterwards.


Viewing all articles
Browse latest Browse all 21

Trending Articles