Sunday, June 12, 2011

MVC3 RadioButtonList Helper

An MVC 3 example which uses the Radio Button helper twice on a page to create 3 radio buttons with the first radio button helper and 2 radio buttons with the 2nd helper.  Both have validation to ensure 1 option is selected. And if the form fails validation the chosen radio option is preselected when the form is reshown.

The radio buttons can be shown on the page horizontally or vertically.

The Html Helper Method

Essentially all this helper does is loop round a list that you provide it and for each item in the list creates a html radio button and a label for it.

I created a new class with the following code:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Mvc.Html;
using System.Linq.Expressions;
using System.Text;

namespace MVC3_RadioButtonList_Helper_Sample
{
    public static class HtmlExtensions
    {
        public static MvcHtmlString RadioButtonForSelectList<TModel, TProperty>(
            this HtmlHelper<TModel> htmlHelper,
            Expression<Func<TModel, TProperty>> expression,
            IEnumerable<SelectListItem> listOfValues)
        {
            var metaData = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
            var sb = new StringBuilder();

            if (listOfValues != null)
            {
                // Create a radio button for each item in the list
                foreach (SelectListItem item in listOfValues)
                {
                    // Generate an id to be given to the radio button field
                    var id = string.Format("{0}_{1}", metaData.PropertyName, item.Value);

                    // Create and populate a radio button using the existing html helpers
                    var label = htmlHelper.Label(id, HttpUtility.HtmlEncode(item.Text));
                    var radio = htmlHelper.RadioButtonFor(expression, item.Value, new { id = id }).ToHtmlString();

                    // Create the html string that will be returned to the client
                    // e.g. <input data-val="true" data-val-required="You must select an option" id="TestRadio_1" name="TestRadio" type="radio" value="1" /><label for="TestRadio_1">Line1</label>
                    sb.AppendFormat("<div class=\"RadioButton\">{0}{1}</div>", radio, label);
                }
            }

            return MvcHtmlString.Create(sb.ToString());
        }
    }
}

I’ve added a div around the radio button and it’s label with a class of ‘RadioButton’, this will let you in CSS position the radio button either vertically or horizontally.  The default is vertically:

InitialPage

But you can switch this to horizontal with a bit of CSS

.RadioButton { float:left; }

InitialPageAsHorizontal

The rest of this article shows how to use the RadioButtonForSelectList helper, you reference it in a view like this: @Html.RadioButtonForSelectList(m => m.TestRadio, Model.TestRadioList)

The Model

For this example I just created an empty MVC3 application and added this model.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;

namespace MVC3_RadioButtonList_Helper_Sample.Models
{
    public class IndexViewModel
    {
        public IEnumerable<SelectListItem> TestRadioList { get; set; }
        public IEnumerable<SelectListItem> TestRadioList2 { get; set; }

        [Required(ErrorMessage = "You must select an option for TestRadio")]
        public String TestRadio { get; set; }

        [Required(ErrorMessage = "You must select an option for TestRadio2")]
        public String TestRadio2 { get; set; }
    }

    public class aTest
    {
        public Int32 ID { get; set; }
        public String Name { get; set; }
    }
}

The Controller Action

I added the following to my controller to populate the model with 2 different lists one for each of the radio button helpers.  The first radio button help also has a default value set so the middle option (‘Line2’) will be pre-selected.

I also changed the Index HttpPost method so that if validation fails the same list of radio buttons is added to the model (obviously I have duplicated the list creation code, in a live example you should create a common function).

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using MVC3_RadioButtonList_Helper_Sample.Models;

namespace MVC3_RadioButtonList_Helper_Sample.Controllers
{
    public class TestController : Controller
    {
        //
        // GET: /Test/
        public ActionResult Index()
        {
            List<aTest> list = new List<aTest>();
            list.Add(new aTest() { ID = 1, Name = "Line1" });
            list.Add(new aTest() { ID = 2, Name = "Line2" });
            list.Add(new aTest() { ID = 3, Name = "Line3" });
            SelectList sl = new SelectList(list, "ID", "Name");

            List<aTest> list2 = new List<aTest>();
            list2.Add(new aTest() { ID = 1, Name = "test1" });
            list2.Add(new aTest() { ID = 2, Name = "test2" });
            SelectList sl2 = new SelectList(list2, "ID", "Name");

            var model = new IndexViewModel();
            model.TestRadioList = sl;
            model.TestRadioList2 = sl2;
           
            model.TestRadio = "2";  // Set a default value for the first radio button helper

            return View(model);
        }

        [HttpPost]
        public ActionResult Index(IndexViewModel model, string returnUrl)
        {
            if (ModelState.IsValid)
            {
                ModelState.AddModelError("", "Always force an error to be raised so we can test the postback sets the radio buttons to their last values.");
            }

            // If we got this far, something failed, redisplay form
            List<aTest> list = new List<aTest>();
            list.Add(new aTest() { ID = 1, Name = "Line1" });
            list.Add(new aTest() { ID = 2, Name = "Line2" });
            list.Add(new aTest() { ID = 3, Name = "Line3" });
            SelectList sl = new SelectList(list, "ID", "Name");

            List<aTest> list2 = new List<aTest>();
            list2.Add(new aTest() { ID = 1, Name = "test1" });
            list2.Add(new aTest() { ID = 2, Name = "test2" });
            SelectList sl2 = new SelectList(list2, "ID", "Name");

            model.TestRadioList = sl;
            model.TestRadioList2 = sl2;

            return View(model);
        }
    }
}

The View

To test the html helper I added a view that is bound to the IndexViewModel.

I added a using statement so the helper method would be found by intelisence @using MVC3_RadioButtonList_Helper_Sample

I added the new helper RadioButtonForSelectList and passed in the TestRadio property from the IndexViewModel which is where the selected result will be put (the radio button ID value e.g. 2).

And the TestRadioList which is a list of SelectListItems for which a radio button will be created for each item in the list.

@Html.RadioButtonForSelectList(m => m.TestRadio, Model.TestRadioList)
@Html.ValidationMessageFor(m => m.TestRadio)

So the error message (as specified in the model ‘TestRadio’ property is shown when no radio button is selected I added a validation message helper.

I then added a 2nd radio button helper to the form like this, which is bound to a different list and it’s return value will be in TestRadio2.

@Html.RadioButtonForSelectList(m => m.TestRadio2, Model.TestRadioList2)
@Html.ValidationMessageFor(m => m.TestRadio2)

The form below shows the validation in action:

ValidationAtWork

 

Here is the source code.

21 comments:

  1. Hi,
    This is great - however, if I use more than one of them on a page, they act together and selecting a radio in one deselects the radio in another. Also, is there a "preselect" via the list. Assuming that in the RadioButtonFor, I can add the "checked" property.
    Finally, I need to be able to set such that the buttons either display horizontally or vertically. I was thinking of a break to insert if using vertical alignment?

    ReplyDelete
  2. Thanks for the comments I've updated the post above to reflect these. The helper class now:

    - Adds a css class to allow you to position the radio buttons horizontally or vertically.

    - The example shows how to set a default value.

    - There is no problem with having more than one radio button helper on a page so I've updated the post to show how to do this.

    - Source code is now available to download.

    ReplyDelete
  3. Thanks for useful article, I also need to know how you can read the values when a user select an item?
    (Amir)

    ReplyDelete
  4. Thank you for a very useful post and helper.

    How can I make the list of TestRadioList and TestRadio inside IndexViewModel dynamic? I tried the whle code using List and defining only 1 TestRadioList and TestRadio within IndexViewMode. But then all the radio button select lists had the same name, and so unique selection per TsxtRadioList couldn't happen.

    ReplyDelete
  5. Very useful!! I converted to VB.NET and then added an optional GroupName parameter on the helper class so I could "name" the id myself.

    This way, I can use jQuery to attach a client side handler for clicking on any radio button.

    ReplyDelete
  6. Very helpful, thanks. Would it be possible to write one for a CheckBoxForSelectList?

    Thanks,

    ReplyDelete
  7. Hi Jon,
    I tried to use your helper, but I've got following error
    "
    Compiler Error Message: CS1973: 'System.Web.Mvc.HtmlHelper' has no applicable method named 'RadioButtonForSelectList' but appears to have an extension method by that name. Extension methods cannot be dynamically dispatched. Consider casting the dynamic arguments or calling the extension method without the extension method syntax.
    "

    Could you please give me any idea, why I've got that error?

    Thanks,
    Aleks

    ReplyDelete
  8. @Anonymous - it sounds like one of the things you're trying to pass into it is a dynamic type (such as any property off of ViewBag). You need to explicitly cast it to string (or int or whatever).

    ReplyDelete
  9. Thanks for a very useful article. I modified it for Enums and it has been great.

    ReplyDelete
  10. You shouldn't need to repopulate the model when validation fails. Can you fix your code so the following works
    [HttpPost]
    public ActionResult Index(IndexViewModel model, string returnUrl)
    {
    if (ModelState.IsValid)
    {
    ViewBag.YouSlected = " Line = " + model.TestRadio.ToString() +
    " Test = " + model.TestRadio2.ToString();

    return View(model);
    // ModelState.AddModelError("", "Always force an error to be raised so we can test the postback sets the radio buttons to their last values.");
    }
    return View(model);
    }
    }
    }

    Index

    @{
    var youSel = ViewBag.YouSlected as string;
    if (!string.IsNullOrEmpty(youSel) ){
    You selected @youSel }
    }

    @Html.ValidationSummary(true, "Form Validation was unsuccessful. Please correct the errors and try again.")

    @using (Html.BeginForm()) {

    ReplyDelete
  11. Hi, why did you defined List list and list 2 two times (in Index GET and POST)?
    Isn't enough to define in Index GET ?

    ReplyDelete
  12. Hi Jon,

    Not sure if other people are getting this, but when I try to download your SOURCE from (http://www.mediafire.com/?2w67qr886efpflr) I get a warning that it's infected with a Js/Exploit.gen trojan. :(

    BTW, thanks for the great tutorial.

    Greg

    ReplyDelete
  13. Can't Express my words.. tat much your article helped me man.. Great job.. keep it up.. Fantastic.. post..!! I really appreciate..!

    ReplyDelete
  14. Really very useful article. thanks so much...

    ReplyDelete
  15. Thanks, It 's Perfect article, Thank you Jon

    ReplyDelete
  16. Thanks Jon but this groups all radio buttons with the same name when I use more than one RadioButtonForSelectList in a foreach loop like so:

    foreach (Question question in Model.Questions) {
    <p>@question.question_text</p>
    @Html.RadioButtonForSelectList(m => question.SelectedResponse, question.ResponseOptions)
    }

    ReplyDelete
  17. Thanks Jon!
    I used this metod in my projects and all was ok.
    But now it is nessary for me show 60 (or more) radiogroups on one page.
    Did you now, how can I use array of properties innstead of 60 properties?

    ReplyDelete
  18. Thanks Jon. This worked for me. Is there a way to set a default selected radiobutton?

    ReplyDelete
  19. I figured it out. I added an optional argument String selectedItemValue = "" and then

    object htmlAttrib = new { id = id };
    if (selectedItemValue == item.Value) htmlAttrib = new { id = id, @checked = "checked" };

    // Create and populate a radio button using the existing html helpers
    var label = htmlHelper.Label(id, HttpUtility.HtmlEncode(item.Text));
    var radio = htmlHelper.RadioButtonFor(expression, item.Value, htmlAttrib).ToHtmlString();

    ReplyDelete
  20. Thanks Jon! Very useful article!

    ReplyDelete