try-catch-FAIL

Failure is inevitable

NAVIGATION - SEARCH

My best (or worst) MVC hack to date…

I really don’t know if I should be proud or embarrassed by what I just implemented.  I’m going to go with “embarrassed”.  If anyone sees a better way to do it, let me know.

The Problem

I have a multi-tab interface (via jQuery UI  tabs).  The view is strongly-typed and has a view model, so it’s decoupled from the domain objects that are used to populate it.  The view is rendered using the MVC Future expression-based input builders, so instead of Html.TextBox(“Blah”), I’ve got Html.TextBoxFor(m => m.Property).  For those that don’t know, the expression-based builders allow you to bind to your view model using expressions instead of magic strings, so you get compile-time safety, rename support, etc.  Anyway, two of the tabs are *almost exactly* the same except for their labels and the underlying object that they’re bound to.  Basically, the original view model looks like this:

public class MySuperViewModel
{
    WidgetViewModel Tab1 { get; set; }
    
    WidgetViewModel Tab2 { get; set; }
}

What I want to do is render the same UI for both FirstTab and SecondTab, have both wired up by ASP.NET automagically, and not have to repeat a bunch of markup.

What doesn’t work

My first though was “oh, I’ll just make a strongly-typed partial view for WidgetTabViewModel, and render it twice!”  That doesn't work though because of how the expression-based input builders work.  They build their IDs based on the expression you pass in.  If I was going to take this approach, inside the partial for the tabs I would be calling this ‘Html.TextBoxFor(m => m.WidgetName)’, which will generate the following markup:

<input name=”WidgetName” … ></input>

Note that there’s nothing in the markup that’s going to let it ASP.NET MVC know where to wire things up in relation to the original view model.  Not good. This is what it needs to look like:

<input name=”Tab1_WidgetName” …></input>

My next thought was “well, I’ll just make a special view model to drive the tab partial view, and pass the expressions in via the view model”, like so:

public class TabViewModel
{
    public Expression<Func<MySuperViewModel, string>> Name;
    
    public Expression<Func<MySuperViewModel, string>> Value;
}

...
//Does not compile, TextBoxFor is actually TextBoxFor<Expression<Func<TabViewModel,object>>
<%=Html.TextBoxFor(Model.Name) %> 

This almost works, but unfortunately the expression-based input builders are defined on HtmlHelper<TModel>, where TModel is your view model type, meaning that the expressions I passed in from the top-level view do not match what TextBoxFor expects as input. 

My horrendous hack/brilliant solution

The fundamental problem is that I want to bind my inputs to expressions as if they were built from the top-level view using its HtmlHelper.  My solution is simple: I just pass in the top-level view’s HtmlHelper view the view model:

public class TabViewModel
{
    public Expression<Func<MySuperViewModel, string>> Name;
    
    public Expression<Func<MySuperViewModel, string>> Value;
    
    //HOT (Hack Or Not)?
    public HtmlHelper<MySuperViewModel> Helper;
}

In the partial view, I can render my inputs and get the correct binding names for MVC using ‘<%=Model.Helper.TextBoxFor(Model.Name))’.  In the master view, I can render the partial view using something like this:

<% Html.RenderPartial("MyTab", new TabViewModel
{
    Name = m => m.Tab1.Name,
    Value = m => m.Tab1.Value,
    Label = "Tab 1",
    Helper = Html
}); %>

I was sort-of surprised when this worked, but it did, and it saved me from having to duplicate about 100 lines of markup.  It still feels dirty though, so if anyone sees a good way to get around this (and I really hope there is), let me know. 

About Matt Honeycutt...

Matt Honeycutt is a software architect specializing in ASP.NET web applications, particularly ASP.NET MVC. He has over a decade of experience in building (and testing!) web applications. He’s an avid practitioner of Test-Driven Development, creating both the SpecsFor and SpecsFor.Mvc frameworks.

He's also an author for Pluralsight, where he publishes courses on everything from web applications to testing!

blog comments powered by Disqus