Monday, October 31, 2016

Guide to using Strong Typed PublishedContnetModels in Umbraco 7

The recent Umbraco 7.4 release there is an included Model Builder tool.  This may not seem like a big deal but it can be very helpful to developers and I recommend embracing it.  In essence what it does it let you generate strong typed models in .Net based off your Document Types and Media Types and their hierarchy.  This can be done a few ways including at runtime in memory or with pre-compile DLL’s.  Then in Views, Macro’s and Controllers these strong typed models can be used instead of normal code to reduce potential coding errors and typos, and to provide IntelliSense in Visual Studio if you do editing there.  These changes for the most part are non-breaking and the old style can still be used.

In short if will replace the first example template with the second.
@inherits Umbraco.Web.Mvc.UmbracoTemplatePage
@Model.Content.GetPropertyValue(“myProperty”)
@(Umbraco.Media(CurrentPage.myImage).umbracoFile)
@Model.Content.GetPropertyValue(“myProperty”, true, “Missing”)
@Umbraco.Field("myProperty ", recursive: true, altFieldAlias: "defaultProperty")

@inherits Umbraco.Web.Mvc.UmbracoTemplatePage<DocumentType>
@Model.Content.MyProperty
@(((Image)Model.Content).MyImage.UmbracoFile
@Model.Content.GetPropertyValue(s => s.MyProperty, true, “Missing”)
@Model.Content.GetPropertyValue(s => s.MyProperty, true, Model.Content.DefaultProperty)

This article describes the following approach:  Generating C# models in the live Umbraco Instance and then pulling them down into the generated models down into the source Visual Studio Project and pre-compiles them there in the main application DLL.  This has some major benefits over other approaches including:
  • All code is pre-compiled and no generation or compilation is happening when the website is actually running or starting up.
  • Models generation isn’t dependent on IIS Express and you don’t have to do desin work there or start up slow projects to manually create models.
  • Models are generated the same place you edit the document types and generated automatically.
  • Models are partial class’s that can be extended and can implement custom interfaces as needed.
  • There is no need for any special visual studio plugins or deployment rules to make this work.  All parts described will work on any Visual Studio Editions.

To clarify we will have the following:
  • A normal production/development server running IIS/Umbraco site.
    • Document and media Types would be edited here, basically whatever system you currently do Umbraco BackOffice work in now
    • New model sources are automatically generated as changes are made, but the runtime DLL’s won’t be touched.
  • Developer box/Visual Studio containing the site project/Umbraco CMS from NuGet packages.
    • Project is configured to do deployments using Web Deploy.
    • Models sources are synced down from the deployed instance back to the Visual Studio project.
    • Generated Models may be extended with local code and are compiled into the main project DLL.
    • View files can be edited here to take advantage of design-time debugging and full IntelliSense or in Umbraco BackOffice as desired.

Setup

  • So to start with we have a basic MVC Website Solution in Visual Studio pulling in the Umbraco NuGet project or existing Umbraco project.
  • We have configured this site to use Web Deploy and have deployed a working site to a server that is up and running.
  • We have created some Document Types and Media Types for our site in Umbraco’s BackOffice.

Enable Model Generator

First we will need to edit the web.config file and add a few settings to the AppSettings section.
    <add key="Umbraco.ModelsBuilder.Enable" value="true" />
    <add key="Umbraco.ModelsBuilder.ModelsMode" value="LiveAppData" />
    <add key="Umbraco.ModelsBuilder.ModelsDirectory" value="~/PublishedContentModels/Generated" />
    <add key="Umbraco.ModelsBuilder.ModelsNamespace" value="ProjectNamespace.PublishedContentModels" />
  • The first option is enabling the model builder, the second is telling the site to only generated model source code and to do so automatically when document and media types are updated.
  • Next we are specifying a directory to save these source files into. We want this to be part of the project directory so we can easily retrieve updates.
  • Now update the ProjectNamespace to use for all the models.  You will want to use the same namespace that is specified as the Default namespace on the website project in Visual Studio as ProjectNamespace portion so that you don’t have to manually change it every time you add or customize a model.

Next you will want to edit the ~/Views/Web.config file add the same namespace above to the namespaces there.
        <add namespace=" ProjectNamespace.PublishedContentModels" />

Add the Umbraco Core Property Value Package

This package is needed to that generated models use IPublishedContent types for all properties that use internal pickers instead of returning string or integer return types.  This enables you to use the first line of code instead of the second.  It also makes checking for null objects much easier.
@Model.Content.LinkedPage.Name
@Umbraco.Content((int)Model.Content.LinkedPage).Name.
@if(Model.Content.LinkedPage == null) {}

Note that if you have an existing site this package will break any templates and macros that are expecting integer or string returns from property pickers.  You will need to go through the project and update such places to expect IPublishedContent or IEnumberable<IPublishedContent> as applicable.  This will not harm performance though as all nodes are cached.

Apply patch for Strong Typed GetPropertyValue methods

You will want to compile and replace your Umbraco.ModelsBuilder with the patched version available here to enable some additional functions.  The following are not yet supported in the Umbraco release but work with this patch.
@Model.Content.GetPropertyValue(s => s.BannerImage, true) // Search recursively up the tree.
@Model.Content.GetPropertyValue(s => s.BannerImage, "someOtherImage.gif")  // Specify a default value.
@Model.Content.GetProeprtyValue(s => s.BannerImage, Model.Content.AlternateImage)

Syncing changes back to Visual Studio

There is a bit more initial setup to get his working.
  • Once these changes are applied and deployed go into the Umbraco site and go to the Developer Dashboard.
  • Click on Model Builder and click Generate Models.
    • You should only need to this once as future changes will automatically regenerate the sources but you can come here to manually do so if you have any issues.
  • In your visual studio website project created a new folder at the root level named PublishedContentModels.
  • Right Click on this new folder and select Replace PublishedContentModels From Server to download all your generated models.
    • You may need to toggle the Show All Files option in the project view and/or refresh the project view to see the new files.
  • The files will not be included in your project by default so Right Click on the Generated folder and select Include In Project to include all models.
  • Republish the website or test locally to load the new models.

Syncing future changes to the Models

In the future when you make Document Type or Media Type changes including adding or removing types or adding, removing, or changing properties you will need to do the following the synchronize the changes.
  • Right Click on the Generated folder and select Replace Generated From Server.
  • Include or Exclude any files for models that may have been added or removed.
  • Publish the website or manually copy over the main DLL to load the new models.

Some Caveats about the Models

In many cases no valid item may be specified or the linked item could have been removed.  As such you will need to check for NULL values or provide a default value.
@if(Model.Content.Resource != null)
var linkedPage = Model.Content.GetPropertyValue(s => s.LinkedPage, Model.Content);

Also you might expect the models to return strong typed Model for content pickers but they do not as the would create recursion issues.  Instead all documents and media are returned as IPublishedContent but that can still be cast to Strong Typed models as needed.
var bannerImage = (Image)Model.Content.BannerImage;
var resourceItems = (IEnumberable<ResourceItem>)Mode.Content.ResourceItems;

Or you can add strong typed accessor’s for those properties.
public partial class BasePage {
        public BasePage TypedUmbracoInternalRedirectId {  get { return (BasePage)UmbracoInternalRedirectId;  } }
        public IEnumberable<ResourceItem>TypedResourceItems {  get { return (IEnumberable<ResourceItem>)ResourceItems;  } }
}


Using the new Published Content Models.

There are lots of things that you can with the new models.  The first thing to deal with is changing your Templates and Partial Views to use the new PublishedContentModels.
./uBlogsyPost.cshtml
@inherits UmbracoTemplatePage
@inherits UmbracoTemplatePage<UBlogsyPost>

./ShopSite.chstml
@inherits Umbraco.Web.Mvc.UmbracoTemplatePage
@inherits Umbraco.Web.Mvc.UmbracoTemplatePage<ShopSite>

Using Properties

Properties can be replaced with calls like these:
@Model.Content.MyProperty
@(((Image)Model.Content).MyImage.UmbracoFile
@Model.Content.GetPropertyValue(s => s.MyProperty, true, “Missing”)
@Model.Content.GetPropertyValue(s => s.MyProperty, true, Model.Content.DefaultProperty)
var locations = Model.Contnet.Children<Location>();
var landingPage = Model.Content.AncestorOrSelf<LandingPage>();

Using Inheritance

The models will have the same inheritance as the site, so if you have a document structure like this:
  • BasePage
  • BasePage/TextPage
  • BasePage/ImagePage

You could have the following templates:
./BasePage.chstml [Master Template]
@inherits Umbraco.Web.Mvc.UmbracoTemplatePage<BasePage>
./TextPage.chstml
@inherits Umbraco.Web.Mvc.UmbracoTemplatePage<TextPage>
    Layout = " BasePage.cshtml";

In this way the master template has typed access to all its properties and the children have their applicable properties as well.

Using Compositions or Interfaces

You can also create compositions or create your own Interfaces across multiple models that may share properties but don’t inherit from each other.  Take this example for instance:
  • MainSite
  • MainSite/BodyPage
  • MainSite/CategoryPage
  • Blog
  • Blog/Landing
  • Blog/Post

In this case the Main Site and blog may have similar properties but they don’t inherit from each other.  In this case you could create and interface and then add these two partials class's extend the existing models with consistent accessors.

Then you can have a template use the Interface instead.
./BasePage.chstml
@inherits Umbraco.Web.Mvc.UmbracoTemplatePage<IBasePage>
    Layout = "BasePage.cshtml";
@Model.Content.PageTitle

Note the ImplementPropertyType decorations are necessary for Model.Content.GetPropertyValue(s => s.type) calls to work on the interface.

You can accomplish the same thing by creating Compositions in Umbraco and the Composition will be auto-generated as interfaces though I haven’t tested this part out myself.

Additional custom properties and code

Additional code can be added for any of these models in partial class’s in the PublishedContentModels folder to add additional logic and move complex programming out of the template files and into the models.

Here is an example of an Image extended model that supports scaling images.

This can be called like so:
@{
  var image = Model.Content.BannerImage.Scale(500, 250);
}
<img src=”@image.UmbracoFile” width=”@iamge.UmbracoWidth” height=”@iamge.UmbracoHeight”>

Conditionals Based off Current Document Type

There may be lots of places in Master Templates and Partial Views where you are running conditional code based on the current document type of the page.  These should be replaced with strong typed check’s instead.  The first line is checking if a node is a specific type directly whereas the second line would return true if the node is that type of inherits from it in the hierarchy.
@if(Model.Content.DocumentTypeAlias == BodyPage.ModelTypeAlias)
@if(Model.Contnet is BodyPage)

Or a similar check to see if an ancestor of a given type exists:
@if(Model.Content.AncestorOrSelf<uBlogsyLanding>() != null) {}

You can also cast a model to a more specific type.  For example:
@inherits UmbracoTemplatePage<BasePage>
@if(Model.Content is BodyPage) {
  var bodyPage = (BodyPage)Model.Content;
  @bodyPage.Contents
}

Strong Type Tree Traversal

There are many new variations on the common node traversal functions that let you specify a model type.  You may need to add using Umbraco.Web; in your code to access them. A few examples:
Model.Content.Parent<CategoryPage>();
Model.Content.Descendants<BodyPage>();

Strong Typed Recursive and Default Text

The following will only work for now if you use the custom patch listed abode and then regenerated your models.

Load a property reclusively and providing a default value.  These will work exactly like the Umbraco equivalents.
@Model.Content.GetPropertyValue(s => s.BannerImage, true)
@Model.Content.GetProeprtyValue(s => s.BannerImage, Model.Content.AlternateImage)

Issues in Macro Partial Views

You may discover that you can’t specify types for macro’s in the same way as you might expect.  This is a limitation due to limitations with the PartialViewMacroPage class and its cacheing. You may not  have the problem but for me the following example won’t work in a Macro Partial View currently.
@inherits Umbraco.Web.Macros.PartialViewMacroPage<BodyPage>
@Model.Content.PageTitle

There is a bit of a workaround that doesn’t add any processing overhead as the items are still cached properly, but it does make the code in Macro view files slightly different.
@inherits Umbraco.Web.Macros.PartialViewMacroPage
@{
  var Content = Umbraco.TypedContent(Model.Content.Id);
  @Content.PageTitle
}

2 comments:

Unknown said...

Confession: I haven't read your article from tip to toe.

Normally when I use ModelBuilder, I pass a ViewModel to the template, this is inherited from the strongly typed model that ModelBuilder creates. This allow really simple razor code like (And this is using your example at top of your article)


@inherits UmbracoViewPage
@Model.MyProperty
@Model.MyImage.UmbracoFile


I can see that it doesn't allow dynamic testing for properties, but then, in a Strongly Typed world, you don't do things like


@Model.Content.GetPropertyValue(s => s.MyProperty, true, “Missing”)


anyway, so I feel this is mute code. If you required this functionality, it would be moved to the controller, and passed as a single value in the ViewModel.


So if your happy with the criticism, you seem to be using ModelBuilder as a half way house to using Strongly Typed Modelling properly.

Though I have to agree, you have taken a very interesting, if novel approach and I can see benefits of this if, for example, you were converting an existing website with dynamics and didn't want to go the whole hog.

References
https://www.zpqrtbnk.net/posts/purelive-models-introduction (By Stephan Gay, he that wrote ModelsBuilder)
http://24days.in/umbraco/2015/strongly-typed-vs-dynamic-content-access/
http://camaya.co/posts/2016/10/01/umbraco-models-builder-basics/


Cheers

PyneJ said...

Your right that a additional method can be added to the partial model, or a view model used to do thing like recursive or default values.

Worth nothing though the @Model.Content.GetPropertyValue(s => s.MyProperty, true, “Missing”) line mentioned is not using dynamics but rather is also strong typed.