Skip to content

Understand And Extend

Warren Buckley edited this page Feb 24, 2016 · 2 revisions

Models are generated as partial classes. In its most basic form (some code being ommitted for simplicity's sake), a model for content type MyDocument ends up in file MyDocument.generated.cs and looks like:

[PublishedContentModel("myDocument")]
public partial class MyDocument : PublishedContentModel
{
  public new const string ModelTypeAlias = "myDocument";
  public new const PublishedItemType ModelItemType = PublishedItemType.Content;
  
  public MyDocument(IPublishedContent content)
    : base(content)
  { }

  public static PublishedContentType GetModelContentType()
  {
    return PublishedContentType.Get(ModelItemType, ModelTypeAlias);
  }
  
  public static PublishedPropertyType GetModelPropertyType<TValue>(Expression<Func<Doc1, TValue>> selector)
  {
    return PublishedContentModelUtility.GetModelPropertyType(GetModelContentType(), selector);
  }

  [ImplementPropertyType("myProperty")]
  public int MyProperty { get { return this.GetPropertyValue<int>("myProperty"); } }
}

What is really important is the MyProperty property. The rest is (a) a constructor and (b) some static helpers to get the PublishedContentType and the PublishedPropertyType objects:

var contentType = MyDocument.GetModelContentType(); // is a PublishedContentType
var propertyType = MyDocument.GetModelPropertyType(x => x.MyProperty); // is a PublishedPropertyType

Composition and Inheritance

Content type composition consists in having content types "inherit" properties from other content types. Contrary to C#, where a class can only inherit from one other class, Umbraco content types can be composed of several other content types.

The MyDocument content type could be composed of the MetaInfo content type (and thus inherit properties Author and Keywords) and of the PageInfo content type (and thus inherit properties Title and MainImage).

Each content type that is involved in a composition is generated both as a class and as an interface, and so the MetaInfo content type would be generated as (some code removed for simplicity's sake):

// the composition interface
public partial interface IMetaInfo : IPublishedContent
{
  public string Author { get; }
  public IEnumerable<string> Keywords { get; }
}

// the composition class
public partial class MetaInfo : PublishedContentModel
{
  // the "static mixin getter" for the property
  public static string GetAuthor(IMetaInfo that)
  {
    return that.GetPropertyValue<string>("author");
  }
  
  public string Author { get { return MetaInfo.GetAuthor(this); } }
}

And the MyDocument model would be generated as (again, some code removed):

public partial class MyDocument : PublishedContentModel, IMetaInfo
{
  // get the property value from the "static mixin getter"
  public string Author { get { return MetaInfo.GetAuthor(this); } }
}

A content type parent is a tree-related concept: in the Umbraco backend, a content type appears underneath its parent, if any. By convention, a content type is always composed of its parent and therefore inherits its properties. However, the parent content type is treated differently, and the child content type directly inherits (as in, C# inheritance) from the parent class.

Therefore, assuming that the MySubDocument content type is a direct child of MyDocument, it would be generated as:

// note: inherits from MyDocument
public partial class MySubDocument : MyDocument
{
  ...
}

Extending

Because a model is generated as a partial class, it is possible to extend it, eg adding a property, by dropping the following code in a MyDocument.cs file:

public partial class MyDocument
{
  public int TenTimesMyProperty { get { return MyProperty * 10; } }
}

In modes where models are built within the site (Dll, PureLive) any *.cs file in the ~/App_Data/Models directory that is not a *.generated.cs file is preserved and compiled alongside the models. If models are built outside the site, eg in Visual Studio, just remember to include the files in the compilation.

If the custom partial class provides a constructor that has the same signature as the generated one, it will be detected and no constructor will be generated (as that would be redundant and would not compile).

If the custom partial class inherits from a base class, it will be detected and the generated model will not inherit from anything (as that would be redundant and would not compile). The base class must inherit (directly or indirectly) from PublishedContentModel in order for the model to be valid, though.

If the custom partial class implements a generated property, it will not be detected and will cause a compilation error. The Models Builder needs to be explicitly notified about the situation: see Control Models Generation.

If the custom partial class implements a static mixin getter (see above), it will be detected and the generated model will not implement the getter (as that would be redundant and would not compile).

Best Practices

Extending models should be used to add stateless, local features to models, and not to transform content models into view models or manage trees of content.

Good

A customer had "posts" that had two "release date" properties. One was a true date picker property and was used to specify an actual date and to order posts. The other was a string and was used to specify dates such as "Summer 2015" or "Q1 2016". Alongside the title of the post, the customer wanted to display the text-date, if any, else the actual-date, if any, else the Umbraco update date. Of course, each view can contain code to deal with the situation, but it is much more efficient to extend the Post model:

public partial class Post
{
  public string DisplayDate
  {
    get
    {
      if (!string.IsNullOrEmpty(this.TextDate)) return this.TextDate;
      if (this.ActualDate != DateTime.MinDate) return this.ActualDate.ToString();
      return this.UpdateDate;
    }
  }
}

And to simplify the view as:

<div class="title">
  <div class="title-text">@Model.Title</div>
  <div class="title-date">@Model.DisplayDate</div>
</div>

Bad

Because, by default, the content object is passed to views, one can be tempted to add view-related properties to the model. Some properties that do not belong to a content model would be (these are actual ideas that have been discussed by Models Builder users):

  • A HomePage property that would walk up the tree and return the "home page" content item
  • A Menu property that would list the content items to display in a top menu
  • etc.

Generally speaking, anything that is tied to the current request, or that depends on more than just the modeled content, is a bad idea. There are much cleaner solutions, such as using true view model classes that would be populated by a true controller and look like:

public class MyDocumentViewModel
{
  public MyDocument Content; // the content model
  public HomePage HomePage; // the home page content model
  public IEnumerable<MenuItem> Menu; // the menu content models
}

One can also extend Umbraco's views to provide a special view helper that would give access to important elements of the website, so that views could contain code such as:

<a href="@MySite.HomePage.Url">@MySite.HomePage.Title</a>

Ugly

The scope and life-cycle of a model is not specified. In other words, you don't know whether the model exists only for you and for the context of the current request, or if it is cached by Umbraco and shared by all requests. Even though as of version 7.4 Umbraco creates distinct models for each request, this will not always be true.

As a consequence, the following code has a major issue: the MyDocument model "caches" an instance of the HomePageDocument model that will never be updated if the home page is re-published.

public partial class MyDocument
{
  public MyDocument(IPublishedContent content)
    : base(content)
  {
    HomePage = content.AncestorOrSelf(1) as HomePageDocument;
  }
  
  public HomePageDocument HomePage { get; private set; }
}

As a rule of thumb, models should never reference and cache other models.