AutoMapper is used to map the ModelsBuilder Api to Umbraco ViewModels. Dave Woestenborghs has written an excellent article on 24 Days that you can read for more information. This step-by-step is basically a paired down version of his article and a heads up for gotchas we encountered when setting up our project.
(If this isn't your first project using this method, you already have this installed, so don't worry about it!)
Download and Install Stephan's custom ModelsBuilder tool for VS:
https://marketplace.visualstudio.com/items?itemName=ZpqrtBnk.UmbracoModelsBuilderCustomTool
-
First, you have to setup API Models in Umbraco. Do this by installing the NuGet package on your entire solution.
PM> Install-Package Umbraco.ModelsBuilder.Api
-
The
web.config
should automatically update with a new configuration key, but if not, you'll need to add it (also make sure you update all other versions of the web.config, such asweb.dev.config
, etc.). You'll also want to update your other ModelsBuilder configuration keys to match below. Your end result should look like:<add key="Umbraco.ModelsBuilder.Enable" value="true" /> <add key="Umbraco.ModelsBuilder.ModelsMode" value="Nothing" /> <add key="Umbraco.ModelsBuilder.EnableApi" value="true" />
(Note: You have to be running your site in debug mode for the API to work - this means you cannot generate from live sites!)
(Additional Pro Tip: If you want to use C# 6 awesomeness, add this line to your web.config, too:)
<add key="Umbraco.ModelsBuilder.LanguageVersion" value="CSharp6" />
- Open Visual Studio and make sure you're on the current project's solution.
- Go to Tools > Options, locate the Umbraco section in the list, and select ModelsBuilder Options.
- Add the site URL (local is fine) and the Umbraco Backoffice Username and Password. You can likely find these in 1Password if you don't know what they are.
- Add your root model for the generated ModelsBuilder files to load to. For our projects, we're using: Models/ModelsBuilder/ModelsBuilder.cs.
- Once you've created the file, right click and select Properties. In the Custom Tool field, add:
UmbracoModelsBuilder
. - If everything is set up correctly to this point, the models should automagically generate when you save. To build them in the future, right click that
ModelsBuilder.cs
file and select Run Custom Tool. When you drop open yourModelsBuilder.cs
file, you should see all the generated models beneath it.
(Note: If you're not seeing anything happen or are getting connection errors, log into the back office, go to the Developer tab and select ModelsBuilder. You can see if the API is properly enabled there.)
AutoMapper comes pre-installed with Umbraco, so you don't have to add any additional NuGet packages to get it up and running. This section is about configurating your models to your view models and the different steps that need to be implemented to get it running.
The obvious note might be that we're converting from a ModelsBuilder model in a DocType to a ViewModel with the same properties, but I feel it needs to be said. While you can map versions of a DocType to only take part of the properties on the original ViewModel, you still want the properties to be named the same thing!
As an example, if we look at our BlogArticle.generated.cs
, we see:
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
//
// Umbraco.ModelsBuilder v3.0.10.102
//
// Changes to this file will be lost if the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Web;
using Umbraco.Core.Models;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Web;
using Umbraco.ModelsBuilder;
using Umbraco.ModelsBuilder.Umbraco;
namespace Villas.Web.Models.ModelsBuilder
{
/// <summary>Blog Article</summary>
[PublishedContentModel("blogArticle")]
public partial class BlogArticle : PublishedContentModel, IComponentBannerWithHeadline, IComponentFeatureImage, IComponentSeoNavigation
{
#pragma warning disable 0109 // new is redundant
public new const string ModelTypeAlias = "blogArticle";
public new const PublishedItemType ModelItemType = PublishedItemType.Content;
#pragma warning restore 0109
public BlogArticle(IPublishedContent content)
: base(content)
{ }
#pragma warning disable 0109 // new is redundant
public new static PublishedContentType GetModelContentType()
{
return PublishedContentType.Get(ModelItemType, ModelTypeAlias);
}
#pragma warning restore 0109
public static PublishedPropertyType GetModelPropertyType<TValue>(Expression<Func<BlogArticle, TValue>> selector)
{
return PublishedContentModelUtility.GetModelPropertyType(GetModelContentType(), selector);
}
///<summary>
/// Author
///</summary>
[ImplementPropertyType("author")]
public string Author
{
get { return this.GetPropertyValue<string>("author"); }
}
///<summary>
/// Categories: Select categories that this article will display in. One or two is probably enough :)
///</summary>
[ImplementPropertyType("categories")]
public IEnumerable<IPublishedContent> Categories
{
get { return this.GetPropertyValue<IEnumerable<IPublishedContent>>("categories"); }
}
///<summary>
/// Description: A brief description of the article to display in the blog archive and landing page
///</summary>
[ImplementPropertyType("description")]
public string Description
{
get { return this.GetPropertyValue<string>("description"); }
}
///<summary>
/// Grid Content
///</summary>
[ImplementPropertyType("gridContent")]
public Newtonsoft.Json.Linq.JToken GridContent
{
get { return this.GetPropertyValue<Newtonsoft.Json.Linq.JToken>("gridContent"); }
}
///<summary>
/// Post Date: All blog articles will be organized by their post date!
///</summary>
[ImplementPropertyType("postDate")]
public DateTime PostDate
{
get { return this.GetPropertyValue<DateTime>("postDate"); }
}
///<summary>
/// Related Articles: Select a few related articles that will display at the bottom of this post.
///</summary>
[ImplementPropertyType("relatedArticles")]
public IEnumerable<IPublishedContent> RelatedArticles
{
get { return this.GetPropertyValue<IEnumerable<IPublishedContent>>("relatedArticles"); }
}
///<summary>
/// Tags: You can have as many tags as you want, but don't overwhelm your readers!
///</summary>
[ImplementPropertyType("tags")]
public IEnumerable<string> Tags
{
get { return this.GetPropertyValue<IEnumerable<string>>("tags"); }
}
///<summary>
/// Feature Image: If this page is selected to be displayed on another page (ie, in a snapshot or featured panel), this image is what will be used. If left blank, and this page has a banner, it will default to the banner image.
///</summary>
[ImplementPropertyType("featureImage")]
public IPublishedContent FeatureImage
{
get { return Villas.Web.Models.ModelsBuilder.ComponentFeatureImage.GetFeatureImage(this); }
}
///<summary>
/// Canonical Link: By default, the canonical link is the current page's URL, so you only need to fill this out if you want to change it :) Please provide the full URL (ie: http://www.cvvillas.com/about-us/)
///</summary>
[ImplementPropertyType("canonicalURL")]
public string CanonicalUrl
{
get { return Villas.Web.Models.ModelsBuilder.ComponentSeoNavigation.GetCanonicalUrl(this); }
}
///<summary>
/// Hide Newsletter Panel: Check this to hide the newsletter panel on this page only.
///</summary>
[ImplementPropertyType("hideNewsletterPanel")]
public bool HideNewsletterPanel
{
get { return Villas.Web.Models.ModelsBuilder.ComponentSeoNavigation.GetHideNewsletterPanel(this); }
}
///<summary>
/// Meta Description: One sentence (160 characters or less) describing the content on the page - think elevator speech. This should be readable by people, too, as it displays on Facebook/Twitter/Social sharing :)
///</summary>
[ImplementPropertyType("metaDescription")]
public string MetaDescription
{
get { return Villas.Web.Models.ModelsBuilder.ComponentSeoNavigation.GetMetaDescription(this); }
}
///<summary>
/// Meta Keywords: 8-10 page specific keywords separated by a comma that will help people find this page on the in-site search and on search engines like Google.
///</summary>
[ImplementPropertyType("metaKeywords")]
public string MetaKeywords
{
get { return Villas.Web.Models.ModelsBuilder.ComponentSeoNavigation.GetMetaKeywords(this); }
}
///<summary>
/// Page Title: This is displayed on the browser tab AND used for SEO. It's also used as the bookmark name. Make it person & bot friendly :)
///</summary>
[ImplementPropertyType("pageTitle")]
public string PageTitle
{
get { return Villas.Web.Models.ModelsBuilder.ComponentSeoNavigation.GetPageTitle(this); }
}
///<summary>
/// Hide in Navigation: Check this to hide this page on all navigation across the site. You'll only be able to find it via in-content or direct link!
///</summary>
[ImplementPropertyType("umbracoNaviHide")]
public bool UmbracoNaviHide
{
get { return Villas.Web.Models.ModelsBuilder.ComponentSeoNavigation.GetUmbracoNaviHide(this); }
}
}
}
Scary! Okay, but I promise it's really not scary :) All the generated files are mocked directly from Umbraco properties to the model. This includes (as you can see) properties that are on compositions. You can tell what compositions a DocType's model is extending by looking for the interfaces that the class is inheriting, like so:
public partial class BlogArticle : PublishedContentModel, IComponentBannerWithHeadline, IComponentFeatureImage, IComponentSeoNavigation
The first step will be to set up a View Model that has the same properties as your generated model. However, since some of the properties on the generated model - such as the ones coming from IComponentSeoNavigation
are used globally (we'll get to that later), we only want to call the properties we're using on this template, like so:
public class BlogArticleViewModel : ViewModelBase
{
[IgnoreMap]
public IPublishedContent BlogLanding => ContentHelpers.BlogLanding();
[IgnoreMap]
public IPublishedContent BlogArchive => ContentHelpers.BlogArchive();
public ImageWithText Banner { get; set; }
public Image FeatureImage { get; set; }
public string Author { get; set; }
public DateTime PostDate { get; set; }
public IEnumerable<string> Tags { get; set; }
public IEnumerable<IPublishedContent> Categories { get; set; }
[IgnoreMap]
public Link NextArticle { get; set; }
[IgnoreMap]
public Link PreviousArticle { get; set; }
[IgnoreMap]
public IEnumerable<ArticlePreview> RelatedArticles { get; set; }
[IgnoreMap]
public Hero Hero { get; set; }
[IgnoreMap]
public bool HasPreviousArticle => PreviousArticle != null;
[IgnoreMap]
public bool HasNextArticle => NextArticle != null;
[IgnoreMap]
public bool HasTags => Tags != null && Tags.Any();
[IgnoreMap]
public bool HasRelatedArticles => Categories != null && Categories.Any();
}
Our BlogArticleViewModel
inherits from ViewModelBase
, which contains all of our global properties (which we'll discuss later). It also has a lot of properties that have AutoMapper's [IgnoreMap]
attritube. These are because the properties with [IgnoreMap]
are having their content set in 3 potential different ways:
- The property does not exist on the DocType, so it's therefore not generated in the model (such as previous/next articles)
- The property is being set directly in the ViewModel, such as our field validation
- The property has custom functionality that requires it to render new models that don't map simply, such as our Related Articles
In all these cases, we tell AutoMapper to [IgnoreMap]
so that it won't try to map them and break.
You might have noticed that we do have one property that is being told to map that doesn't exist on both models, which is the Banner
. Some DocTypes will map directly from a Model to a ViewModel, but some require custom modifications. Our Banner
is one such instance. (Note: ModelsBuilder has further documentation on how to manipulate a model's properties here, but we're going to go through a simple example (that includes a composition, so maybe slightly not as simple) in this documentation.)
The IComponentBannerWithHeadline
was originally pulling in an object
, but we want it to pull in our ImageWithText
class, which can be easily applied because our package comes with a custom Property Converter. To do this, create a new file called ComponentBannerWithHeadline.cs
, which will hold two partials in it: a partial interface, and a partial class.
public partial interface IComponentBannerWithHeadline
{
ImageWithText Banner { get; }
}
public partial class ComponentBannerWithHeadline
{
[ImplementPropertyType("banner")]
public ImageWithText Banner => this.GetPropertyValue<ImageWithText>("banner");;
}
Because of the way compositions work with ModelsBuilder, a DocType's model will actually only inherit generated properties from a composition, not custom properties like we've added here. This means we want to do a little bit extra. We're going to change our property assignment to this instead:
[ImplementPropertyType("banner")]
public ImageWithText Banner => GetBanner(this);
public static ImageWithText GetBanner(IComponentBannerWithHeadline that) { return that.GetPropertyValue<ImageWithText>("banner"); }
This allows us to then add our ImageWithText
banner to all pages (such as the BlogArticle
), but if we need to make a change to the property, we only need to change it on the composition using the static GetBanner
function.
So, next we need to create a BlogArticle.cs
so that we can add the custom banner to it. This is not a composition, so it only needs a partial BlogArticle
class and not an interface. Like the composition, though, it needs to have a property with the same name (because it's inheriting from the composition's interface):
public partial class BlogArticle
{
[ImplementPropertyType("banner")]
public ImageWithText Banner => ComponentBannerWithHeadline.GetBanner(this);
}
See how it's using the static function to get the banner and render it as an extension of the composition? This is how we keep the data manipulation in one place but extend the composition everywhere it's used, so that we have an ImageWithText
banner and not an object
:)
(Note: For manipulating content on a generated model that isn't coming from a composition, we can set the property on that model alone and not worry about building the static function to extend it.)
Now that we have the models set up, we need to map them to each other. This is done in two separate steps. One is in the controller that will render the view model and the other is in the mapper configuration. We're going to talk about the controller first and then the automapper configuration for the mappings after.
For each DocType on the site, you'll need to have a controller to render the content for the purposes of route hijacking with the view models. The basic controller would be like this (Note: You MUST have the controller named the same as your DocType or it's going to fail!):
public class BlogArticleController : RenderMvcController
{
public override ActionResult Index(RenderModel model)
{
var viewModel = Mapper.Map<BlogArticleViewModel>((BlogArticle) CurrentPage);
// All our custom stuff goes here
var blogService = new BlogService();
viewModel.Hero = blogService.GetArticleHero(CurrentPage);
viewModel.PreviousArticle = blogService.GetPreviousArticle(CurrentPage);
viewModel.NextArticle = blogService.GetNextArticle(CurrentPage);
viewModel.RelatedArticles = blogService.GetRelatedArticles(CurrentPage);
return CurrentTemplate(viewModel);
}
}
However, for all pages that are simple and don't require any modifications, you're pretty much reusing the same controller logic over and over without the custom snippets. To avoid that, I've created a base controller that uses generics. We do use custom logic for our Blog Article, but if we didn't, we could use this controller and inherit from it.
The BaseSurfaceController
:
public abstract class BaseSurfaceController<TViewModel, TBuiltModel> : RenderMvcController
where TViewModel : ViewModelBase
where TBuiltModel : PublishedContentModel
{
public override ActionResult Index(RenderModel model)
{
var viewModel = Mapper.Map<TViewModel>((TBuiltModel)CurrentPage);
return CurrentTemplate(viewModel);
}
}
The BlogArticleController
if it were to inherit from it:
public class BlogArticleController : BaseSurfaceController<BlogArticleViewModel,BlogArticle>
{
}
Now that we've set up our controller and told it to map, we need to set up our AutoMapper
configuration. I have an Events folder in my project with an AutoMapperConfiguration.cs file and it inherits from MapperConfiguration
. In it, we need to tell it how to map our models:
public class AutoMapperConfiguration : MapperConfiguration
{
public override void ConfigureMappings(IConfiguration config, ApplicationContext applicationContext)
{
config.CreateMap<HomePage, HomePageViewModel>();
#region Blog Mappings
config.CreateMap<BlogArticle, BlogArticleViewModel>();
#endregion
}
}
You'll note that my configuration is using a custom IgnoreAllNonExisting()
extension, which helps avoid issues with AutoMapper requiring all properties to be mapped or ignored. This StackOverflow article covers the issue, if you want to read it in more detail, and I've implemented the solution in an AutoMapperExtensions.cs file:
public static class AutoMapperExtensions
{
public static IMappingExpression<TSource, TDestination> IgnoreAllNonExisting<TSource, TDestination>
(this IMappingExpression<TSource, TDestination> expression)
{
var sourceType = typeof(TSource);
var destinationType = typeof(TDestination);
var existingMaps = Mapper.GetAllTypeMaps().First(x => x.SourceType.Equals(sourceType) && x.DestinationType.Equals(destinationType));
foreach (var property in existingMaps.GetUnmappedPropertyNames())
{
expression.ForMember(property, opt => opt.Ignore());
}
return expression;
}
}
Remember that you'll need to add a new mapping configuration for every mapping that you use in your code, in either direction (you may want this when you're doing form submissions), whether or not it's called in a controller, helper, service, etc.
So, this handles mapping properties that are used on pages and are being called via mappings in controllers, but what about Global properties that aren't being called by a controller because they're on a master/layout template? To do that, we set up a base view model (I name mine ViewModelBase.cs), which we inherit from all our other view models, which you can see again at the top of BlogArticleViewModel.cs:
public class BlogArticleViewModel : ViewModelBase
Our ViewModelBase
is small and at this time, contains only one thing, which is context that we'll be setting on all our controllers via an MVC FilterAttribute
.
public class ViewModelBase
{
public CvVillasContext Context { get; set; }
}
To set up this context, we have four steps we need to get through:
We have our Context set in the view model, and it contains basically all our global properties (or in this case, I've broken mine out into models that are organized inside our context):
public class CvVillasContext
{
public Seo Seo { get; set; }
public Navigation Navigation { get; set; }
public Settings SiteSettings { get; set; }
}
These models are technically view models, but I keep them separate and in a Globals section of my Models. They're very simple models that, for the most part map to a composition or part of a composition on a DocType. I'll show them all so you can get the full idea:
public class Seo
{
public string PageTitle { get; set; }
public string MetaDescription { get; set; }
public string MetaKeywords { get; set; }
public Image MetaImage { get; set; }
public string TwitterUsername { get; set; }
public string CanonicalUrl { get; set; }
public bool NoIndex { get; set; }
#region Field Validation
public bool HasMetaImage => MetaImage != null && !string.IsNullOrEmpty(MetaImage.Url);
#endregion
}
public class Navigation
{
#region Global
public GlobalHeader GlobalHeader { get; set; }
#endregion
#region Navigation
public bool HideInNavigation { get; set; }
#endregion
}
public class Settings
{
public string BodyClass { get; set; }
}
After we have these all ready, we need to set up our ViewModelFactory
to create the Context from our FilterAttribute
.
The factory starts with a simple interface that allows you to create and set new instances of the Context:
public interface IViewModelFactory
{
T Create<T>() where T : CvVillasContext, new();
void Set<T>(T model) where T : CvVillasContext, new();
}
Next, set up the class for the interface to allow us to create our context (yay!):
public class ViewModelFactory : IViewModelFactory
{
public T Create<T>() where T : CvVillasContext, new()
{
var model = new T();
Set(model);
return model;
}
public void Set<T>(T model) where T : CvVillasContext, new() { }
}
Okay, simple stuff out of the way, now we're off to...
We're setting up a FilterAttribute, which inherits from MVC's ActionFilterAttribute
and would be assigned to an action on a controller. In our case, though, we want to assign it to all controllers that are RenderMvcController
s.
The basic start is:
public class CvVillasContextFilterAttribute : ActionFilterAttribute
{
private readonly IViewModelFactory _viewModelFactory;
public CvVillasContextFilterAttribute()
{
_viewModelFactory = new ViewModelFactory();
}
public override void OnResultExecuting(ResultExecutingContext filterContext)
{
var viewModel = filterContext.Controller.ViewData.Model;
var model = viewModel as ViewModelBase;
if (model != null)
{
var controller = filterContext.Controller as RenderMvcController;
var context = _viewModelFactory.Create<CvVillasContext>();
// SET ALL OUR CUSTOM CONTEXT PROPERTIES HERE
model.Context = context;
}
base.OnResultExecuting(filterContext);
}
}
All the magic that actually needs to happen goes in the // SET ALL OUR CUSTOM CONTEXT PROPERTIES HERE
. I've set up a service for each the properties on my Context model, which I then call in this section to populate the model. We'll use the SeoService
as an example, because it does some more of our beautiful mapping magic:
public class SeoService
{
private readonly IPublishedContent _homePage = ContentHelpers.HomePage();
public Seo GetSeo()
{
// Gets the published content as the current request from Umbraco and casts it as the built Component for the SEO metadata
var currentPublishedContent = UmbracoContext.Current.PublishedContentRequest.PublishedContent as IComponentSeoNavigation;
// If Published Content is null (eep!) then we'll return an empty model
if (currentPublishedContent == null)
{
return new Seo
{
PageTitle = string.Empty,
CanonicalUrl = string.Empty,
MetaDescription = string.Empty,
MetaImage = null,
MetaKeywords = string.Empty,
NoIndex = true,
TwitterUsername = string.Empty
};
}
// Yay, we have published content! So we map it to our Seo model
return new Seo
{
CanonicalUrl = currentPublishedContent.GetSafeString("canonicalUrl", currentPublishedContent.SecureUrlWithDomain()),
MetaDescription = currentPublishedContent.MetaDescription,
MetaImage = currentPublishedContent.GetPropertyValue<Image>("metaImage", true),
MetaKeywords = currentPublishedContent.GetPropertyValue<string>("metaKeywords", true),
PageTitle = GetPageTitle(currentPublishedContent),
TwitterUsername = _homePage.GetSafeString("twitterUsername")
};
}
}
The really important thing to note here is currentPublishedContent
, as we're taking the current content request and then casting it to the IComponentSeoNavigation
so that we can use the properties directly off the generated ModelsBuilder model. Please note: If you create partial classes, but do not create partial interfaces for the purposes of custom modifications to your model, this will not work. You MUST have a partial interface as well as a partial class as demonstrated earlier.
Then, once this is done, it gets added to that nice spot in the original version that we fill in. I fully commented this for my own purposes and I'm going to leave it in there to help anyone else out.
public class CvVillasContextFilterAttribute : ActionFilterAttribute
{
private readonly IViewModelFactory _viewModelFactory;
public CvVillasContextFilterAttribute()
{
_viewModelFactory = new ViewModelFactory();
}
public override void OnResultExecuting(ResultExecutingContext filterContext)
{
// This gets the ViewData from the MVC Filter Context (aka the MVC Model's ViewDataDictionary)
var viewModel = filterContext.Controller.ViewData.Model;
// Take the view model and cast it as our custom ViewModelBase
var model = viewModel as ViewModelBase;
// If the cast is successful...
if (model != null)
{
// This was copied from Dirk's code - I'm not sure why the controller needs to be cast as a
// RenderMvcController since it doesn't seem to be called anywhere, but it makes sense that this
// filter context would ONLY be applied to controllers that are RenderMvcControllers (since we only want
// this action to be run when we're rendering our Umbraco views)
var controller = filterContext.Controller as RenderMvcController;
// Create a fresh context from our factory so that we can set the properties on it
var context = _viewModelFactory.Create<CvVillasContext>();
// Set up all our services...
var navigationService = new NavigationService();
var seoService = new SeoService();
var settingsService = new SettingsService();
// And set the properties on our context from the services (most of these are mapped from ModelsBuilder)
context.Navigation = navigationService.GetNavigation();
context.Seo = seoService.GetSeo();
context.SiteSettings = settingsService.GetSettings();
// Set the context of the model to our fancy new context
model.Context = context;
}
// Update the filter context with our custom attribute
base.OnResultExecuting(filterContext);
}
}
The final step to this whole process is to get our global properties rendering via the FilterAttribute
by applying it globally in our startup:
protected override void ApplicationStarting(UmbracoApplicationBase umbraco, ApplicationContext context)
{
GlobalFilters.Filters.Add(new CvVillasContextFilterAttribute());
}
Once you've done this, you're set to call ViewModelBase
in your Master/Layout template and you have your global properties pulling into your view and in the context across the entirety of all your view models in all your templates.