Published Content with overridden Properties

Montag, 20.08.2018

Mirko Matytschak

This is an article for Umbraco developers. It shows, how you can override properties of an IPublishedContent for example in a route hijacking scenario: You got your own MVC controller, you got your document and a view in the controller and want to set some properties of the document before you render it using the view. Not possible? Everything is possible!

The other day I had to solve an issue with metadata of blog posts. Our LAYKIT platform provides a simple blog implementation, which is sufficient for most cases where our customers want to publish posts on a regular base. The implementation is based on the Umbraco Grid Control. Therefore it is possible to render blog articles with the same code as we use for normal text pages, although a much simpler data model is used to enter blog posts in the backoffice.

This works because we use a template page for the blog posts, which uses a normal text page layout. The template page is an ordinary page at a certain place in the document tree, which will never be rendered directly. The template page contains grid controls for the actual post and other blog related items like the list of the most current blog posts.

Now, when someone navigates to a blogpost in the browser, we don't render the blog post itself, but the template page. This all happens in a controller, which manages the route hijacking for our BlogPost document type. The essential lines of code are the following:

   HttpContext.Current.Items.Add( "CurrentPage", model.Content.Id );  // We need the blog post later
   var content = LaykitTemplates.BlogArticleTemplate;
   return View( "TextPage", content );

As you can see, we save the id of the blog post (model.Content.Id) as part of the HttpContext and render the BlogArticleTemplate document. The variable content has the type IPublishedContent, an interface, which represents a published document in Umbraco. The template page contains a BlogPost grid control which gets the CurrentPage item from the HttpContext and inserts the blog post text as part of the template page.

The issue with this solution is, that the view renders the metadata (title, description, keywords) of the template page instead of the metadata of the blog post. There are possible solutions for this issue using partial views, but these are cumbersome and error prone. Wouldn't it be nice, if we could add some transient properties to the template page, which could be used during rendering of the page?

Unfortunately this is not possible out-of-the-box in Umbraco. But we can still do something very simple to get this task done. You can see the idea in the following code of a controller which hijacks the route for documents with the document type BlogPost:

public class BlogPostController : RenderMvcController
{
    public override System.Web.Mvc.ActionResult Index( RenderModel model )
    {
        var blogPost = model.Content;
        System.Web.HttpContext.Current.Items.Add( "CurrentPage", blogPost.Id );
        var content = new OverriddenPublishedContent( LaykitTemplates.BlogArticleTemplate );
        content.OverriddenProperties = new Dictionary<string, object>()
        {
            { "tags", blogPost.GetProperty( "tags" )?.Value },
{ "title", blogPost.GetProperty( "title" )?.Value },
            { "description",
Umbraco.StripHtml( blogPost.GetProperty( "abstract" )?.Value?.ToString() )},
            { "changeFrequency", blogPost.GetProperty( "changeFrequency" )?.Value },
            { "sitePriority", blogPost.GetProperty( "sitePriority" )?.Value },
        };
        return View( "TextPage", content );
    }
}

Instead of using the BlogArticleTemplate directly, we wrap an instance of the OverriddenPublishedContent class around the template page. The OverriddenPublishedContent class implements IPublishedContent on base of an existing (inner) instance of IPublishedContent. It offers a property OverriddenProperties, which allows us to define arbitrary new properties. To keep everything simple we pass the additional properties as a Dictionary. As you can see in the code, we are able to transform existing properties of the blog post and assign it with the same or another name to the dictionary which is then passed over to the template document.

The class OverriddenPublishedContent is very simple. It delegates most IPublishedContent member calls to the inner object, which was passed as a parameter to the constructor. Here are the constructor and the OverriddenProperties property:

public OverriddenPublishedContent( IPublishedContent inner )
{
    this.inner = inner;
}
public Dictionary<string, object> OverriddenProperties
{
    set
    {
        foreach (var item in value)
        {
            this.properties.Add( item.Key, new OverriddenProperty( item.Key, item.Value ) );
        }
    }
}

public int Id => inner.Id; // delegate all calls to the inner object

We transform every entry in the dictionary to an IPublishedProperty instance. We use an additional class OverriddenProperty for this purpose:

public class OverriddenProperty : IPublishedProperty
{
    private readonly string alias;
    private readonly object value;
    public OverriddenProperty( string alias, object value )
    {
        this.alias = alias;
        this.value = value;
    }
    public string PropertyTypeAlias => alias;
    public bool HasValue => true;
    public object DataValue => value;
    public object Value => value;
public object XPathValue => null;
}

This is straightforward. The view actually uses only the member Value of the OverriddenProperty class.

The last piece of code to complete the solution is the implementation of GetValue and GetProperties of our OverriddenPublishedContent class (note that there are two method signatures of GetProperty):

public ICollection<IPublishedProperty> Properties
{
    get
    {
        var result = new List<IPublishedProperty>( this.properties.Values );
        result.AddRange( inner.Properties );
        return result;
    }
}
public IPublishedProperty GetProperty( string alias )
{
    if (this.properties.ContainsKey( alias ))
        return this.properties[alias];
    return this.inner.GetProperty( alias );
}
public IPublishedProperty GetProperty( string alias, bool recurse )
{
    if (this.properties.ContainsKey( alias ))
        return this.properties[alias];
    return this.inner.GetProperty( alias, recurse );
}

These are the three members of IPublishedContent, that we need to change and that's it: we are now able to assign properties to our own IPublishedContent implementation. The views render this implementation without any complaints.

I hope, this post helps somebody to solve similar issues.

Update: You'll never walk alone… ;-) It seems as if the Umbraco developers had similar issues to solve. They wrote a class PublishedContentWrapped, which does the job of delegating all calls to the inner (wrapped) content. You can derive from this class and override the three members Properties, GetProperty(string), and GetProperty(string,bool) as shown above. And -- while you're at it -- you can use the class PublishedContentWithKeyWrapped, which enables the views to have access to the Key property of the content you're about to wrap. You might want to use the following constructor:

public OverriddenPublishedContent( IPublishedContent inner ) : 
    base((IPublishedContentWithKey)inner)
    {
    }

It's save to downcast to IPublishedContentWithKey, because in Umbraco every document is an implementation of this interface.

Einen Kommentar verfassen

Anleitung zur Textformatierung

Zum Formatieren des Textes verwenden Sie [b][/b] und [i][/i]. Verwenden Sie [url=http://ihre-site]Text[/url] für Links.

* Pflichtfelder