Conventional HTML in ASP.NET MVC: Client-side templates

In our last post, we brought everything together to build composable blocks of tags driven off of metadata. We did this to make sure that when a concept exists in our application, it’s only defined once and the rest of our system builds off of this concept. This reduces logic duplication across several layers, ensuring that we don’t have to “remember” to do repetitive tasks, like a required field needing an asterisk and data attributes.

All of this works great because we’ve got all the information at our disposal on the server-side, and we can push the completed product down to the client (browser). But what if we’re building a SPA, using Angular or Knockout or Ember or Backbone? Do we have to revert back to our old ways of duplication? Or can we have the best of both worlds?

There tend to be three general approaches:

  • Just hard code it and accept the duplication
  • Include metadata in your JSON API calls, through hypermedia or other means
  • Build intelligence into templates

I’ve done all three, and each have their benefits and drawbacks. Most teams I talk to go with #1, and some go with #2. Very few teams I meet even think about #3.

What I’d like to do is have the power of my original server-side Razor templates, with the strongly-typed views and intelligent expression-based helpers, but instead of complete HTML templates, have these be Angular views or Ember templates:

image

When we deliver our templates to the client as part of our SPA, we’ll serve up a special version of them, one that’s been parsed by our Razor engine. Normally, the Razor engine performs two tasks:

  • HTML generation
  • Binding model data

Instead, we’ll only generate our template, and the client will then bind the model to our template.

Serving templates, Ember style

Normally, the MVC view engine runs the Razor parser. But we’re not going that path, we’re going to parse the templates ourselves. The result of parsing will be placed inside our script tags. This part is a little long, so I’ll just link to the entire set of code.

HtmlHelperExtensions

A couple key points here. First, the part that runs the template through the view engine to render an HTML string:

builder.AppendLine("<script type=\"text/x-handlebars\" data-template-name=\"" + fullTemplateName + "\">");
var controllerContext = new ControllerContext(helper.ViewContext.HttpContext, new RouteData(), helper.ViewContext.Controller);
controllerContext.RouteData.Values["controller"] = string.IsNullOrEmpty(relativeDirName) ? "Home" : Path.GetDirectoryName(relativeDirName);
var result = ViewEngine.FindView(controllerContext, subtemplateName, null, false);
var stringWriter = new StringWriter(builder);
var viewContext = new ViewContext(controllerContext, result.View, new ViewDataDictionary(), new TempDataDictionary(), stringWriter);
result.View.Render(viewContext, stringWriter);

builder.AppendLine("</script>");

We render the view through our normal Razor view engine, but surround the result in a script tag signifying this is a Handlebars template. We’ll place the results in cache of course, as there’s no need to perform this step more than once. In our context objects we build up, we simply leave our ViewData blank, so that there isn’t any data bound to input elements.

We also make sure our templates are named correctly, using the folder structure to match Ember’s conventions. In our one actual MVC action, we’ll include the templates in the first request:

@Scripts.Render("~/bundles/ember")
@Scripts.Render("~/bundles/app")

@Html.Action("Enumerations", "Home")
@Html.RenderEmber()

Now that our templates are parsed and named appropriately, we can focus on building our view templates.

Conventional Handlebars

At this point, we want to use our HTML conventions to build out the elements needed for our Ember templates. Unfortunately, we won’t be able to use our previous tools to do so, as Ember uses Handlebars as its templating language. If we were using Angular, it might be a bit easier to build out our directives, but not by much. Client-side binding using templates or directives requires special syntax for binding to scope/model/controller data.

We don’t have our convention model, or even our HtmlTag library to use. Instead, we’ll have to use the old-fashioned way – building up a string by hand, evaluating rules as we go. I could have built a library for creating Ember view helpers, but it didn’t seem to be worth it in my case.

Eventually, I want to get to this:

@Html.FormBlock(m => m.FirstName)

But have this render this:

<div class="form-group">
    <label class="required control-label col-md-2"
       {{bind-attr for="view.firstName.elementId"}}>
       First Name
    </label>
    <div class="col-md-10">
        {{view TextField class="required form-control" 
               data-key="firstName" 
               valueBinding="model.firstName" 
               viewName="firstName" 
               placeholder="First"
        }}
    </div>
</div>

First, let’s start with our basic input and just cover the very simple case of a text field.

public static MvcHtmlString Input<TModel, TValue>(this HtmlHelper<TModel> helper,
    Expression<Func<TModel, TValue>> expression,
    IDictionary<string, object> htmlAttributes)
{
    var text = ExpressionHelper.GetExpressionText(expression).ToCamelCase();
    var modelMetadata = ModelMetadata.FromLambdaExpression(expression, helper.ViewData);
    var unobtrusiveAttributes = GetUnobtrusiveValidationAttributes(helper, expression);

    var builder = new StringBuilder("{{view");

    builder.Append(" TextField");

    if (unobtrusiveAttributes.ContainsKey("data-val-required"))
    {
        builder.Append(" class=\"required\"");
    }

    builder.AppendFormat(" data-key=\"{0}\"", text);

    builder.AppendFormat(" valueBinding=\"model.{0}\"", text);
    builder.AppendFormat(" viewName=\"{0}\"", text);

    if (!string.IsNullOrEmpty(modelMetadata.NullDisplayText))
        builder.AppendFormat(" placeholder=\"{0}\"", modelMetadata.NullDisplayText);

    if (htmlAttributes != null)
    {
        foreach (var item in htmlAttributes)
        {
            builder.AppendFormat(" {0}=\"{1}\"", item.Key, item.Value);
        }
    }

    builder.Append("}}");

    return new MvcHtmlString(builder.ToString());
}

We grab the expression text and model metadata, and begin building up our Handlebars snippet. We apply our conventions manually for each required attribute, including any additional attributes we need based on the MVC-style mode of passing in extra key/value pairs as a dictionary.

Once we have this in place, we can layer on our label helper:

public static MvcHtmlString Label<TModel, TValue>(
    this HtmlHelper<TModel> helper, 
    Expression<Func<TModel, TValue>> expression)
{
    var text = ExpressionHelper.GetExpressionText(expression);
    var metadata = ModelMetadata.FromLambdaExpression(expression, helper.ViewData);
    var unobtrusiveAttributes = GetUnobtrusiveValidationAttributes(helper, expression);

    var builder = new StringBuilder("<label ");
    if (unobtrusiveAttributes.ContainsKey("data-val-required"))
    {
        builder.Append(" class=\"required\"");
    }
    builder.AppendFormat(" {{{{bind-attr for=\"view.{0}.elementId\"}}}}", text.ToCamelCase());
    builder.Append(">");

    string labelText = metadata.DisplayName ?? (metadata.PropertyName == null
        ? text.Split(new[] {'.'}).Last()
        : Regex.Replace(metadata.PropertyName, "(\\B[A-Z])", " $1"));

    builder.Append(labelText);
    builder.Append("</label>");

    return new MvcHtmlString(builder.ToString());
}

It’s very similar to the code in the MVC label helper, with the slight tweak of defaulting label names to the property names with spaces between words. Finally, our input block combines these two together:

public static MvcHtmlString FormBlock<TModel, TValue>(
    this HtmlHelper<TModel> helper,
    Expression<Func<TModel, TValue>> expression)
{
    var builder = new StringBuilder("<div class='form-group'>");
    builder.Append(helper.Label(expression));
    builder.Append(helper.Input(expression));
    builder.Append("</div>");
    return new MvcHtmlString(builder.ToString());
}

Now, our views start to become a bit more sane, and it takes a keen eye to see that it’s actually a Handlebars template. We still get strongly-typed helpers, metadata-driven elements, and synergy between our client-side code and our server-side models:

@model MvcApplication.Models.AccountCreateModel
{{title 'Create account'}}

<form {{action 'create' on='submit'}}>
    <fieldset>
        <legend>Account Information</legend>
        @Html.FormBlock(m => m.Username)
        @Html.FormBlock(m => m.Password)
        @Html.FormBlock(m => m.ConfirmPassword)
    </fieldset>

We’ve now come full-circle, leverage our techniques that let us be ultra-productive building out pages on the server side, but not losing that productivity on the client-side. A concept such as “required field” lives in exactly one spot, and the rest of our system reads and reacts to that information.

And that, I think, is pretty cool.

Related Articles:

Post Footer automatically generated by Add Post Footer Plugin for wordpress.

About Jimmy Bogard

I'm a technical architect with Headspring in Austin, TX. I focus on DDD, distributed systems, and any other acronym-centric design/architecture/methodology. I created AutoMapper and am a co-author of the ASP.NET MVC in Action books.
This entry was posted in ASP.NET MVC. Bookmark the permalink. Follow any comments here with the RSS feed for this post.
  • Pingback: Conventional HTML in ASP.NET MVC: Data-bound elements | Jimmy Bogard's Blog

  • jimmyhogard

    Garbage. Does not scale.

    • RyanVice

      Is this really your own personal troll account Jimmy? You’ve reached new heights of awesome with this!

      • jbogard

        I don’t know what this is, I was just about to delete it lol

  • Betty

    Would this still work with optimization tools that combine everything (including views) into one js file? I guess it would probably depend on if there’s any conditional logic in the views.

    • jbogard

      Yeah, it should. We just embedded the templates in the page but ultimately the approach just builds a single string.

    • http://www.make-awesome.com David Boike

      It shouldn’t be hard to adapt this to use ASP.NET bundling, and then jam them all into one JS file that (in Angular-speak) inserts them all into the $templateCache.

  • Pingback: The Morning Brew - Chris Alcock » The Morning Brew #1674

  • Vlad Kopachinsky

    I think solution very complex because use magic string. We are have our template builder for ( handlebars, mustaches and etc ) with generic syntax like @each.For(r=>r.Name) . Please look at article ( http://blog.incframework.com/en/client-template/ )

    • jbogard

      Wow, neat stuff! My example is expression-based as well, I think we came up with the same approach, different implementations, no? I build out a helper for a model member with an expression.

      • Vlad Kopachinsky

        Maybe. Let me try compare your solution and our:
        Please look at first attach file it show how write template for form.

        You solution narrow but It no so bad. Our template builder usage for generate tables, list or something html content but can worked as form builder or everything. Please look at sample template ( load data by ajax and insert with template ) on github ( https://github.com/IncodingSoftware/Do_Action_Insert )

        P.S. Please maybe you can take 30 – 50 minutes and look our template ( also it one part of incoding framework ) . I would be very grateful. Thanks.

        • Guest

          missing attach file

  • Ciel

    I see someone saying this doesn’t scale, but I really beg to differ. I’ve yet to discover a framework, technique, pattern, or infrastructure that scales indefinitely and this is no different. It accomplishes what the author set out to do in the initial post, it is fluid and elegant, and while there is always room for improvement, it is a hell of a lot better than anyone I’ve seen critiquing in the posts up until now offering.

    Programmers who just ridicule code without offering constructive ways to solve the problem are no better than trolls, I think. We’re supposed to be building solutions that make things better for people. The only comment I’ve seen that was remotely constructive was the one on THIS post by Vlad Kapachinsky.

    TLDR; If you think it is bad, then do better. Otherwise, shut up.

  • Pingback: Conventional HTML in ASP.NET MVC: Client-side t...

  • Sam Sippe

    Hi Jimmy, Off topic but I wanted to read some of your old category posts and the category view isn’t working on the site e.g. http://lostechies.com/jimmybogard/category/dependency-injection/

    Cheers

  • Daniel Mackay

    Hi Jimmy. First of all, great series of posts! Do you have a complete solution with all these techniques combined?