MVC3 EditorTemplate for a nullable boolean using RadioButtons
Asked Answered
R

6

19

I have a property on one of my objects that is a nullable boolean, I want my logic to have true represent Yes, false to be No and null to be N/A. Now because I am going to have multiple properties like this on many different objects it makes the most sense to make an editor and display templates for these properties. I am going to use jQuery UI to apply a visual element of buttonset after this is all working but for now, that's beyond the scope of my problem. My editor template looks like this.

@model bool?
<div data-ui="buttonset">
@Html.RadioButtonFor(x => x, true, new { id = ViewData.TemplateInfo.GetFullHtmlFieldId("") + "Yes"}) <label for="@(ViewData.TemplateInfo.GetFullHtmlFieldId(""))Yes">Yes</label>
@Html.RadioButtonFor(x => x, false, new { id = ViewData.TemplateInfo.GetFullHtmlFieldId("") + "No" }) <label for="@(ViewData.TemplateInfo.GetFullHtmlFieldId(""))No">No</label>
@Html.RadioButtonFor(x => x, "null", new { id = ViewData.TemplateInfo.GetFullHtmlFieldId("") + "NA" }) <label for="@(ViewData.TemplateInfo.GetFullHtmlFieldId(""))NA">N/A</label>
</div>

My problem is that under no circumstances can I get this editor template to show the current value of the model correctly. Because I am not rendering a property of a model at this scope but the model itself, the built in logic in MVC3 will not set the checked property correctly because of a check that is made to verify the name is not empty or null (See MVC3 source, InputExtensions.cs:line#259). I can't set the checked attribute dynamically by comparing to the Model because the browser checks the radiobutton on the presence of the checked attribute not it's value, so even though my radio buttons would look like the following the last one is still the one selected.

<div class="span-4" data-ui="buttonset">
<input checked="checked" id="MyObject_BooleanValueYes" name="MyObject.BooleanValue" type="radio" value="True" /><label for="MyObject_BooleanValueYes">Yes</label>
<input checked="" id="MyObject_BooleanValueNo" name="MyObject.BooleanValue" type="radio" value="False" /><label for="MyObject_BooleanValueNo">No</label>
<input checked="" id="MyObject_BooleanValueNA" name="MyObject.BooleanValue" type="radio" value="null" /><label for="MyObject_BooleanValueNA">N/A</label>
</div><span class="field-validation-valid" data-valmsg-for="MyObject.BooleanValue" data-valmsg-replace="true"></span>&nbsp;</div>

I can't conditionally add the HtmlAttribute using something like if?truevalue:falsevalue becuase the true/false parts would be of different anonymous types and I get an error.

I'm struggling on how this should be done and am hoping one of you have a suggestion on how to tackle this problem?

Ranita answered 19/7, 2011 at 18:13 Comment(1)
JarrettV's anwer bellow I think is more elegantLingerie
G
28
@model bool?
<div data-ui="buttonset">
@{
    Dictionary<string, object> yesAttrs = new Dictionary<string, object>(); 
    Dictionary<string, object> noAttrs = new Dictionary<string, object>(); 
    Dictionary<string, object> nullAttrs = new Dictionary<string, object>(); 

    yesAttrs.Add("id", ViewData.TemplateInfo.GetFullHtmlFieldId("") + "Yes");
    noAttrs.Add("id", ViewData.TemplateInfo.GetFullHtmlFieldId("") + "No");
    nullAttrs.Add("id", ViewData.TemplateInfo.GetFullHtmlFieldId("") + "NA");

    if (Model.HasValue && Model.Value)
    {
        yesAttrs.Add("checked", "checked");
    }
    else if (Model.HasValue && !Model.Value)
    {
        noAttrs.Add("checked", "checked");
    }
    else
    {
        nullAttrs.Add("checked", "checked");
    }
}

@Html.RadioButtonFor(x => x, "true", yesAttrs) <label for="@(ViewData.TemplateInfo.GetFullHtmlFieldId(""))Yes">Yes</label>
@Html.RadioButtonFor(x => x, "false", noAttrs) <label for="@(ViewData.TemplateInfo.GetFullHtmlFieldId(""))No">No</label>
@Html.RadioButtonFor(x => x, "null", nullAttrs) <label for="@(ViewData.TemplateInfo.GetFullHtmlFieldId(""))NA">N/A</label>
</div>
Gennagennaro answered 22/7, 2011 at 8:51 Comment(7)
That should work. I think I may have been too caught up in this whole single line "magic" MVC + razor lets you accomplish. It's very clean and easy to read but sometimes I think I get too used to it and need to step back and acknowledge that not everything can fit into a single line. I'll try it out as soon as I can.Ranita
Yep, it works but I wouldn't call it elegant. On the upside it's hidden away in an editor template so you don't need to see it!Gennagennaro
@AdamFlanagan, it works, but somehow it displays the labes Yes/No/Null and next to them the corresponding radiobuttons... Any idea how to hide them?Outflank
@Outflank Do you just want the radio buttons with no labels?Gennagennaro
@AdamFlanagan it was CSS thing... working with CSS I manage to only display the labels while hidding the radiobuttons. Now look&feel similar to booleans in iPhone.Outflank
Also... I manage Bool and Bool? in the same template (to display 2 or 3 radiobuttons). The only thing I really don't know how to manage is DEFAULT VALUES. Some booleans need to be started as TRUE, some as FALSE and others as NOT SET.Outflank
Great solution! But I only got this to work by changing the RadioButton with "null" to an empty string (AKA ""). Otherwise I kept getting ModelState.IsValid = false saying that the field can't be "null".Armillia
G
2

How about some extension method fun to keep that "one line to rule them all". :-)

public static class DictionaryHelper
{
    // This returns the dictionary so that you can "fluently" add values
    public static IDictionary<TKey, TValue> AddIf<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, bool addIt, TKey key, TValue value)
    {
        if (addIt)
            dictionary.Add(key, value);
        return dictionary;
    }
}

And then in your template file you simply change the signature of how you are adding the additional parameters including the checked="checked" attribute to the element.

@model bool?
<div data-ui="buttonset">
@Html.RadioButtonFor(x => x, true, new Dictionary<string,object>()
    .AddIf(true, "id", ViewData.TemplateInfo.GetFullHtmlFieldId("") + "Yes")
    .AddIf(Model.HasValue && Model.Value, "checked", "checked")
) <label for="@(ViewData.TemplateInfo.GetFullHtmlFieldId(""))Yes">Yes</label>

@Html.RadioButtonFor(x => x, false, new Dictionary<string,object>()
    .AddIf(true, "id", ViewData.TemplateInfo.GetFullHtmlFieldId("") + "No")
    .AddIf(Model.HasValue && !Model.Value, "checked", "checked")
) <label for="@(ViewData.TemplateInfo.GetFullHtmlFieldId(""))No">No</label>

@Html.RadioButtonFor(x => x, "null", new Dictionary<string,object>()
    .AddIf(true, "id", ViewData.TemplateInfo.GetFullHtmlFieldId("") + "NA")
    .AddIf(!Model.HasValue, "checked", "checked")
) <label for="@(ViewData.TemplateInfo.GetFullHtmlFieldId(""))NA">N/A</label>
</div>
Gin answered 26/12, 2012 at 19:7 Comment(3)
@user3281466: Anywhere you like, so long as the namespace it lives in is included where you use it. It's an extension method. :)Xenophanes
I really like this as a general solution to conditional attributes (so +1), but the new solution by @Canvas is much simpler and works perfectly.Xenophanes
This might have been something I crafted for MVC2 given the timeframe of the answer.Gin
K
2

The problem is that you need to set the checked attribute because the Html.RadioButtonFor does not check a radio button based on a nullable bool (which appears to be a flaw).

Also by putting the radio buttons inside of the label tag, you can select value by clicking the label.

Shared/EditorTemplates/Boolean.cshtml

@model bool?
<label>
    <span>n/a</span>
    @Html.RadioButtonFor(x => x, "", !Model.HasValue ? new { @checked=true } : null) 
</label>
<label>
    <span>Yes</span>
    @Html.RadioButtonFor(x => x, true, Model.GetValueOrDefault() ? new { @checked = true } : null)
</label>
<label>
    <span>No</span>
    @Html.RadioButtonFor(x => x, false, Model.HasValue && !Model.Value ? new { @checked = true } : null)
</label>
Knelt answered 16/9, 2015 at 13:54 Comment(1)
I searched everywhere for a simple solution to this. Well done. Works a treat! +1Xenophanes
O
1

You just need to handle the special null case like so:

<label class="radio">
  @Html.RadioButtonFor(x => x.DefaultBillable, "", new { @checked = !this.Model.DefaultBillable.HasValue })
  Not set
</label>
<label class="radio">
  @Html.RadioButtonFor(x => x.DefaultBillable, "false")
  Non-Billable
</label>
<label class="radio">
  @Html.RadioButtonFor(x => x.DefaultBillable, "true")
  Billable
</label>
Ozieozkum answered 16/4, 2013 at 21:11 Comment(1)
The problem with this solution is that if the attribute "checked" exists at all in an input, the browser will assume it should be checked. Even if the attribute has the value of false (checked="False"), the browser will still check the radio, since the checked attribute exists at all. There are hacks around this, but the fact remains that you can't use conditional logic when setting the checked attribute.Phosphate
I
0

You can use a @helper to simplify the accepted answer:

@model bool?

<div data-ui="buttonset">
    @Radio(true,  "Yes", "Yes")
    @Radio(false, "No",  "No")
    @Radio(null,  "N/A", "NA")
</div>

@helper Radio(bool? buttonValue, string buttonLabel, string buttonId)
{
    Dictionary<string, object> attrs = new Dictionary<string, object>(); 

    // Unique button id
    string id = ViewData.TemplateInfo.GetFullHtmlFieldId("") + buttonId;

    attrs.Add("id", id);

    // Check the active button
    if (Model == buttonValue)
    {
        attrs.Add("checked", "checked");
    }

    @Html.RadioButtonFor(m => m, buttonValue, attrs) <label for="@id">@buttonLabel</label>
}
Illustrate answered 8/6, 2017 at 15:35 Comment(4)
This is true, however it's worth noting this is only available in the MVC5 and earlier. MVC Core does not have helper methods.Ranita
That's a huge step backwards, its very sad.Illustrate
Yes and no. There are alternatives now such as ViewComponents as an alternative to ChildActions, if you have C#7 enabled, you can use Local Functions if you just need a reusable code block. Or if you want to render HTML (like your suggestion), there are the TagHelpers. Just more learning is all :-)Ranita
Not just learning. Large websites have made extensive use of helpers, and these will now have to be re-written by programmers who cost money to employ - far more than the VS license fee. Microsoft should have been more responsible, maintaining support for helpers but warning they were to be deprecated.Illustrate
B
0

using Canvas example above I built a customization model and view so you can control the values via a model and edit them in code, bools aren't always a yes/no/(n/a) so Here's how it looks in MVC5.

using a generic model for the nullable bool

public class Customisable_NullableRadioBool
    {
        public bool? Result { get; set; }
        public string TrueLabel { get; set; }
        public string FalseLabel { get; set; }
        public string NullLabel { get; set; }
        public string AttributeTitle { get; set; }
    }

Here's the CSHTML to be stored in: ~/Views/Shared/EditorTemplates/Customisable_NullableRadioBool.cshtml

@model Customisable_NullableRadioBool
@Model.AttributeTitle<br />
<div class="control-group">
    <label>
        @Html.RadioButtonFor(x => x.Result, "", !Model.Result.HasValue ? new { @checked = true } : null)
        <span>@Model.NullLabel</span>
    </label>
    <br />
    <label>
        @Html.RadioButtonFor(x => x.Result, true, Model.Result.GetValueOrDefault() ? new { @checked = true } : null)
        <span>@Model.TrueLabel</span>
    </label>
    <br />
    <label>
        @Html.RadioButtonFor(x => x.Result, false, Model.Result.HasValue && !Model.Result.Value ? new { @checked = true } : null)
        <span>@Model.FalseLabel</span>
    </label>
</div>

And then you can reference the generic class and the editor template through the rest of your project and render the editor template like this.

@Html.EditorFor(m => m.YourCustomisable_NullableBool, "Customisable_NullableRadioBool")

And the rendered output examples

Rendered html example

Basinet answered 24/4, 2019 at 8:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.