Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dillorscroft/bf04b12954eb3b9c87c66caa63fc5c75 to your computer and use it in GitHub Desktop.
Save dillorscroft/bf04b12954eb3b9c87c66caa63fc5c75 to your computer and use it in GitHub Desktop.
Pattern for binding Nested Content content to strongly-typed models (via Models Builder) in Umbraco CMS v7.
This pattern has a couple of assumptions built in:
1) All nested content partial views will be saved in the directory: /Views/Partials/NestedContent
2) All nested content partials views will be named using the alias of the Document Type the
nested content is based on, but starting with an underscore. For example: Document Type
"Full Width Text Component" (alias = fullWidthTextComponent) should have a partial view
named "_FullWidthTextComponent.cshtml".
3) All Models Builder generated data models are namespaced to "AM.Models".
Here's how this works:
Take a look at Home.cshtml. Here we have a template view that is bound to the HomePageModel
data model. That model has a property named ContentComponents which is an
IEnumerable<IPublishedContent> of content (created with a Nested Content Data Type).
To render model.ContentComponents we call an HTML helper and pass it the property:
@Html.RenderNestedContentPartials(Model.ContentComponents)
HtmlHelperExtensions.cs is where we define the RenderNestedContentPartials() helpers.
Here we attempt to find a data model class for each of the Nested Content items. If we're
successful we pass the item's Document Type alias, the data model object, and an optional
ViewDataDictionary to a Partial renderer helper and return what we get back. If we're not
successful in mapping the item to a data model we pass the item as an IPublishedContent
object to the Partial helper.
TypeHelper.cs is a helper that will take an IPublishedContent item and attempt to map
it to a data model class in the AM.Models namespace (of course, this namespace would be
whatever is appropriate to your project). The helper is a singleton that contains a
dictionary of found types (so we don't have to do the lookup and instantiation on every
request). If we find a match then it instantiates an instance of the data model and
returns it.
Finally, NestedContentViewEngine.cs registers the location of our partial view files to
the RazorViewEngine. Of course, you could change this to be a different location or
omit it entirely and put your cshtml files in the /Views/Partials directory (where
Umbraco expects them to be). The reason for modifying the Razor View Engine rather than
just hardcoding the partial path into the RenderNestedContentPartials() helper is
to support *.Mobile.cshtml switching.
@inherits Umbraco.Web.Mvc.UmbracoViewPage<HomePageModel>
@using AM.Helpers.Extensions
@using AM.Models
@{
Layout = "Master.cshtml";
}
<div class='container components'>
<div class='row'>
<div class='col-md-24'>
@Html.RenderNestedContentPartials(Model.ContentComponents)
</div>
</div>
</div>
using System;
using System.Collections.Generic;
using System.Text;
using System.Web;
using System.Web.Mvc;
using System.Web.Mvc.Html;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
namespace AM.Helpers.Extensions
{
public static class HtmlHelperExtensions
{
#region Nested Content Extensions
/// <summary>
/// MVC Html helper extension for rendering a single Nested Content object (type is IPublishedContent). Use viewDataDictionary to pass in additional data to the view.
/// </summary>
/// <param name="htmlHelper"></param>
/// <param name="contentModel"></param>
/// <param name="viewDataDictionary"></param>
/// <returns></returns>
public static IHtmlString RenderNestedContentPartials(this HtmlHelper htmlHelper, IPublishedContent contentModel, ViewDataDictionary viewDataDictionary = null)
{
return htmlHelper.RenderNCPartials(new IPublishedContent[] { contentModel }, viewDataDictionary);
}
/// <summary>
/// MVC Html helper extension for rendering a list of Nested Content objects (type is IPublishedContent). Use viewDataDictionary to pass in additional data to the view.
/// </summary>
/// <param name="htmlHelper"></param>
/// <param name="contentModels"></param>
/// <param name="viewDataDictionary"></param>
/// <returns></returns>
public static IHtmlString RenderNestedContentPartials(this HtmlHelper htmlHelper, IEnumerable<IPublishedContent> contentModels, ViewDataDictionary viewDataDictionary = null)
{
return htmlHelper.RenderNCPartials(contentModels, viewDataDictionary);
}
private static IHtmlString RenderNCPartials(this HtmlHelper htmlHelper, IEnumerable<IPublishedContent> contentModels, ViewDataDictionary viewDataDictionary)
{
if (contentModels == null || HttpContext.Current == null)
{
return new HtmlString("");
}
StringBuilder result = new StringBuilder();
foreach (IPublishedContent content in contentModels)
{
TypeHelper tHelper = new TypeHelper();
object typedContent = tHelper.GetGeneratedContentModel(content);
if (typedContent != null)
{
result.Append(htmlHelper.Partial(content.DocumentTypeAlias, typedContent, viewDataDictionary).ToString());
}
else
{
result.Append(htmlHelper.Partial(content.DocumentTypeAlias, content, viewDataDictionary).ToString());
}
}
return new HtmlString(result.ToString());
}
#endregion
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using Umbraco.Core;
namespace AM
{
public class NestedContentViewEngine : RazorViewEngine
{
private static string[] NewPartialViewFormats = new[] {
"~/Views/Partials/NestedContent/_{0}.cshtml",
"~/Views/Partials/NestedContent/_{0}.vbhtml"
};
public NestedContentViewEngine()
{
base.PartialViewLocationFormats = base.PartialViewLocationFormats.Union(NewPartialViewFormats).ToArray();
}
}
public class RegisterNestedContentViewEngine : ApplicationEventHandler
{
protected override void ApplicationStarting(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext)
{
ViewEngines.Engines.Add(new NestedContentViewEngine());
base.ApplicationStarting(umbracoApplication, applicationContext);
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Umbraco.Core.Models;
namespace AM.Helpers
{
public sealed class TypeHelper
{
private static volatile TypeHelper _instance;
private static object _lock = new object();
private Dictionary<string, Type> _mappedTypes = new Dictionary<string, Type>();
public TypeHelper() { }
public static TypeHelper Instance
{
get
{
if (_instance == null)
{
lock (_lock)
{
if (_instance == null)
{
_instance = new TypeHelper();
}
}
}
return _instance;
}
}
public object GetGeneratedContentModel(IPublishedContent content)
{
if (content != null)
{
if (this._mappedTypes.Keys.Contains(content.DocumentTypeAlias))
{
return Activator.CreateInstance(this._mappedTypes.Where(t => t.Key == content.DocumentTypeAlias).FirstOrDefault().Value, new object[] { content });
}
else
{
foreach (Type item in GetTypesInNamespace(Assembly.GetExecutingAssembly(), "AM.Models"))
{
FieldInfo field = item.GetField("ModelTypeAlias");
if (field != null)
{
if (content.DocumentTypeAlias == field.GetValue(null).ToString())
{
this._mappedTypes.Add(content.DocumentTypeAlias, item);
return Activator.CreateInstance(item, new object[] { content });
}
}
}
}
}
return null;
}
#region Private
private IEnumerable<Type> GetTypesInNamespace(Assembly assembly, string nameSpace)
{
return assembly.GetTypes().Where(t => String.Equals(t.Namespace, nameSpace, StringComparison.Ordinal));
}
#endregion
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment