Getting an HtmlHelper for an Alternate Model Type

Published on Wednesday, October 17, 2012

First off, I'm back and have a lot of little tips to blog about over the next several weeks. Since my last post I have changed jobs and am no longer working with Mono, Gtk#, or XML on a daily basis. However, I am still developing for the .NET platform and have been focusing recently on ASP.NET MVC and Entity Framework. Now, on to the topic at hand...

By default, the Razor view engine for ASP.NET MVC rigs up a nice little object of type HtmlHelper<TModel> for us that can be used as the first argument in extension methods. This convention gets used a lot for all sorts of HTML helpers such as automatic generation of form controls, validation messages, etc. In most cases, you want to operate on the model type that was passed to the view and the default HtmlHelper instance works fine. However, there are some cases when you might need access to a HtmlHelper that has a different generic type than that of the view's model. Some of these include displaying the display name metadata (such as that from a DisplayAttribute on your model) in column headers for a table (in which case the view's model is probably an IEnumerable and not the actual type that you want metadata for) and displaying form controls for models included through encapsulation inside the view's model. Regardless of the reason, it's not easy to create an HtmlHelper for a type other than the specified model - you can't just construct one from scratch because the HtmlHelper class expects a lot of information about the context and data in the view.

Thankfully there are ways to make this work. I would like to give a lot of credit to Tahir Hassan for his answer to a related question on Stack Overflow. In his answer he describes an HTML helper extension method that can get a new HtmlHelper of a requested type. The code in this article is based very heavily on his code with very few changes. Below is the code for a series of extension methods and direct methods (more on these in a later post) for getting an alternate HtmlHelper.

public static class HtmlHelpers
{
    public static HtmlHelper<TModel> For<TModel>(this HtmlHelper helper) where TModel : class, new()
    {
        return For<TModel>(helper.ViewContext, helper.ViewDataContainer.ViewData, helper.RouteCollection);
    }

    public static HtmlHelper<TModel> For<TModel>(this HtmlHelper helper, TModel model)
    {
        return For<TModel>(helper.ViewContext, helper.ViewDataContainer.ViewData, helper.RouteCollection, model);
    }

    public static HtmlHelper<TModel> For<TModel>(ViewContext viewContext, ViewDataDictionary viewData, RouteCollection routeCollection) where TModel : class, new()
    {
        TModel model = new TModel();
        return For<TModel>(viewContext, viewData, routeCollection, model);
    }

    public static HtmlHelper<TModel> For<TModel>(ViewContext viewContext, ViewDataDictionary viewData, RouteCollection routeCollection, TModel model)
    {
        var newViewData = new ViewDataDictionary(viewData) { Model = model };
        ViewContext newViewContext = new ViewContext(
            viewContext.Controller.ControllerContext,
            viewContext.View,
            newViewData,
            viewContext.TempData,
            viewContext.Writer);
        var viewDataContainer = new ViewDataContainer(newViewContext.ViewData);
        return new HtmlHelper<TModel>(newViewContext, viewDataContainer, routeCollection);
    }

    private class ViewDataContainer : System.Web.Mvc.IViewDataContainer
    {
        public System.Web.Mvc.ViewDataDictionary ViewData { get; set; }

        public ViewDataContainer(System.Web.Mvc.ViewDataDictionary viewData)
        {
            ViewData = viewData;
        }
    }
}

Once these extension methods are available you can write things like the following in your views:

<th>@(Html.For<TableItemModel>().DisplayNameFor(m => m.ThisColumnProperty))</th>
<td>@(Html.For(rowItem).TextBoxFor(m => m.ThisColumnProperty))</td>

By itself this is already a powerful capability and opens up your views to additional model types easily. In my next post I'll detail how to use the non-extension versions with the MVC wrappers for KendoUI to automatically set the title of a bound grid column.