可空布尔值的复选框

我的模型有一个必须为空的布尔值

public bool? Foo
{
get;
set;
}

所以在我的 Razor cshtml 里

@Html.CheckBoxFor(m => m.Foo)

除了那个没用。用(布尔)铸造也没用。如果我这样做

@Html.CheckBoxFor(m => m.Foo.Value)

它不会创建错误,但是在发布时不会绑定到我的模型,并且 foo 被设置为 null。在页面上显示 Foo 并将其绑定到帖子中的模型的最佳方式是什么?

117227 次浏览

I had a similar issue in the past.

Create a Checkbox input in HTML, and set the attribute name="Foo" This should still post properly.

<input type="checkbox" name="Foo" checked="@model.Foo.Value" /> Foo Checkbox<br />

My model has a boolean that has to be nullable

Why? This doesn't make sense. A checkbox has two states: checked/unchecked, or True/False if you will. There is no third state.

Or wait you are using your domain models in your views instead of view models? That's your problem. So the solution for me is to use a view model in which you will define a simple boolean property:

public class MyViewModel
{
public bool Foo { get; set; }
}

and now you will have your controller action pass this view model to the view and generate the proper checkbox.

I got it to work with

@Html.EditorFor(model => model.Foo)

and then making a file at Views/Shared/EditorTemplates/Boolean.cshtml with the following:

@model bool?


@Html.CheckBox("", Model.GetValueOrDefault())

Complicating a primitive with hidden fields to clarify whether False or Null is not recommended.

Checkbox isn't what you should be using -- it really only has one state: Checked. Otherwise, it could be anything.

When your database field is a nullable boolean (bool?), the UX should use 3-Radio Buttons, where the first button represents your "Checked", the second button represents "Not Checked" and the third button represents your null, whatever the semantics of null means. You could use a <select><option> drop down list to save real estate, but the user has to click twice and the choices aren't nearly as instantaneously clear.

  1     0      null
True  False  Not Set
Yes   No     Undecided
Male  Female Unknown
On    Off    Not Detected

The RadioButtonList, defined as an extension named RadioButtonForSelectList, builds the radio buttons for you, including the selected/checked value, and sets the <div class="RBxxxx"> so you can use css to make your radio buttons go horizontal (display: inline-block), vertical, or in a table fashion (display: inline-block; width:100px;)

In the model (I'm using string, string for the dictionary definition as a pedagogical example. You can use bool?, string)

public IEnumerable<SelectListItem> Sexsli { get; set; }
SexDict = new Dictionary<string, string>()
{
{ "M", "Male"},
{ "F", "Female" },
{ "U", "Undecided" },


};


//Convert the Dictionary Type into a SelectListItem Type
Sexsli = SexDict.Select(k =>
new SelectListItem
{
Selected = (k.Key == "U"),
Text = k.Value,
Value = k.Key.ToString()
});


<fieldset id="Gender">
<legend id="GenderLegend" title="Gender - Sex">I am a</legend>
@Html.RadioButtonForSelectList(m => m.Sexsli, Model.Sexsli, "Sex")
@Html.ValidationMessageFor(m => m.Sexsli)
</fieldset>


public static class HtmlExtensions
{
public static MvcHtmlString RadioButtonForSelectList<TModel, TProperty>(
this HtmlHelper<TModel> htmlHelper,
Expression<Func<TModel, TProperty>> expression,
IEnumerable<SelectListItem> listOfValues,
String rbClassName = "Horizontal")
{
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 = String.Empty;


if (item.Selected == true)
{
radio = htmlHelper.RadioButtonFor(expression, item.Value, new { id = id, @checked = "checked" }).ToHtmlString();
}
else
{
radio = htmlHelper.RadioButtonFor(expression, item.Value, new { id = id }).ToHtmlString();


}// Create the html string to return to client browser
// e.g. <input data-val="true" data-val-required="You must select an option" id="RB_1" name="RB" type="radio" value="1" /><label for="RB_1">Choice 1</label>


sb.AppendFormat("<div class=\"RB{2}\">{0}{1}</div>", radio, label, rbClassName);
}
}


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

I have bool? IsDisabled { get; set; } in Model. Inserted if in View.

<div class="inputClass" id="disabled">
<div>
@if(Model.IsDisabled==null)
{
Model.IsDisabled = false;
}
@Html.CheckBoxFor(model => model.IsDisabled.Value)
</div>
</div>

For me the solution was to change the view model. Consider you are searching for invoices. These invoices can be paid or not. So your search has three options: Paid, Unpaid, or "I don't Care".

I had this originally set as a bool? field:

public bool? PaidInvoices { get; set; }

This made me stumble onto this question. I ended up created an Enum type and I handled this as follows:

@Html.RadioButtonFor(m => m.PaidInvoices, PaidStatus.NotSpecified, new { @checked = true })
@Html.RadioButtonFor(m => m.PaidInvoices, PaidStatus.Yes)
@Html.RadioButtonFor(m => m.PaidInvoices, PaidStatus.No)

Of course I had them wrapped in labels and had text specified, I just mean here's another option to consider.

Checkbox only offer you 2 values (true, false). Nullable boolean has 3 values (true, false, null) so it's impossible to do it with a checkbox.

A good option is to use a drop down instead.

Model

public bool? myValue;
public List<SelectListItem> valueList;

Controller

model.valueList = new List<SelectListItem>();
model.valueList.Add(new SelectListItem() { Text = "", Value = "" });
model.valueList.Add(new SelectListItem() { Text = "Yes", Value = "true" });
model.valueList.Add(new SelectListItem() { Text = "No", Value = "false" });

View

@Html.DropDownListFor(m => m.myValue, valueList)

This is an old question, and the existing answers describe most of the alternatives. But there's one simple option, if you have bool? in your viewmodel, and you don't care about null in your UI:

@Html.CheckBoxFor(m => m.boolValue ?? false);

Just check for the null value and return false to it:

@{ bool nullableValue = ((Model.nullableValue == null) || (Model.nullableValue == false)) ? false : true; }
@Html.CheckBoxFor(model => nullableValue)

I also faced the same issue. I tried the following approach to solve the issue because i don't want to change the DB and again generate the EDMX.

@{


bool testVar = (Model.MYVar ? true : false);


}


<label>@Html.CheckBoxFor(m => testVar)testVar</label><br />

Found answer in similar question - Rendering Nullable Bool as CheckBox. It's very straightforward and just works:

@Html.CheckBox("RFP.DatesFlexible", Model.RFP.DatesFlexible ?? false)
@Html.Label("RFP.DatesFlexible", "My Dates are Flexible")

It's like accepted answer from @afinkelstein except we don't need special 'editor template'

I would actually create a template for it and use that template with an EditorFor().

Here is how I did it:

  1. Create My template, which is basically a partial view I created in the EditorTemplates directory, under Shared, under Views name it as (for example): CheckboxTemplate:

    @using System.Globalization
    @using System.Web.Mvc.Html
    @model bool?
    
    
    @{
    bool? myModel = false;
    if (Model.HasValue)
    {
    myModel = Model.Value;
    }
    }
    
    
    <input type="checkbox" checked="@(myModel)" name="@ViewData.TemplateInfo.HtmlFieldPrefix" value="True" style="width:20px;" />
    
  2. Use it like this (in any view):

    @Html.EditorFor(x => x.MyNullableBooleanCheckboxToBe, "CheckboxTemplate")

Thats all.

Templates are so powerful in MVC, use them.. You can create an entire page as a template, which you would use with the @Html.EditorFor(); provided that you pass its view model in the lambda expression..

@{  bool testVar = ((bool)item.testVar ? true : false); }
@Html.DisplayFor(modelItem => testVar)

When making an EditorTemplate for a model which contains a nullable bool...

  • Split the nullable bool into 2 booleans:

    // Foo is still a nullable boolean.
    public bool? Foo
    {
    get
    {
    if (FooIsNull)
    return null;
    
    
    return FooCheckbox;
    }
    set
    {
    FooIsNull   = (value == null);
    FooCheckbox = (value ?? false);
    }
    }
    
    
    // These contain the value of Foo. Public only so they are visible in Razor views.
    public bool FooIsNull { get; set; }
    public bool FooCheckbox { get; set; }
    
  • Within the editor template:

    @Html.HiddenFor(m => m.FooIsNull)
    
    
    @if (Model.FooIsNull)
    {
    // Null means "checkbox is hidden"
    @Html.HiddenFor(m => m.FooCheckbox)
    }
    else
    {
    @Html.CheckBoxFor(m => m.FooCheckbox)
    }
    
  • Do not postback the original property Foo, because that is now calculated from FooIsNull and FooCheckbox.

The cleanest approach I could come up with is to expand the extensions available to HtmlHelper while still reusing functionality provided by the framework.

public static MvcHtmlString CheckBoxFor<T>(this HtmlHelper<T> htmlHelper, Expression<Func<T, bool?>> expression, IDictionary<string, object> htmlAttributes) {


ModelMetadata modelMeta = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
bool? value = (modelMeta.Model as bool?);


string name = ExpressionHelper.GetExpressionText(expression);


return htmlHelper.CheckBox(name, value ?? false, htmlAttributes);
}

I experimented with 'shaping' the expression to allow a straight pass through to the native CheckBoxFor<Expression<Func<T, bool>>> but I don't think it's possible.

All the answers above came with it's own issues. Easiest/cleanest way IMO is to create a helper

MVC5 Razor

App_Code/Helpers.cshtml

@helper CheckBoxFor(WebViewPage page, string propertyName, bool? value, string htmlAttributes = null)
{
if (value == null)
{
<div class="checkbox-nullable">
<input type="checkbox" @page.Html.Raw(htmlAttributes)>
</div>
}
else if (value == true)
{
<input type="checkbox" value="true" @page.Html.Raw(htmlAttributes) checked>
}
else
{
<input type="checkbox" value="false" @page.Html.Raw(htmlAttributes)>
}
}

Usage

@Helpers.CheckBoxFor(this, "IsPaymentRecordOk", Model.IsPaymentRecordOk)

In my scenario, a nullable checkbox means that a staff member had not yet asked the question to the client, so it's wrapped in a .checkbox-nullable so that you may style appropriately and help the end-user identify that it is neither true nor false

CSS

.checkbox-nullable {
border: 1px solid red;
padding: 3px;
display: inline-block;
}

Extension methods:

        public static MvcHtmlString CheckBoxFor<TModel>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, bool?>> expression)
{
return htmlHelper.CheckBoxFor<TModel>(expression, null);
}
public static MvcHtmlString CheckBoxFor<TModel>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, bool?>> expression, object htmlAttributes)
{
ModelMetadata metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
bool? isChecked = null;
if (metadata.Model != null)
{
bool modelChecked;
if (Boolean.TryParse(metadata.Model.ToString(), out modelChecked))
{
isChecked = modelChecked;
}
}
return htmlHelper.CheckBox(ExpressionHelper.GetExpressionText(expression), isChecked??false ,  htmlAttributes);
}

Radio buttons are useful, but they don't allow you to deselect. If you want this behaviour, then consider using a drop-down/select. The following code will generate the SelectList and this binds successfully to a nullable boolean:

public static SelectList GetBoolTriState()
{
var items = new List<SelectListItem>
{
new SelectListItem {Value = "", Text = ""},
new SelectListItem {Value = "True", Text = "Yes"},
new SelectListItem {Value = "False", Text = "No"},
};


return new SelectList(items, "Value", "Text");
}

If checked = True and not checked = null

Model

public NotNullFoo
{
get { return this.Foo?? false; }
set { this.Foo= (value == false ? null : true as bool?); }
}




public bool? Foo
{
get;
set;
}

View

  @Html.CheckBoxFor(x => Model.NotNullFoo })