Sunday, 2 November 2014

Using Umbraco Mapper with the Umbraco Grid

Like most people in the Umbraco community I've been keen to download and have a play with Umbraco 7.2, released in beta last week. There's a lot of new features, but the shiniest new toy is without doubt the grid.

This allows a developer to set up a very flexible interface for the editor to manage their content in a layout that closely reflects what they'll see on the website.

When it comes to rendering out the content of the grid, it's very simple. In your view you can just make a call to CurrentPage.GetGridHtml("myGridAlias") and this will spit out the HTML wrapped with bootstrapped elements. You can pass in overrides to use a different template, and create your own.

Nothing wrong with this at all but my preferred approach when rendering out Umbraco content is to create strongly typed view models and, via a hijacked route, map the Umbraco content to this model with the help of the Umbraco Mapper package. Once done, this provides a very clean view model for front-end developers to work with, avoiding the need to work with the more complex Umbraco APIs and dynamics in the views.

Clearly the output of the grid, even if strongly typed, is still going to be relatively "loose" in that we need to be able to handle all that we allow our editors to throw at it. Nonetheless, it seemed to a useful exercise to see how Umbraco Mapper could be used to handle mapping from the Grid data type - and am pleased to say it looks to be rather straightforward.

In order to have something to map too we need our own custom class. The following has been constructed to follow the structure of the JSON format that the grid data is stored in the database:

namespace MyApp.Models
{
    using System.Collections.Generic;
    using System.Web.Mvc;
    using Newtonsoft.Json;

    public class GridContent
    {
        public IList<GridSection> Sections { get; set; }

        public class GridSection
        {
            public int Size { get; set; }

            public IList<GridRow> Rows { get; set; }

            public class GridRow
            {
                public string Name { get; set; }

                public IList<GridArea> Areas { get; set; }

                public class GridArea
                {
                    public IList<GridControl> Controls { get; set; }

                    public class GridControl
                    {
                        public object Value { get; set; }

                        public GridEditor Editor { get; set; }

                        public object TypedValue 
                        {
                            get
                            {
                                switch (Editor.Alias)
                                {
                                    case "headline":
                                    case "quote":
                                        return Value.ToString();
                                    case "rte":
                                        return new MvcHtmlString(Value.ToString());
                                    case "media":
                                        return JsonConvert.DeserializeObject<GridContentMediaValue>(Value.ToString());
                                    default:
                                        return string.Empty;
                                }
                            }
                        }

                        public class GridEditor
                        {
                            public string Alias { get; set; }
                        }
                    }
                }
            }
        }
    }
}

A key property to note is TypedValue. This is used to convert the raw value saved for the grid control element into a typed value appropriate for the content that is stored. This might be a simple string or something more complex like a media item. The implementation of this property would need to be extended to handle all the types we allow our editors to use.

For example here's a complex type representing a media item, again following the JSON structure of the saved information:

namespace MyApp.Models
{
    public class GridContentMediaValue
    {
        public int Id { get; set; }

        public string Image { get; set; }

        public string Thumbnail { get; set; }
    }
}

Then for a given page, I've created a view model that looks like this, following the structure of my document type that uses the grid data type.

namespace MyApp.Models
{
    public class GridPageViewModel
    {
        public string Heading { get; set; }

        public GridContent Grid { get; set; }
    }
}

Umbraco Mapper has a handy feature called custom mappings. These can be set up when the mapper is instantiated - usually in a single location like a base controller - to effectively say, "whenever you are given a given type on a view model to map to, use this function".

With that feature, I'm able to do this in my controller:

namespace MyApp.Controllers
{
    using System.Web.Mvc;
    using Newtonsoft.Json;
    using Umbraco.Core.Models;
    using Umbraco.Web;
    using Umbraco.Web.Models;
    using Umbraco.Web.Mvc;
    using Umbraco72a.Models;
    using Zone.UmbracoMapper;

    public class GridPageController : RenderMvcController
    {
        public override ActionResult Index(RenderModel model)
        {
            var mapper = new UmbracoMapper();
            mapper.AddCustomMapping(typeof(GridContent).FullName, MapGridContent);

            var vm = new GridPageViewModel();
            mapper.Map(CurrentPage, vm);

            return View("Grid", vm);
        }

        public static object MapGridContent(IUmbracoMapper mapper, IPublishedContent contentToMapFrom, string propName, bool isRecursive)
        {
            var gridJson = contentToMapFrom.GetPropertyValue<string>(propName, isRecursive);
            return JsonConvert.DeserializeObject<GridContent>(gridJson);
        }   
 }
}

Which finally allows me to render my grid content in a very clean, strongly typed way, like this:

@model MyApp.Models.GridPageViewModel

<div>
 <h1>@Model.Heading</h1>

 @foreach (var section in Model.Grid.Sections)
 {
  <div>
   @foreach (var row in section.Rows)
   {
    <div>
     @foreach (var area in row.Areas)
     {
      <div>
       @foreach (var control in area.Controls)
       {
        <div class="@control.Editor.Alias">
         @switch (control.Editor.Alias)
         {
          case "media":
           var media = control.TypedValue as GridContentMediaValue;
           <img src="@media.Image" />
           break;
          default:
           <span>@control.TypedValue</span>
           break;
         }                                        
        </div>
       }
      </div>   
     }
    </div>
   }
  </div>
 }
</div>

For me this looks to provide a nice balance between allowing the editor the flexibility that the grid editor provides whilst retaining the strongly typed view model and mark-up generation code in the view. Would love to hear comments from anyone else playing with the grid editor and similarly considering best practices for using it.

4 comments:

  1. Great blogpost. There also was a small chat about it on Twitter: https://twitter.com/crumpled_jeavon/status/527759407535628288

    ReplyDelete
  2. That package looks good to handle Grids.
    https://github.com/skybrud/Skybrud.Umbraco.GridData
    What are your thoughts?

    ReplyDelete
  3. Looks useful I agree. Personally I slightly prefer the approach I've outlined here as it allows me to define the view model type - the GridDataModel (or GridContent in my example) - but it could certainly work just as well if I took a dependency on that in the view model, and used the property value converter provided there to do the conversion from the Umbraco JSON data to this strongly typed model.

    ReplyDelete
  4. Defo usefull...Great start point!

    ReplyDelete