Correct way to bind an mvc3 radiobutton to a model

Solution 1:

For the short answer, you need to do three things:

  1. Remove the new { Checked = "checked" } from the second radio button. This hard-coded checked value will override all of the magic.
  2. When you return your ViewResult from the controller action, give it an instance of your model class where TermsAndConditions is false. This will provide the default false value you need in order to have the false radio button preselected for you.
  3. Use true and false as the values for your radio buttons instead of "True" and "False". This is because your property is of type bool. Strictly speaking, you coincidentally chose the correct string representations for true and false, but the value parameter for the RadioButtonFor method is of type object. It's best to pass in the actual type you want to compare to rather than converting it to a string yourself. More on this below.

Here's what's going on in depth:

The framework wants to do all of this for you automatically, but you did those first two things incorrectly which makes you have to fight with the framework to get the behavior you want.

The RadioButtonFor method calls .ToString() on the value of the property you specified and compares it to the .ToString() of the value you passed in when creating the radio button. If they are equal, then it internally sets isChecked = true and ends up rendering checked="checked" in the HTML. This is how it decides which radio button to check. It simply compares the value of the radio button to the value of the property and checks the one that matches.

You can render radio buttons for pretty much any property this way and it will magically work. Strings, ints, and even enum types all work! Any object that has a ToString method that returns a string which uniquely represents the object's value will work. You just have to make sure you're settings the radio button's value to a value that your property might actually have. The easiest way to do this is just to pass in the value itself, not the string representation of the value. Let the framework convert it to a string for you.

(Since you happened to pass in the correct string representations of true and false, then those values will work as long as you fix your two actual mistakes, but it's still wise to pass in the actual values and not their strings.)

Your first real mistake was hard-coding Checked = "checked" for the "No" radio button. This will override what the framework is trying to do for you and results in this radio button always being checked.

Obviously you want the "No" radio button to be preselected, but you have to do it in a way that's compatible with everything above. You need to give the view an instance of your model class where TermsAndConditions is set to false, and let it "bind" that to the radio buttons. Normally, a controller action which responds to the initial GET request of a URL doesn't give the View an instance of the model class at all. Typically, you just return View();. However, since you want a default value selected, you must provide the view with a instance of your model that has TermsAndConditions set to false.

Here is some source code illustrating all of this:

Some sort of Account class that you probably already have. (Your View's model):

public class Account
{
    public bool TermsAndConditions { get; set; }
    //other properties here.
}

Some methods in your controller:

//This handles the initial GET request.
public ActionResult CreateAccount()
{
    //this default instance will be used to pre-populate the form, making the "No" radio button checked.
    var account = new Account
    {
        TermsAndConditions = false
    };

    return View( account );
}

//This handles the POST request.
[HttpPost]
public ActionResult CreateAccount( Account account )
{
    if ( account.TermsAndConditions )
    {
        //TODO: Other validation, and create the account.
        return RedirectToAction( "Welcome" );
    }
    else
    {
        ModelState.AddModelError( "TermsAndConditionsAgreement", "You must agree to the Terms and Conditions." );
        return View( account );
    }           
}

//Something to redirect to.
public ActionResult Welcome()
{
    return View();
}

The entire View:

@model Account
@{
    ViewBag.Title = "Create Account";
}
@using ( Html.BeginForm() )
{
    <div>
        <span>Do you agree to the Terms and Conditions?</span>
        <br />
        @Html.RadioButtonFor( model => model.TermsAndConditions, true, new { id = "TermsAndConditions_true" } )
        <label for="TermsAndConditions_true">Yes</label>
        <br />
        @Html.RadioButtonFor( model => model.TermsAndConditions, false, new { id = "TermsAndConditions_false" } )
        <label for="TermsAndConditions_false">No</label>
        <br />
        @Html.ValidationMessage( "TermsAndConditionsAgreement" )
    </div>
    <div>
        <input id="CreateAccount" type="submit" name="submit" value="Create Account" />
    </div>
}

BONUS: You'll notice that I added a little extra feature to the radio buttons. Rather than just use plain text for the radio button labels, I used the HTML label element with the for attribute set to the IDs of the each radio button. This lets users click on the label to select the radio button instead of having to click on the radio button itself. This is standard HTML. For this to work I had to set manual IDs on the radio buttons, otherwise they would both get the same ID of just "TermsAndConditions", which wouldn't work.

Solution 2:

There are a few things you need to do here in order to ensure the user's selection is maintained after server side validation.

a) Bind the "checked" property of each radio to your model in the view, for example:

Yes
@Html.RadioButtonFor(model => model.TermsAndConditions, "True", model.TermsAndConditions == true ? new { Checked = "checked" } : null)
No
@Html.RadioButtonFor(model => model.TermsAndConditions, "False", model.TermsAndConditions == false ? new { Checked = "checked" } : null)

b) To define the initial default value when the view is first displayed, initialise the model returned to the view in the GET request (in the controller action), for example:

public ActionResult SomeForm()
{
    return View(new SomeModel { TermsAndConditions = false });
}

b) Ensure in your [HttpPost] controller action that you return the model when the validation fails, for example:

[HttpPost]
public ActionResult SomeForm(SomeModel model)
{
    if (!ModelState.IsValid)
        return View(model);

    // Do other stuff here
}

This way when the view is rendered in the response after validation fails, it will have the actual model state that was passed in (thus maintaining the user's selection).