Tuesday, March 10, 2009

Block components in ASP.NET MVC

A couple of days ago I was browsing stackoverflow for some questions to answer when I hit this one:

I have repetitious markup for elements, like:

<div class="box">
    <div class="top"></div>

    <div class="content">
      content goes here
    </div>
    <div class="bottom"></div>
</div>

Top and bottom are styled with CSS to include images for borders.

I could use JQuery to inject tags, but is there a better way to template controls in ASP.NET MVC?

MonoRail users might recognize this as a block component, that is, a ViewComponent (ViewUserControl in ASP.NET MVC / WebFormViewEngine terms) that accepts a block (or several blocks, also called sections) of HTML to be inserted somewhere within the ViewComponent.

The author of the question goes on to answer himself with two HtmlHelper extensions methods:

  public static void BeginBox(this HtmlHelper htmlHelper)
  {
      StringBuilder box = new StringBuilder();

      box.AppendLine("<div class=\"box\">");
      box.AppendLine("    <div class=\"top\"></div>");
      box.AppendLine("        <div class=\"content\">");

      htmlHelper.ViewContext.HttpContext.Response.Write(box.ToString());
  }

  public static void EndBox(this HtmlHelper htmlHelper)
  {
      StringBuilder box = new StringBuilder();

      box.AppendLine("        </div>");
      box.AppendLine("    <div class=\"bottom\"></div>");
      box.AppendLine("</div>");

      htmlHelper.ViewContext.HttpContext.Response.Write(box.ToString());
  }

This works just fine, but I strongly dislike having markup in code. Plus, we could forget to call Html.EndBox() which would break everything. Can we somehow move this to a ViewUserControl? By using an Action as the Model of the ViewUserControl, we can:

box.ascx

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<Action>" %>
<div class="box">
    <div class="top"></div>
    <div class="content">
        <% Model(); %>
    </div>
    <div class="bottom"></div>
</div>

Then in the view:

<% Html.RenderPartial("box", Lambda.Action(() => { %>
content goes here
<% })); %>

Note the Lambda.Action helper method, which is defined as:

public static class Lambda {
    public static Action Action(Action a) {
        return a;
    }
}

This keeps the Action as an Action, otherwise the runtime tries to convert it to a ViewDataDictionary with the obvious conversion error that causes.

Also note that you could write any markup, call any helper, or reference any ViewData or Model property of the view, instead of the simple text "content goes here".

The thing gets more interesting as we add support for multiple sections, which I'll blog about next. (see second part)

No comments: