How to use HtmlConventions to create a radio button list input for FuBuMVC

Ran into a little difficulty with a FuBuMVC website today while trying to have InputFor generate a radio button list. The problem was that we were using an enum (as the one below) to represent the values, which passed through InputFor turns into an input field of type text. I wrote a small Attribute in order to take care of it with a custom FuBu HtmlConvention. The code in this post is available in full at GitHub.

Note: Updated section about editor builder class after merging with id fix by Jaime Febres; see article comments. Thank you Jaime :)

Early version of TextPosition.cs

public enum TextPosition
{
  Above = 0,
  Below = 1,
  Right = 2,
  Left = 3,
}

Early version MyDemonstrationModel.cs

public class MyDemonstrationModel
{
  public TextPosition Position { get; set; }
}

So, what I wanted was for the call to InputFor to give me an ul-list with radio buttons and labels when I passed it a property of this enum type. Note that the code has been altered into something more demonstrationable.

MyDemonstration.cshtml

@this.InputFor(model => model.Position)
<ul>
  <li>
    <input id="Position_Above" name="Position" type="radio" value="Above" checked="checked" />
    <label for="Position_Above">Display text <strong>above</strong> the image.</label>
  </li>
  <li>
    <input id="Position_Below" name="Position" type="radio" value="Below" />
    <label for="Position_Below">Display text <strong>below</strong> the image.</label>
  </li>
<!-- And so on.. -->

I also needed DisplayFor to yield a span tag with the selected radio button’s label text inside of it. Another requirement was that HTML should be allowed in the labels; so no HTML encoding in other words.

@this.DisplayFor(model => model.Position)
<span>Display text <strong>above</strong> the image.</span>

How to use a FuBuMVC HtmlConvention to render a radio button list

Obviously, each possible value of the enum was to turn into its own list item element as seen above. So, the first thing that I needed was a way of attaching label texts to the different radio button alternatives. In System.ComponentModel there is an attribute called DescriptionAttribute which came in quite handy here. I modified the enum adding label texts like so:

TextPosition.cs

public enum TextPosition
{
  [Description("Display text <strong>above</strong> the image.")]
  Above = 0,

  [Description("Display text <strong>below</strong> the image.")]
  Below = 1,

  [Description("Display text to the <strong>right</strong> of the image.")]
  Right = 2,

  [Description("Display text to the <strong>left</strong> of the image.")]
  Left = 3,
}

In order to extract the description string from the selected enum value I used the small extension method below, allowing me to simply call ToDescriptionString() on my enum type property.

ObjectExtensions.cs

public static string ToDescriptionString(this Object obj)
{
  var attributes = (DescriptionAttribute[]) obj.GetType().GetField(obj.ToString()) .GetCustomAttributes(typeof(DescriptionAttribute), false);
  return attributes.Length > 0 ? attributes[0].Description : string.Empty;
}

The method basically just checks to see if the supplied object has the DescriptionAttribute, and returns the corresponding string if it does. So, with all the tools needed to pass label information it was time to start working on the actual radio buttons. For this, I added three classes to the solution; an attribute class, as well as builder classes for viewing and editing. My collegue Karl Ahlin had done something similar to get a rich text editor to work using TinyMCE, so why not continue down the path.

Visual Studio 2012 Solution explorer showing the FubuMVC HtmlConvention for the EnumRadioButtonList.

The FuBuMVC Radio Button List

The EnumRadioButtonListAttribute is only supposed to tag our enums for rendering as radio button lists, so as long as it extends the Attribute class we should be OK. The AttributeUsageAttribute allows us to, among other things, specify an intended usage target.

EnumRadioButtonListAttribute.cs

[AttributeUsage(AttributeTargets.Property, Inherited = true, AllowMultiple = false)]
public class EnumRadioButtonListAttribute : Attribute {}

A slight addition to the model class will allow us to tell that the Position enum should indeed be turned into a radio button list. Just add the EnumRadioButtonList attribute to the enum as below.

MyDemonstrationModel.cs

public class MyDemonstrationModel
{
  [EnumRadioButtonList]
  public TextPosition Position { get; set; }
}

It is in the EnumRadioButtonListEditor and EnumRadioButtonListDisplay classes that things get interesting. Let us start by looking at the editor builder.

EnumRadioButtonListEditor.cs

public class EnumRadioButtonListEditor : ElementBuilder
{
  protected override bool matches(AccessorDef def)
  {
    return def.Accessor .HasAttribute<EnumRadioButtonListAttribute>();
  }

As you can see, we inherit from the FuBuMVC class ElementBuilder (in FubuMVC.Core.UI.Configuration). This will let us construct our own build output HtmlTag object which will be rendered for enums with the EnumRadioButtonList attribute when passing them to InputFor. We override the matches method specifying what we should apply the builder on and have it trigger on our attribute. So, having done this, the below Build method will be run for anything matching the condition.

  public override HtmlTag Build(ElementRequest request)
  {
    var enumType = request.Accessor.PropertyType;
    var enumValues = Enum.GetValues(enumType);
    var list = new HtmlTag("ul");
    foreach (var enumValue in enumValues)
    {
      var item = ListItemFor(enumValue, request);
      list.Children.Add(item);
    }
    return list;
  }

As you can see in the code snippet above, there is no error handling added. This means that if anyone would add the attribute to something that is not an enum, things might not work all that well. I do not see it as a problem though, looking through the do-it-right-or-don’t looking glass. What goes around, comes around. First we find out which enum we are supposed to render by getting the PropertyType from the ElementRequest input object. Once this is done, we can retrieve an Array of all the enum values by calling the static GetValues method for the type. After this, it is just a matter of looping through them constructing li-elements with corresponding content for each one as below.

  private static HtmlTag ListItemFor(object enumValue, ElementRequest request)
  {
    var radioId = string.Format("{0}_{1}", request.ElementId, enumValue);

    var radioButton = new HtmlTag("input")
        .Attr("id", radioId)
        .Attr("name", request.ElementId)
        .Attr("type", "radio")
        .Attr("value", enumValue.ToString());
    if (enumValue.ToString() == request.StringValue())
    {
      radioButton.Attr("checked", "checked");
    }

    var label = new HtmlTag("label")
        .Attr("for", radioId)
        .Encoded(false)
        .Text(enumValue.ToDescriptionString());

    return new HtmlTag("li", tag => tag.Children.AddMany(new[] {radioButton, label}));
  }
}

Note: Updated code above and following paragraph to support nested properties after Jaime’s gist; see article comments.

The ListItemFor method above creates HTML list item tags containing radio buttons and labels depending on what is specified for each of the enum values. First an id is specified in order to tie the <input type=”radio” .. /> tag to the <label for=”..” .. /> (this id will be a combination of the property name id generated by FuBuMVC and the current property value; for instance, the example above would yield Position_Above and Position_Below if Position is a top level property). The names of all the radio buttons should be the same, so we use the property’s name FuBuMVC’s id for this as well, and then do a comparison to see which radio button should be selected.

The important thing for the label tag is to set the Encoded property to false, or you will just get a bunch of encoded characters instead of your pretty HTML attempts. Specifying the Text for the label allows us the opportunity to use the ToDescriptionString extension method that we created before.

FuBuMVC Radio Button List InputFor builder in action.

EnumRadioButtonListDisplay.cs

public class EnumRadioButtonListDisplay : ElementBuilder
{
  protected override bool matches(AccessorDef def)
  {
    return def.Accessor.HasAttribute<EnumRadioButtonListAttribute>();
  }

  public override HtmlTag Build(ElementRequest request)
  {
    var enumType = request.Accessor.PropertyType;
    var enumValues = Enum.GetValues(enumType);

    var selectedValue = enumValues.Cast<object>()
          .First(e => e.ToString() == request.StringValue());

    var span = new HtmlTag("span")
          .Encoded(false)
          .Text(selectedValue.ToDescriptionString());

    return span;
  }
}

As you can see above, the EnumRadioButtonListDisplay element builder is rather similar to the editor one; here we just get the selected enum value and return it inside a span tag.

FuBuMVC Radio Button List DisplayFor builder in action.

The only thing left to do in order for this to work, is registering the HTML conventions in FuBuMVC. Create a custom HTML conventions class inheriting from FubuMVC.Core.UI.HtmlConventionRegistry, add the display and editor builders as below, and we’re done.

CustomHtmlConventions.cs

public class CustomHtmlConventions : HtmlConventionRegistry
{
  public CustomHtmlConventions()
  {
    Displays.Builder<EnumRadioButtonListDisplay>();
    Editors.Builder<EnumRadioButtonListEditor>();
  }
}

Oh, you may also want to add a short HtmlConvention<CustomHtmlConventions>(); into your FuBuMVC configuration class (the one inheriting from FubuRegistry that is).

3 Comments

  1. Jaime Febres August 31, 2012
    • Mathias Kunto September 2, 2012
  2. Jaime Febres August 31, 2012

Leave a Reply