Practical .NET

Building a Useful HTML Helper for ASP.NET MVC Views

Creating a full-featured extension for the ASP.NET MVC HtmlHelper class sounds like a lot of work. But, because of the way the Microsoft .NET Framework works, it's almost trivially easy. To show that, here's an EditBlockFor method that adds a label, a textbox and a validation message to your ASP.NET View.

There's a block of code that I'm constantly writing in my ASP.NET MVC Views. That block consists of a label, an editor and a validation message area for a property on my Model class. My block looks like this:

@Html.LabelFor(Function(c) c.FirstName)
@Html.EditorFor(Function(c) c.FirstName)
@Html.ValidationMessageFor(Function(c) c.FirstName)

What I want is a new method on the HtmlHelper class that generates all the output from this block in one method call. I want to just have to write something like this:

@Html.EditBlockFor(function(c) c.FirstName)

That sounds terrifically complicated to do because, after all, a new *For method should accept a lambda expression (rather than something simple like a string) to specify the property to be displayed. And what if that property has been decorated with attributes, like this:

Public Class Customer
  Public Property Id As Integer
  <DisplayName("First Name")>
  <MaxLength(25)>
  Public Property FirstName As String
  <DisplayName("Last Name")>
  <MaxLength(25)>
  Public Property LastName As String
End Class

In that case, the label generated by the method should respond to the DisplayName attribute rather than just use the property's name as the label text; the textbox generated should leverage HTML5 to handle the MaxLength attribute. And those attributes are just the tip of the iceberg: There are lots more the EditBlockFor should deal with (not to mention that, if the property is a Boolean value, it should generate a checkbox and not a textbox).

But it gets worse: The *For methods don't accept just the lambda expression that specifies the property to use. The *For methods also accept a collection of HtmlAttributes that are to be incorporated into the HTML generated by the *For method. The method should support that, also.

You could imagine, if you're not careful, that you could end up re-creating ASP.NET MVC when all you want is to do is bundle up three existing methods.

A Simple Solution
That last sentence actually holds the answer to the problem, though. One of the nicer features of the Microsoft .NET Framework is that the internals of many framework methods are implemented in other framework classes. Often, framework methods call other framework methods to work their magic. More importantly, you're free to call those methods yourself.

To put it another way: Because the existing EditFor, LabelFor and MessageFor methods are doing all the right things, why not just use those existing .NET Framework methods inside the new EditBlockFor method? If you solve the problem by leveraging the framework, your code gets very simple.

The simplest way to add my new method is to create an extension method that attaches itself to the HtmlHelper class. However, the HtmlHelper class that you access through the View's Html property is data typed to whatever type the View is using. In a View bound to a Customer class, the View's Html property actually holds a class of the type HtmlHelper(of Customer). To fully match the existing *For methods, your method needs to attach to a generic class.

The critical issue in writing this extension method, therefore, is not the code inside the method -- that code is almost trivial. The critical issue is in defining that method as a generic method with exactly the same parameters as the existing *For methods so that you can capture the parameters that your code needs. In Visual Basic, the skeleton for that extension method looks like this:

Public Module PHVExtensions
  <Extension()>
  Public Function EditBlockFor(Of T, TValue)(helper As Mvc.HtmlHelper(Of T),
                                     prop As Expression(Of System.Func(Of T, TValue)),
                                     Optional htmlAttributes As
                                     Dictionary(Of String, Object) = Nothing) As IHtmlString
  End Function
End Module

In C#, the skeleton looks like this:

public static class PHVExtensions
{
  public static IHtmlString EditBlockFor<T, TValue>(this HtmlHelper<T> helper,  
                                            Expression<System.Func<T, TValue>> prop, 
                                            Dictionary<String, Object> htmlAttributes = null) 
  {
 
  }
}

Now, inside your method, you just need to call the methods you're bundling, passing to each method the parameters passed to your EditBlockFor method. As you call each method, you must catch each result, concatenate them together in a way that makes sense to you, and return the whole result wrapped inside an HtmlString object. That code looks like this:

Dim html1 As HtmlString
Dim html2 As HtmlString
Dim html3 As HtmlString

html1 = helper.LabelFor(prop, htmlAttributes)
html2 = helper.EditorFor(prop, htmlAttributes)
html3 = helper.ValidationMessageFor(prop)
        
Return New HtmlString(html1.ToString & ": " &
                      html2.ToString & " " &
                      html3.ToString)

With the equivalent C# code you'll also need to include a namespace directive (a using statement) for the System.Web.Mvc.Html namespace.

One caveat, though: To use this extension method in a View you must include a namespace directive in the View for the project of which the extension method is a part. That's true even if you define the method inside the same project as the View that's using it. So, in Visual Basic, an actual View that uses my extension method has to include an @imports statement at the top of the View with the namespace of the project containing the method. Here's a Visual Basic example that assumes the extension method is in the same project as the View:

@ModelType MyMvcProject.Customer
@imports MyMvcProject

@Html.EditBlockFor(Function(c) c.FirstName)

In C#, the code looks like this:

@model MyMvcProject.Models.Customer
@using MyMvcProject.Models

@Html.EditBlockFor(c => c.FirstName)

And, you'll notice, with this skeleton you can now build any new extension to the HtmlHelper class you want by leveraging the helper's other, existing methods. The .NET Framework: It's a beautiful thing.

About the Author

Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.

comments powered by Disqus

Featured

Subscribe on YouTube