Fun with UmbracoVirtualNodeRouteHandler

This morning I answered a question on Our Umbraco regarding the best approach to provide a url allowing deep links for virtual nodes in Umbraco. In my answer I demonstrated how to use UmbracoVirtualNodeRouteHandler as a means of telling Umbraco what node to pass as my RenderModel.

Shannon talks about the UmbracoVirtualNodeRouteHandler with custom routing on his blog

I thought I'd expand on some of the functionality available to the developer by documenting a trick I use with implementations of UmbracoVirtualNodeRouteHandler to provide custom routing.

Use Case

Quite often I find myself reusing a single document type with multiple templates; I'm sure you have also. Most commonly a LoginRegisterPage or similar.

Now I don't know about you but one of the things I miss when using Umbraco is the ability to use Url.Action to generate urls for my content. I like routing in MVC and I think it's important, especially with the type of page as above to be able to generate the correct urls from within your views or controllers.

To understand what I am going to do here you first need to understand how routing works in Umbraco.

If you have the DocumentType LoginRegisterPage with the assigned template LoginPage this will map to a RenderMvcController called LoginRegisterPageController with an ActionResult LoginPage. This behaviour is standardised and predictable so we can utilise it.

  • DocumentType == Controller
  • Template == ActionResult

Example

So let's start by creating some routes.

// Custom routing for the membership pages so we can use
// proper redirects. 
RouteTable.Routes.MapUmbracoRoute(
"LoginPage",
"Login/",
new
{
    controller = "LoginRegisterPage",
    action = "LoginPage"
},
new LoginRegisterPageNodeRouteHandler());

RouteTable.Routes.MapUmbracoRoute(
"RegisterPage",
"Register/",
new
{
    controller = "LoginRegisterPage",
    action = "RegisterPage"
},
new LoginRegisterPageNodeRouteHandler());

Here I have created two routes that map to the relative urls /Login and /Register. They both map to the same controller but since two different templates are used it will map to two different ActionResult methods.

Note: I didn't use the word "virtual" in my handler names since I am mapping to actual nodes.

From here I will create a class that takes advantage of the predictability.

/// <summary>
/// An Umbraco route handler that allows the retrieval of the template alias name from the route action. 
/// </summary>
public abstract class TemplatedUmbracoVirtualNodeRouteHandler : UmbracoVirtualNodeRouteHandler
{
    /// <summary>
    /// Get the template from the current route.
    /// </summary>
    /// <param name="requestContext">
    /// The <see cref="RequestContext"/> containing information about the current HTTP request and route. 
    /// </param>
    /// <returns>
    /// The <see cref="string"/> representing the template alias.
    /// </returns>
    protected virtual string GetTemplateAlias(RequestContext requestContext)
    {
        return requestContext.RouteData.GetRequiredString("action");
    }
}

This class adds an additional method GetTemplateAlias to our handler pulling in the RouteData action variable which we know matches the template name of the node we want in the content tree.

And from here we can implement this our handler.

/// <summary>
/// The generic login/register page node route handler.
/// </summary>
public class LoginRegisterPageNodeRouteHandler : TemplatedUmbracoVirtualNodeRouteHandler
{
    /// <summary>
    /// returns the <see cref="IPublishedContent"/> associated with the route.
    /// </summary>
    /// <param name="requestContext">
    /// The request context.
    /// </param>
    /// <param name="umbracoContext">
    /// The umbraco context.
    /// </param>
    /// <returns>
    /// The <see cref="IPublishedContent"/>.
    /// </returns>
    protected override IPublishedContent FindContent(RequestContext requestContext, UmbracoContext umbracoContext)
    {
        UmbracoHelper helper = new UmbracoHelper(umbracoContext);
        string alias = typeof(LoginRegisterPage).Name;
        string templateAlias = this.GetTemplateAlias(requestContext);

        return helper.TypedContentAtRoot()
            .First()
            .Descendants()
            .First(d => d.DocumentTypeAlias.InvariantEquals(alias)
                     && d.GetTemplateAlias().InvariantEquals(templateAlias));
    }
}
Note: I use Ditto to create strong-typed models for my back office content. It's ace, you should give it a go!

If you look at the code in my handler you will see that I am searching the content for nodes that match both the name and template of the one I want.

It's as simple as that really... I can now use Url.Action("LoginPage", "LoginRegisterPage") and Url.Action("RegisterPage", "LoginRegisterPage") throughout my code to provide the correct urls to my respective pages priding me with functionality I sorely missed.

comments powered by Disqus