10

CheckBoxFor(t => t.boolValue, new { disabled="disabled" }) method to render a checkbox, in disabled mode.

The method renders a hidden field as well.

My question is why does this hidden field has a false value for disabled check box? I believe the purpose of the hidden field is to have some extra behavior over the default check box behavior

Is there a way to override default MVC functionality so that the value of this hidden field is based on the state of the checkbox even in disabled mode?

Shameet
  • 113
  • 1
  • 1
  • 8

4 Answers4

14

The hidden field is used to bind the checkbox value to a boolean property. The thing is that if a checkbox is not checked, nothing is sent to the server, so ASP.NET MVC uses this hidden field to send false and bind to the corresponding boolean field. You cannot modify this behavior other than writing a custom helper.

This being said, instead of using disabled="disabled" use readonly="readonly" on the checkbox. This way you will keep the same desired behavior that the user cannot modify its value but in addition to that its value will be sent to the server when the form is submitted:

@Html.CheckBoxFor(x => x.Something, new { @readonly = "readonly" })

UPDATE:

As pointed out in the comments section the readonly attribute doesn't work with Google Chrome. Another possibility is to use yet another hidden field and disable the checkbox:

@Html.HiddenFor(x => x.Something)
@Html.CheckBoxFor(x => x.Something, new { disabled = "disabled" })

UPDATE 2:

Here's a full testcase with the additional hidden field.

Model:

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

Controller:

public class HomeController : Controller
{
    public ActionResult Index()
    {
        return View(new MyViewModel { Foo = true });
    }

    [HttpPost]
    public ActionResult Index(MyViewModel model)
    {
        return Content(model.Foo.ToString());
    }
}

View:

@model MyViewModel

@using (Html.BeginForm())
{
    @Html.HiddenFor(x => x.Foo)
    @Html.CheckBoxFor(x => x.Foo, new { disabled = "disabled" })
    <button type="submit">OK</button>
}

When the form is submitted the value of the Foo property is true. I have tested with all major browsers (Chrome, FF, IE).

Darin Dimitrov
  • 1,023,142
  • 271
  • 3,287
  • 2,928
  • Well, the thing is readonly does not work with checkboxes. Atleast in chrome. Yes you are right about the need for hidden field for unchecked check box. My wish is that same hidden field can be used to handle the disabled part, considering readonly does not work. Which browser have you tried readonly with? – Shameet Jun 13 '12 at 09:47
  • @Shameet, oh yes, you are absolutely right. Readonly doesn't work with checkboxes. I have updated my answer with another suggestion. – Darin Dimitrov Jun 13 '12 at 09:52
  • Darin, Having an extra hidden with the same property name does not work. The model binder still sets the property to false. Having another property (with its hidden) to handle disabled will work but with lot of code to handle. I think writing own templated view for boolean will be an elegant solution?? – Shameet Jun 13 '12 at 10:19
  • @Shameet, the additional hidden field works for me. Let me update my answer to provide my full test case. This could of course be externalized into a custom editor template so that all you have to do in your view is: `@Html.EditorFor(x => x.Foo)`. – Darin Dimitrov Jun 13 '12 at 11:08
  • AFAIK, this would generate multiple input controls with the same name, and they all will be posted back. Is model binder just picking up the first one? If yes, would that solution still work if checkbox is conditionally (only sometimes) disabled? – Sebastian K Mar 21 '13 at 19:16
  • I guess it would if we render our explicit hidden field only when checkbox is disabled. – Sebastian K Mar 21 '13 at 19:22
  • It's worth noting that this behavior of including a hidden field for the CheckBox helper method will cause problems if you override your form to use a `GET` instead of a `POST`. In this case the only work around I've found is to create the checkbox using plain old HTML, the checkbox helper methods are not compatible with forms that submit using `GET`. – Nick Albrecht Jun 13 '13 at 16:02
3

I find the added parameter in the query string looks a mess and makes people think that we as developers have done something wrong. So I've resorted to the extreme method of creating my own InputExtensions class which allows me to decide on whether I want the hidden input to be rendered or not.

The following is the InputExtensions class I've created (based on the existing MVC code to maintain full compatibility):

public static class InputExtensions
{
    //
    // Checkboxes
    //

    public static MvcHtmlString CheckBoxFor<TModel>(this HtmlHelper<TModel> htmlHelper,
        Expression<Func<TModel, bool>> expression, bool renderHiddenInput)
    {
        if (renderHiddenInput)
        {
            return System.Web.Mvc.Html.InputExtensions.CheckBoxFor(htmlHelper, expression);
        }
        return CheckBoxFor(htmlHelper, expression, false);
    }

    public static MvcHtmlString CheckBoxFor<TModel>(this HtmlHelper<TModel> htmlHelper,
        Expression<Func<TModel, bool>> expression, object htmlAttributes, bool renderHiddenInput)
    {
        if (renderHiddenInput)
        {
            return System.Web.Mvc.Html.InputExtensions.CheckBoxFor(htmlHelper, expression, htmlAttributes);
        }
        return CheckBoxFor(htmlHelper, expression, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes), false);
    }

    public static MvcHtmlString CheckBoxFor<TModel>(this HtmlHelper<TModel> htmlHelper,
        Expression<Func<TModel, bool>> expression, IDictionary<string, object> htmlAttributes,
        bool renderHiddenInput)
    {
        if (renderHiddenInput)
        {
            return System.Web.Mvc.Html.InputExtensions.CheckBoxFor(htmlHelper, expression, htmlAttributes);
        }

        if (expression == null)
        {
            throw new ArgumentNullException("expression");
        }

        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 CheckBoxHelper(htmlHelper, metadata, ExpressionHelper.GetExpressionText(expression), isChecked, htmlAttributes);
    }

    private static MvcHtmlString CheckBoxHelper(HtmlHelper htmlHelper, ModelMetadata metadata, string name, bool? isChecked, IDictionary<string, object> htmlAttributes)
    {
        RouteValueDictionary attributes = ToRouteValueDictionary(htmlAttributes);

        bool explicitValue = isChecked.HasValue;
        if (explicitValue)
        {
            attributes.Remove("checked"); // Explicit value must override dictionary
        }

        return InputHelper(htmlHelper,
                           InputType.CheckBox,
                           metadata,
                           name,
                           value: "true",
                           useViewData: !explicitValue,
                           isChecked: isChecked ?? false,
                           setId: true,
                           isExplicitValue: false,
                           format: null,
                           htmlAttributes: attributes);
    }

    //
    // Helper methods
    //

    private static MvcHtmlString InputHelper(HtmlHelper htmlHelper, InputType inputType, ModelMetadata metadata, string name, object value, bool useViewData, bool isChecked, bool setId, bool isExplicitValue, string format, IDictionary<string, object> htmlAttributes)
    {
        string fullName = htmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(name);
        if (string.IsNullOrEmpty(fullName))
        {
            throw new ArgumentException("Value cannot be null or empty.", "name");
        }

        var tagBuilder = new TagBuilder("input");
        tagBuilder.MergeAttributes(htmlAttributes);
        tagBuilder.MergeAttribute("type", HtmlHelper.GetInputTypeString(inputType));
        tagBuilder.MergeAttribute("name", fullName, true);

        string valueParameter = htmlHelper.FormatValue(value, format);
        var usedModelState = false;

        bool? modelStateWasChecked = GetModelStateValue(htmlHelper.ViewData, fullName, typeof(bool)) as bool?;
        if (modelStateWasChecked.HasValue)
        {
            isChecked = modelStateWasChecked.Value;
            usedModelState = true;
        }
        if (!usedModelState)
        {
            string modelStateValue = GetModelStateValue(htmlHelper.ViewData, fullName, typeof(string)) as string;
            if (modelStateValue != null)
            {
                isChecked = string.Equals(modelStateValue, valueParameter, StringComparison.Ordinal);
                usedModelState = true;
            }
        }
        if (!usedModelState && useViewData)
        {
            isChecked = EvalBoolean(htmlHelper.ViewData, fullName);
        }
        if (isChecked)
        {
            tagBuilder.MergeAttribute("checked", "checked");
        }
        tagBuilder.MergeAttribute("value", valueParameter, isExplicitValue);

        if (setId)
        {
            tagBuilder.GenerateId(fullName);
        }

        // If there are any errors for a named field, we add the css attribute.
        ModelState modelState;
        if (htmlHelper.ViewData.ModelState.TryGetValue(fullName, out modelState))
        {
            if (modelState.Errors.Count > 0)
            {
                tagBuilder.AddCssClass(HtmlHelper.ValidationInputCssClassName);
            }
        }

        tagBuilder.MergeAttributes(htmlHelper.GetUnobtrusiveValidationAttributes(name, metadata));

        return MvcHtmlString.Create(tagBuilder.ToString(TagRenderMode.SelfClosing));
    }

    private static RouteValueDictionary ToRouteValueDictionary(IDictionary<string, object> dictionary)
    {
        return dictionary == null ? new RouteValueDictionary() : new RouteValueDictionary(dictionary);
    }

    private static object GetModelStateValue(ViewDataDictionary viewData, string key, Type destinationType)
    {
        ModelState modelState;
        if (viewData.ModelState.TryGetValue(key, out modelState))
        {
            if (modelState.Value != null)
            {
                return modelState.Value.ConvertTo(destinationType, culture: null);
            }
        }
        return null;
    }

    private static bool EvalBoolean(ViewDataDictionary viewData, string key)
    {
        return Convert.ToBoolean(viewData.Eval(key), CultureInfo.InvariantCulture);
    }
}

Then you can call the method like so:

@Html.CheckBoxFor(t => t.boolValue, new { disabled="disabled" }, false)
Professor of programming
  • 2,978
  • 3
  • 30
  • 48
2

Here is a quick tip I discovered : If you set a default value for the boolean value on the object. It's possible to write out the tag with the value as true/false (this value should be the opposite of the default boolean value on the object)

<input type="checkbox" name="IsAllDayEvent" value="true" id="IsAllDayEvent" />

The value is only sent if the checkbox is checked, so it works.

0

Although Darin's answer is perfectly fine, there is another way of making sure that disabled checkbox post backs as expected.

This solution will also work in the scenarios where checkbox is conditionally disabled based on a property in a model, or enabled/disabled client-side based on user selection or client-side retrieved data.

You can keep your view simple: (setting disabled here is optional)

@Html.CheckBoxFor(m => m.Something, new { id = "somethingCheckbox", disabled = "disabled" })

This will, as Darin mentioned generate an extra hidden input field set to false, so that the checkbox value will post-back also when checkbox is not checked. Therefore we know that Something field will always post-back regardless if checkbox is checked or not, disabled or not. The only problem that if checkbox is disabled it will always post back as False.

We can fix that with some javaScript post processing before submitting. In my case the code looks something along those lines:

var dataToPost = $form.serializeArray();

if (dataToPost != null) {
    for (var i = 0; i < dataToPost.length; i++) {
        if (dataToPost[i].name === "Something") {
            dataToPost[i].value = $("#somethingCheckbox").attr("checked") === "checked";
            }
        }
    }  
$.post($form.attr("action"), dataToPost)

You may need to modify it for your scenario, but you should get the idea.

Sebastian K
  • 6,235
  • 1
  • 43
  • 67