views:

813

answers:

4

I am trying to formulate a work-around for the lack of a "checkbox group" in ASP.NET MVC. The typical way to implement this is to have check boxes of the same name, each with the value it represents.

<input type="checkbox" name="n" value=1 />
<input type="checkbox" name="n" value=2 />
<input type="checkbox" name="n" value=3 />

When submitted, it will comma delimit all values to the request item "n".. so Request["n"] == "1,2,3" if all three are checked when submitted. In ASP.NET MVC, you can have a parameter of n as an array to accept this post.

public ActionResult ActionName( int[] n ) { ... }

All of the above works fine. The problem I have is that when validation fails, the check boxes are not restored to their checked state. Any suggestions.

Problem Code: (I started with the default asp.net mvc project)

Controller

    public class HomeController : Controller
    {
        public ActionResult Index()
        {   var t = getTestModel("First");
            return View(t);
        }

        [AcceptVerbs(HttpVerbs.Post)]
        public ActionResult Index(TestModelView t)
        {   if(String.IsNullOrEmpty( t.TextBoxValue))
                ModelState.AddModelError("TextBoxValue", "TextBoxValue required.");
            var newView = getTestModel("Next");
            return View(newView);
        }

        private TestModelView getTestModel(string prefix)
        {   var t = new TestModelView();
            t.Checkboxes = new List<CheckboxInfo>()
            {   new CheckboxInfo(){Text = prefix + "1", Value="1", IsChecked=false},
                new CheckboxInfo(){Text = prefix + "2", Value="2", IsChecked=false} 
            };
            return t;
        }
    }
    public class TestModelView
    {   public string TextBoxValue { get; set; }
        public List<CheckboxInfo> Checkboxes { get; set; }
    }
    public class CheckboxInfo
    {   public string Text { get; set; }
        public string Value { get; set; }
        public bool IsChecked { get; set; }
    }
}

ASPX

<%
using( Html.BeginForm() ){ 
%>  <p><%= Html.ValidationSummary() %></p>
    <p><%= Html.TextBox("TextBoxValue")%></p>
    <p><%  
    int i = 0;
    foreach (var cb in Model.Checkboxes)
    { %>
        <input type="checkbox" name="Checkboxes[<%=i%>]" 
            value="<%= Html.Encode(cb.Value) %>" <%=cb.IsChecked ? "checked=\"checked\"" : String.Empty %> 
            /><%= Html.Encode(cb.Text)%><br />
    <%      i++;
    } %></p>
    <p><input type="submit" value="submit" /></p>
<%
}
%>

Working Code

Controller

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Index(TestModelView t)
{
    if(String.IsNullOrEmpty( t.TextBoxValue))
    {   ModelState.AddModelError("TextBoxValue", "TextBoxValue required.");
        return View(t); 
    }
    var newView = getTestModel("Next");
    return View(newView);
}

ASPX

int i = 0;
foreach (var cb in Model.Checkboxes)
{ %>
    <input type="checkbox" name="Checkboxes[<%=i%>].IsChecked" <%=cb.IsChecked ? "checked=\"checked\"" : String.Empty %> value="true" />
    <input type="hidden"   name="Checkboxes[<%=i%>].IsChecked" value="false" />
    <input type="hidden" name="Checkboxes[<%=i%>].Value" value="<%= cb.Value %>" />
    <input type="hidden" name="Checkboxes[<%=i%>].Text" value="<%= cb.Text %>" />
    <%= Html.Encode(cb.Text)%><br />
<%      i++;
} %></p>
<p><input type="submit" value="submit" /></p>

Of course something similar could be done with Html Helpers, but this works.

+1  A: 

I don't know how to solve your problem, but you could define your checkboxes with this code:

<%= Html.CheckBox("n[0]") %><%= Html.Hidden("n[0]",false) %>
<%= Html.CheckBox("n[1]") %><%= Html.Hidden("n[1]",false) %>
<%= Html.CheckBox("n[2]") %><%= Html.Hidden("n[2]",false) %>

Hidden fields are needed, because if checkbox is not checked, form doesn't send any value. With hidden field it sends false.

Your post method will be:

[HttpPost]
public ActionResult Test(bool[] n)
{
    return View();
}

It may not be optimal and I am open to comments, but it works:)

EDIT

Extended version:

<%= Html.CheckBox("n[0].Checked") %><%= Html.Hidden("n[0].Value",32) %><%= Html.Hidden("n[0].Checked",false) %>
<%= Html.CheckBox("n[1].Checked") %><%= Html.Hidden("n[1].Value",55) %><%= Html.Hidden("n[1].Checked",false) %>
<%= Html.CheckBox("n[2].Checked") %><%= Html.Hidden("n[2].Value",76) %><%= Html.Hidden("n[2].Checked",false) %>

Your post method will be:

[HttpPost]
public ActionResult Test(CheckedValue[] n)
{
    return View();
}

public class CheckedValue
{
     public bool Checked { get; set; }
     public bool Value { get; set; }
}

I wrote it without VS, so it may need little correction.

LukLed
I could do something to that effect, though I would have to bind that to the value list. As in the examples in the question, I need to know that [0] is 1 or in the real case, 79 or whatever. I suppose I could handle that with storing the value list is session.
Greg Ogle
Bingo! good thinking. I had forgotten a couple things... 1. to reference .Value and 2. that when you have any part of an array with a posted value, the binder will create the object at the position. If you skip an index, it won't create any past the skip.
Greg Ogle
A: 

Well... the check boxes aren't going to know their state on their own, especially if you are not using the Html.CheckBox helper (if you are, see LuKLed's answer). You're going to have to put the checked state of each box in your ViewData (or Model) and then perform a look-up in your View in one way or another.

Warning: Really ugly proof-of-concept code:

Controller:

//validation fails
ViewData["checkboxn"] = n;
return View();

View:

<% int[] n = (int[])ViewData["checkboxn"]; %>
<input type="checkbox" name="n" value=1 <%= n != null && n.Contains(1) ? "checked=\"checked\"" : "" %> />
<input type="checkbox" name="n" value=2 <%= n != null && n.Contains(2) ? "checked=\"checked\"" : "" %> />
<input type="checkbox" name="n" value=3 <%= n != null && n.Contains(3) ? "checked=\"checked\"" : "" %> />

All I'm doing here is passing the array n back to the view, and if it contains a value for the respective checkbox, adding checked="checked" to the element.

You would probably want to refactor this into an HtmlHelper of your own, or otherwise make this less ugly, of course.

Kurt Schindler
+1  A: 

Behold the final solution:

public static class Helpers
{
    public static string CheckboxGroup<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> propertySelector, int value) where TProperty: IEnumerable<int>
    {
        var groupName = GetPropertyName(propertySelector);
        var modelValues = propertySelector.Compile().Invoke(htmlHelper.ViewData.Model);

        var svalue = value.ToString();
        var builder = new TagBuilder("input");
        builder.GenerateId(groupName);
        builder.Attributes.Add("type", "checkbox");
        builder.Attributes.Add("name", groupName);
        builder.Attributes.Add("value", svalue);
        var contextValues = HttpContext.Current.Request.Form.GetValues(groupName);
        if ((contextValues != null && contextValues.Contains(svalue)) || (modelValues != null && modelValues.Contains(value)))
        {
            builder.Attributes.Add("checked", null);
        }
        return builder.ToString(TagRenderMode.Normal);
    }

    private static string GetPropertyName<T, TProperty>(Expression<Func<T, TProperty>> propertySelector)
    {
        var body = propertySelector.Body.ToString();
        var firstIndex = body.IndexOf('.') + 1;
        return body.Substring(firstIndex);
    }
}

And in your view:

                        <%= Html.CheckboxGroup(model => model.DocumentCoverCustom, "1")%>(iv),<br />
                        <%= Html.CheckboxGroup(model => model.DocumentCoverCustom, "2")%>(vi),<br />
                        <%= Html.CheckboxGroup(model => model.DocumentCoverCustom, "3")%>(vii),<br />
                        <%= Html.CheckboxGroup(model => model.DocumentCoverCustom, "4")%>(ix)<br />
Hans
A: 

This solution may be of interest to those wanting a clean/simple approach: http://stackoverflow.com/questions/3291501/asp-net-mvc-maintain-state-of-a-dynamic-list-of-checkboxes/3298821#3298821

I wouldn't really recommend the use of Html.CheckBox unless you have a super simple, single checkbox bound to a single bool (or a couple of static ones at most). When you start having lists of checkboxes in a single array or dynamic checkboxes, it is difficult to work with and you end up programming the whole world in server side bloat just to deal with the shortfalls and get everything working. Forget it, and just use the clean HTML focused solution above and you're up and running quickly with less mess to maintain in the future.

Aaron