Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ASP.NET MVC: Tri-state checkbox

Tags:

asp.net-mvc

I'm just now starting to learn ASP.NET MVC. How would I go about creating a reusable tri-state checbox? In WebForms this would be a control, but I don't know the MVC equivalent.

like image 201
Jonathan Allen Avatar asked Nov 26 '10 06:11

Jonathan Allen


3 Answers

Add a TriStateCheckBox (or TriStateCheckBoxFor if you use the strongly typed overloads) extension method to HtmlHelper and add the namespace of that extension method class to the namespaces section of your web.config.

As for the implementation, I'd recommend having at look at the InputExtensions source on codeplex and using that to create your own.

like image 62
Richard Szalay Avatar answered Nov 03 '22 01:11

Richard Szalay


Limitations:

View Rendering - When rendering HTML content, there is no attribute you can possibly place on an <input type="checkbox" /> that will give it the property indeterminate.

At some point, you'll have to use JavaScript to grab the element and set the indeterminate property:

// vanilla js
document.getElementById("myChk").indeterminate = true;
// jQuery
$("#myCheck).prop("indeterminate", true);

Form Data - model binding will always be limited to what values are actually sent in the request, either from the url or the data payload (on a POST).

In this simplified example, both unchecked and indeterminate checkboxes are treated identically:

Checkbox state and post data

And you can confirm that for yourself in this Stack Snippet:

label {
  display: block;
  margin-bottom: 3px;
}
<form action="#" method="post">

  <label >
    <input type="checkbox" name="chkEmpty">  
    Checkbox
  </label>
  <label >
    <input type="checkbox" name="chkChecked" checked>  
    Checkbox with Checked 
  </label>
  <label >
    <input type="checkbox" name="chkIndeterminate" id="chkIndeterminate">  
    <script> document.getElementById("chkIndeterminate").indeterminate = true; </script>
    Checkbox with Indeterminate 
  </label>

  <label >
    <input name="RegularBool" type="checkbox" value="true">
    <input name="RegularBool" type="hidden" value="false">
    RegularBool
  </label>

 


  <input type="submit" value="submit"/>
  
</form>

Model Binding - Further, model binding will only occur on properties that are actually sent. This actually poses a problem even for regular checkboxes, since they won't post a value when unchecked. Value types do always have a default value, however, if that's the only property in your model, MVC won't new up an entire class if it doesn't see any properties.

ASP.NET solves this problem by emitting two inputs per checkbox:

Html.CheckBoxFor

Note: The hidden input guarantees that a 'false' value will be sent even when the checkbox is not checked. When the checkbox is checked, HTTP is allowed to submit multiple values with the same name, but ASP.NET MVC will only take the first instance, so it will return true like we'd expect.


Render Only Solution

We can render a checkbox for a nullable boolean, however this really only works to guarantee a bool by converting nullfalse when rendering. It is still difficult to share the indeterminate state across server and client. If you don't need to ever post back indeterminate, this is probably the cleanest / easiest implementation.


Roundtrip Solution

As there are serious limitations to using a HTML checkbox to capture and post all 3 visible states, let's separate out the view of the control (checkbox) with the tri-state values that we want to persist, and then keep them synchronized via JavsScript. Since we already need JS anyway, this isn't really increasing our dependency chain.

Start with an Enum that will hold our value:

/// <summary> Specifies the state of a control, such as a check box, that can be checked, unchecked, or set to an indeterminate state.</summary>
/// <remarks> Adapted from System.Windows.Forms.CheckState, but duplicated to remove dependency on Forms.dll</remarks>
public enum CheckState
{
    Checked,
    Indeterminate,
    Unchecked
}

Then add the following property to your Model instead of a boolean:

public CheckState OpenTasks { get; set; }

Then create an EditorTemplate for the property that will render the actual property we want to persist inside of a hidden input PLUS a checkbox control that we'll use to update that property

Views/Shared/EditorTemplates/CheckState.cshtml:

@model CheckState

@Html.HiddenFor(model => model, new { @class = "tri-state-hidden" })
@Html.CheckBox(name: "",
               isChecked: (Model == CheckState.Checked),
               htmlAttributes: new { @class = "tri-state-box" })

Note: We're using the same hack as ASP.NET MVC to submit two fields with the same name, and placing the HiddenFor value that we want to persist first so it wins. This just makes it easy to traverse the DOM and find the corresponding value, but you could use different names to prevent any possible overlap.

Then, in your view, you can render both the property + checkbox using the editor template the same way you would have used a checkbox, since it renders both. So just add this to your view:

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

The finally piece is to keep them synchronized via JavaScript on load and whenever the checkbox changes like this:

// on load, set indeterminate
$(".tri-state-hidden").each(function() {
    var isIndeterminate = this.value === "@CheckState.Indeterminate";
    if (isIndeterminate) {
        var $box = $(".tri-state-box[name='" + this.name + "'][type='checkbox']");
        $box.prop("indeterminate", true);
    }
});

// on change, keep synchronized
$(".tri-state-box").change(function () {
    var newValue = this.indeterminate ? "@CheckState.Indeterminate"
                 : this.checked ? "@CheckState.Checked"
                                : "@CheckState.Unchecked";

    var $hidden = $(".tri-state-hidden[name='" + this.name + "'][type='hidden']");
    $hidden.val(newValue);
});

Then you can use however you'd like in your business model. For example, if you wanted to map to a nullable boolean, you could use the CheckState property as a backing value and expose/modify via getters/setters in a bool? like this:

public bool? OpenTasksBool
{
    get
    {
        if (OpenTasks == CheckState.Indeterminate) return null;
        return OpenTasks == CheckState.Checked;
    }
    set
    {
        switch (value)
        {
            case null: OpenTasks = CheckState.Indeterminate; break;
            case true: OpenTasks = CheckState.Checked; break;
            case false: OpenTasks = CheckState.Unchecked; break;
        }
    } 
}

Alternative Solution

Also, depending on your domain model, you could just use Yes, No, ⁿ/ₐ radio buttons

like image 24
KyleMit Avatar answered Nov 03 '22 00:11

KyleMit


ASP.NET MVC certainly doesn't provide such component, actually it simply relies on the standard elements available in HTML but you may want to check out this solution.

like image 22
Denis Ivin Avatar answered Nov 03 '22 01:11

Denis Ivin