Wednesday, November 9, 2011

MVC RadioButtonList HTML Helper


The MVC framework does not come with a built-in RadioButtonList comes standard in ASP.NET web forms.  The MVC Futures assembly (which can be downloaded here) does come with this HtmlHelper but there are some issues with it including the ones described here by Elijah Manor.  He concludes that the method in the futures assembly isn’t very useful and states, “To get around this I ended up writing a foreach creating individual Html.RadioButton and labels.”  However, he did not show the code he wrote to implement it.  I came to a similar conclusion as Elijah so I figured I’d share the code and discuss a couple of interesting issues.
First off, my main goal was to be able to use a Helper that looked something like this:
   1:  <%=Html.RadioButtonList(m => m.Name, new[] { "Foo", "Bar" })%>
to produce HTML that looked something like this:
   1:  <table>
   2:      <tr>
   3:          <td>
   4:              <input id="Name_Foo" name="Name" type="radio" value="Foo" /><label for="Name_Foo" id="Name_Foo_Label">Foo</label>
   5:          </td>
   6:          <td>
   7:              <input id="Name_Bar" name="Name" type="radio" value="Bar" /><label for="Name_Bar" id="Name_Bar_Label">Bar</label>
   8:          </td>
   9:      </tr>
  10:  </table>
This output is very similar to the output produced by the horizontal layout of the web forms RadioButtonList server control – of course if you want to use a non-table approach, have at it! 
Originally, I looked to build a typical HTML helper method internally leveraging the already built HTML RadioButton extension methods that ship with MVC. However, the primary thing I found odd is there does not seem to be a way to create a radio button control where the “id” and “name” attributes are different values (as shown in the above HTML). (Note: if someone knows a way around this, please let me know!)  On the other hand, the controls that come in FluentHtml library of MvcContrib does allow these attributes to correctly have different values.  I *highly* recommend trying out the FluentHtml library as it is a much nicer way (in my opinion) to create views than the standard HTML helpers that ship with MVC. So I built some HTML helpers that leverage the FluentHtml library rather than the built-in library.  The primary difference when authoring the custom HTML helper methods is that the FluentHtml methods extend IViewModelContainer<T> rather than the HtmlHelper class.  This translates to the extension methods hanging off the “this” pointer rather than the Html property of the view page.  My code for the RadioButtonList extensions was:
   1:  public static string RadioButtonList<T>(this IViewModelContainer<T> container, Expression<Func<T, object>> expression, IEnumerable<string> items) where T : class
   2:  {
   3:      var func = expression.Compile();
   4:      var result = func(container.ViewModel);
   5:      var selectList = new SelectList(items, result);
   6:      return container.RadioButtonList(expression, selectList);
   7:  }
   8:   
   9:  public static string RadioButtonList<T>(this IViewModelContainer<T> container, Expression<Func<T, object>> expression, IEnumerable<SelectListItem> items) where T : class
  10:  {
  11:      TagBuilder tableTag = new TagBuilder("table");
  12:      tableTag.AddCssClass("radio-main");
  13:   
  14:      var trTag = new TagBuilder("tr");
  15:      foreach (var item in items)
  16:      {
  17:          var tdTag = new TagBuilder("td");
  18:          var radioTag = container.RadioButton(expression).Value(item.Value ?? item.Text).Checked(item.Selected).LabelAfter(item.Text);
  19:          tdTag.InnerHtml = radioTag.ToString();
  20:   
  21:          trTag.InnerHtml += tdTag.ToString();
  22:      }
  23:      tableTag.InnerHtml = trTag.ToString();
  24:   
  25:      return tableTag.ToString();
  26:  }
Notice on line #18, I am taking advantage of the fluent interface enabled by the FluentHtml library to chain the method calls together. Also, I provide an overload that that you can use either IEnumerable<SelectListItem> or IEnumerable<string>.  Taking advantage of the SelectListItem semantics enables the ability to leverage the “Selected” property of the SelectListItem instance since I’m compiling the expression on line #3 above that was sent in to get the current value of the model’s property. This enabled me to now have this 1 line of code in my view:
   1:  <%=this.RadioButtonList(m => m.Name, new[] { "Foo", "Bar" })%>
If you do not want to use the FluentHtml library, you can still come close to this with the built in extension methods.  The only thing lacking is that the HTML produced will have the same values for the “name” and “id” attributes which is really not technically correct. Also, there’s more manual work to create the “label” tag.  The code using the built-in methods is:
   1:  public static string RadioButtonList2(this HtmlHelper helper, string name, IEnumerable<string> items) 
   2:  {
   3:      var selectList = new SelectList(items);
   4:      return helper.RadioButtonList2(name, selectList);
   5:  }
   6:   
   7:  public static string RadioButtonList2(this HtmlHelper helper, string name, IEnumerable<SelectListItem> items) 
   8:  {
   9:      TagBuilder tableTag = new TagBuilder("table");
  10:      tableTag.AddCssClass("radio-main");
  11:   
  12:      var trTag = new TagBuilder("tr");
  13:      foreach (var item in items)
  14:      {
  15:          var tdTag = new TagBuilder("td");
  16:          var rbValue = item.Value ?? item.Text;
  17:          var rbName = name + "_" + rbValue;
  18:          var radioTag = helper.RadioButton(rbName, rbValue, item.Selected, new { name = name });
  19:          
  20:          var labelTag = new TagBuilder("label");
  21:          labelTag.MergeAttribute("for", rbName);
  22:          labelTag.MergeAttribute("id", rbName + "_label");
  23:          labelTag.InnerHtml = rbValue;
  24:   
  25:          tdTag.InnerHtml = radioTag.ToString() + labelTag.ToString();
  26:   
  27:          trTag.InnerHtml += tdTag.ToString();
  28:      }
  29:      tableTag.InnerHtml = trTag.ToString();
  30:   
  31:      return tableTag.ToString();
  32:  }
The important thing is that this type of pattern can be used to build your own re-usable libraries in your own apps.  Perhaps you don’t want a horizontal layout or you don’t want to use HTML tables. The above approach can be customized to your suit needs.