Stay DRY with Nested Model Builders

So, we talked about Model Builders to clean up your Sitecore Controllers in the last blog post. Now you might be wondering: "Well, this sounds nice. It looks like some sort of design pattern - I guess design patterns are a good thing. But really, aren't you just moving some code from the controller to another class and consider your controller clean? Seriously dude, not as impressive as you might think."

Maybe you're right. But it feels like a good fit so far as we are still learning on how to integrate NitroNet in our solutions and there might be some ideas here to integrate in your Sitecore project. This is my attempt to let you be part of this journey.

I'm trying to keep the examples in these blog posts as simple as possible to get the main points across. As we go along, we might build up on previous examples. So now sounds like the perfect time to read the previous blog post which introduces Model Builders, if you haven't done so already.

Enough with the blabbering, let's start carpin all those diems.

A link list component with icons

To illustrate a possible use case for Nested Model Builders, we will build a component with...

  • ... a title
  • ... a subtitle
  • ... a list of links where each link also shows an icon

Step 1: The ILinkList Interface Template

We assume there is an Interface Template defined in Sitecore to represent those fields and we start by adding an ILinkList interface to map it with GlassMapper.

namespace MyProject.Feature.PageContent.DomainModels
{
    [SitecoreType(TemplateId = "{F1B496B2-142A-4C27-88EE-E45322067CDD}")]
    public interface ILinkList : IBaseItem
    {
        [SitecoreField("Title")]
        string Title { get; set; }

        [SitecoreField("Subtitle")]
        string Subtitle { get; set; }

        [SitecoreField("Links")]
        IList<Link> Links { get; set; }
    }
}
Sitecore Helix Tip

This domain model is defined in a Feature Module and is only aware of the corresponding Interface Template. The composition which defines where those fields will be available to authors is strictly placed in the Project Layer and is defined by template inheritance. This is where Sitecore Helix shows one of it strength to enable you to achieve loose coupling in your projects.

Step 2: The LinkListController

Next, we will add a Controller Rendering. For this example, we assume that your Interface Template is available as a datasource to the rendering. Also, the actual handlebars views won't be shown in this post.

namespace MyProject.Feature.PageContent.Areas.FeaturePageContent.Controllers
{
    public class LinkListController : GlassController<ILinkList>
    {
        private readonly IModelFactory modelFactory;

        public LinkListController(IModelFactory modelFactory)
        {
            this.modelFactory = modelFactory;
        }

        public override ActionResult Index()
        {
            var model = this.modelFactory.CreateModel<ILinkList, LinkListViewModel>(this.DataSource);

            if (model == null) return new EmptyResult();

            return this.View("modules/linklist/linklist", model);
        }
    }
}

Step 3: The LinkListViewModel

The Model Builder we need to build later is responsible to return a LinkListViewModel based on the ILinkList that we provide as an input from the datasource. The view model only contains the title, subtitle and links defined by the requirements for this component.

namespace MyProject.Feature.PageContent.Areas.FeaturePageContent.Models
{
    public class LinkListViewModel
    {
        public string Title { get; set; }
        public string Subtitle { get; set; }
        public IEnumerable<IconLinkViewModel> Links { get; set; }
    }
}
Sitecore Helix Tip

The links that should be able to show an icon are represented by an IconLinkViewModel. In this example, the concept of an IconLink will be reused in multiple features and is therefore defined in a Foundation Module.

The nested IconLinkViewModel consists of an IconViewModel and a LinkViewModel - two even "more atomic" concepts that are reused within the project. I think you get the idea. Here's the code:

namespace MyProject.Foundation.Presentation.Models
{
    public class IconLinkViewModel
    {
        public IconViewModel SvgIcon { get; set; }
        public LinkViewModel Link { get; set; }
    }
}
namespace MyProject.Foundation.Presentation.Models
{
    public class IconViewModel
    {
        public string Name { get; set; }
    }
}
namespace MyProject.Foundation.Presentation.Models
{
    public class LinkViewModel
    {
        public string Url { get; set; }
        public string Text { get; set; }
        public string Target { get; set; }
    }
}

Step 4: The LinkListModelBuilder

At this point, we created a GlassMapper interface to get data from Sitecore and we implemented a controller which asks for a view model based on that data to pass it to the view. Then, we had a look at the view model containing the information specified by the requirements, which itself is composed of feature specific properties and reusable building blocks.

The last step left is to actually build those models. Let's start at the top:

namespace MyProject.Feature.PageContent.ModelBuilder
{
    [UsedImplicitly]
    public class LinkListModelBuilder : AbstractModelBuilder<ILinkList, LinkListViewModel>
    {
        private readonly IModelFactory modelFactory;
        private readonly ILinkListService linkListService;

        public LinkListModelBuilder(
            ISitecoreContext sitecoreContext,
            IModelFactory modelFactory,
            ILinkListService linkListService)
            : base(sitecoreContext)
        {
            this.modelFactory = modelFactory;
            this.linkListService = linkListService;
        }

        public override LinkListViewModel Build(ILinkList input)
        {
            return new LinkListViewModel
            {
                Title = this.GlassHtml.Editable(input, i => i.Title),
                Subtitle = this.GlassHtml.Editable(input, i => i.Subtitle),
                Links = this.GetIconLinks(input)
            };
        }

        private IEnumerable<IconLinkViewModel> GetIconLinks(ILinkList input)
        {
            var linkItems = this.linkListService.GetLinkListItems(input);
            return linkItems.Select(link => this.modelFactory.CreateModel<LinkListItem, IconLinkViewModel>(link));
        }
    }
}
Sitecore Helix Tip

The LinkListModelBuilder is located within the Feature Module, as it implements the logic to create a view model only relevant to this feature.

The internals of the this.linkListService.GetLinkListItems(input) method are not shown here to simplify the example. In essence, it takes the links from the input and returns a list of LinkListItems like this:

namespace MyProject.Feature.PageContent.Models
{
    public class LinkListItem
    {
        public LinkType Type { get; set; }
        public string Url { get; set; }
        public string Text { get; set; }
        public string Target { get; set; }
    }
}

Each LinkListItem contains the information to build an IconLinkViewModel. The task of building those is delegated to a nested ModelBuilder.

namespace MyProject.Feature.PageContent.ModelBuilder
{
    [UsedImplicitly]
    public class IconLinkModelBuilder : AbstractModelBuilder<LinkListItem, IconLinkViewModel>
    {
        private readonly IModelFactory modelFactory;
        private readonly ISvgIconService svgIconService;

        public IconLinkModelBuilder(
            ISitecoreContext sitecoreContext,
            IModelFactory modelFactory,
            ISvgIconService svgIconService)
            : base(sitecoreContext)
        {
            this.modelFactory = modelFactory;
            this.svgIconService = svgIconService;
        }

        public override IconLinkViewModel Build(LinkListItem input)
        {
            return new IconLinkViewModel
            {
                SvgIcon = this.modelFactory.CreateModel<ISvgIcon, IconViewModel>(this.GetIcon(input)),
                Link = this.modelFactory.CreateModel<LinkListItem, LinkViewModel>(input)
            };
        }

        private ISvgIcon GetIcon(LinkListItem item)
        {
            switch (item.Type)
            {
                case LinkType.Media:
                    return this.svgIconService.GetIcon(Icons.Download.Id);
                default:
                    return this.svgIconService.GetIcon(Icons.ArrowRight.Id);
            }
        }
    }
}
Sitecore Helix Tip

Because we consider the responsibility of choosing an icon part of the feature, this Model Builder is also located in the same Feature Module. If another feature needs to build IconLinkViewModels based on its own input, it can define its own ModelBuilders without breaking the feature boundaries. This approach enables you to always place the logic exactely where it belongs while staying decoupled.

We are left with the task to build the reusable IconViewModel and LinkViewModel provided by the Foundation Layer. Therefore we can leverage those existing ModelBuilders.

namespace MyProject.Foundation.Presentation.ModelBuilder
{
    [UsedImplicitly]
    public class IconModelBuilder : AbstractModelBuilder<ISvgIcon, IconViewModel>
    {
        public IconModelBuilder(ISitecoreContext sitecoreContext)
            : base(sitecoreContext) {}

        public override IconViewModel Build(ISvgIcon datasource)
        {
            return datasource == null ? null : new IconViewModel { Name = datasource.Name };
        }
    }
}
namespace MyProject.Foundation.Presentation.ModelBuilder
{
    [UsedImplicitly]
    public class LinkModelBuilder : AbstractModelBuilder<Link, LinkViewModel>
    {
        public LinkModelBuilder(ISitecoreContext sitecoreContext)
            : base(sitecoreContext) {}

        public override LinkViewModel Build(Link link)
        {
            if (string.IsNullOrWhiteSpace(link?.Url)) return null;

            return new LinkViewModel
            {
                Url = link.Url,
                Text = link.Text,
                Target = link.Target
            };
        }
    }
}

That's it.

Helix Architecture Summary

To build this feature, we ended up creating or using the following things within the corresponding layers:

Feature Layer

  • ILinkList (Glass.Mapper Interface)
  • LinkListController (Sitecore Controller)
  • LinkListViewModel (Main ViewModel)
  • LinkListModelBuilder (Main ModelBuilder)
  • LinkListItem (Model)
  • IconLinkModelBuilder (Nested ModelBuilder)

Foundation Layer

  • IconLinkViewModel (Nested ViewModel)
  • IconViewModel (Nested ViewModel)
  • LinkViewModel (Nested ViewModel)
  • IconModelBuilder (Nested ModelBuilder)
  • LinkModelBuilder (Nested ModelBuilder)

This might seem like a lot for just a link list, but there are at least two major advantages to this approach:

  1. Each class or interface does have exactely one purpose
  2. Global concepts can be reused, while still allowing to be replaced