2012-08-29

ASP MVC CheckListBox

I needed to present the user with a list of objects from which they could select multiple items.  There is a MultiSelectList class in ASP MVC so I looked into how to use that.  It would seem that to use this class we need to use Html.ListBox.  I think this is a poor choice because it requires the user to hold down the Control key to select additional options, and it is too easy to deselect all of your values accidentally by clicking the control accidentally without the Control key held down.

What I really wanted was something like a CheckListBox, a list of items with a check box next to them, so that’s what I have implemented.  Here is an example of how to set up the view data for my CheckListBox extension.

public ActionResult Index()
{
var availableItems
= new List<MyItem>();
availableItems.Add(
new MyItem("A", "One"));
availableItems.Add(
new MyItem("B", "Two"));
availableItems.Add(
new MyItem("C", "Three"));
availableItems.Add(
new MyItem("D", "Four"));
var selectedItems
= availableItems.Skip(1).Take(2);
ViewData[
"Items"] = CheckListBoxItems.Create(
availableItems,
x
=> x.Code,
x
=> x.Name,
x
=> selectedItems.Contains(x));
return View();
}

[HttpPost]
public ActionResult Index(CheckListBoxItems items)
{
return Index();
}


Make sure that when your application starts you register the custom binder for CheckListBoxItems



protected void Application_Start()
{
...
ModelBinders.Binders.Add(
typeof(CheckListBoxItems),
new CheckListBoxItemsModelBinder());
}


Here is the first example of how you can mark up your view html.



<%: Html.CheckListBox("Items", (CheckListBoxItems)ViewData["Items"]) %>
<!-- Outputs the following HTML
<input type="hidden" name="Items[A].Key" value="A"/>
<input type="checkbox" name="Items[A].Selected" value="true" />&nbsp;One<br/>
<input type="hidden" name="Items[B].Key" value="B"/>
<input type="checkbox" name="Items[B].Selected" value="true" checked />&nbsp;Two<br/>
<input type="hidden" name="Items[C].Key" value="C"/>
<input type="checkbox" name="Items[C].Selected" value="true" checked />&nbsp;Three<br/>
<input type="hidden" name="Items[D].Key" value="D"/>
<input type="checkbox" name="Items[D].Selected" value="true" />&nbsp;Four<br/>
-->


And here is another example where you can pass in the HTML to use for each item in the list.  The HTML is just a string used in String.Format where {0} is the check box html and {1} is where the text will be displayed.  In the following example I pass the parameters in in the order 1,0 because I want the text first followed by the check box control.



<table>
<tr>
<th>Item</th>
<th>Selected</th>
</tr>
<%: Html.CheckListBox(
"Items",
(CheckListBoxItems)ViewData[
"Items"],
"<tr><td>{1}</td><td>{0}</td></tr>") %>
</table>
<!-- Outputs the following HTML
<table>
<tr>
<th>Item</th>
<th>Selected</th>
</tr>
<tr>
<td>One</td>
<td>
<input type="hidden" name="Items[A].Key" value="A"/>
<input type="checkbox" name="Items[A].Selected" value="true" />
</td>
</tr>
<tr>
<td>Two</td>
<td>
<input type="hidden" name="Items[B].Key" value="B"/>
<input type="checkbox" name="Items[B].Selected" value="true" checked />
</td>
</tr>
<tr>
<td>Three</td>
<td>
<input type="hidden" name="Items[C].Key" value="C"/>
<input type="checkbox" name="Items[C].Selected" value="true" checked />
</td>
</tr>
<tr>
<td>Four</td>
<td>
<input type="hidden" name="Items[D].Key" value="D"/>
<input type="checkbox" name="Items[D].Selected" value="true" />
</td>
</tr>
</table>
-->


Here is the source code:



//CheckListBoxItem.cs
using System.Collections.Generic;

namespace System.Web.Mvc
{
public class CheckListBoxItem
{
public string Key { get; set; }
public string Text { get; set; }
public bool Selected { get; set; }
}

public class CheckListBoxItems : List
<CheckListBoxItem>
{
public static CheckListBoxItems Create
<T>(
IEnumerable
<T> source,
Func
<T, string> key,
Func
<T, string> text,
Func
<T, bool> selected)
{
var result = new CheckListBoxItems();
foreach (T item in source)
{
CheckListBoxItem newItem = new CheckListBoxItem();
result.Add(newItem);
newItem.Key = key(item);
newItem.Text = text(item);
newItem.Selected = selected(item);
}
return result;
}
}
}

//CheckListBoxItems.cs
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace System.Web.Mvc.Html
{
public static class CheckListBoxExtensions
{
public static MvcHtmlString CheckListBox(this HtmlHelper htmlHelper, string name, IEnumerable
<CheckListBoxItem> items)
{
return htmlHelper.CheckListBox(name, items, "{0}
&nbsp;{1}<br/>");
}

public static MvcHtmlString CheckListBox(this HtmlHelper htmlHelper, string name, IEnumerable
<CheckListBoxItem> items, string itemFormat)
{
name = htmlHelper.Encode(name);
var resultBuilder = new StringBuilder();
var itemList = items.ToList();
for (int index = 0; index
< itemList.Count; index++)
{
CheckListBoxItem item
= itemList[index];
string encodedKey = htmlHelper.Encode(item.Key);
string encodedText = htmlHelper.Encode(item.Text);
string keyHtml =
string.Format("<input
type=\"hidden\" name=\"{0}[{1}].Key\" value=\"{2}\"/>", name, encodedKey, encodedKey);
string checkBoxHtml =
string.Format(
"
<input type=\"checkbox\" name=\"{0}[{1}].Selected\" value=\"true\" {2} />",
name, encodedKey, item.Selected ? "checked" : "");
resultBuilder.AppendFormat(itemFormat, keyHtml + checkBoxHtml, encodedText);
}
return MvcHtmlString.Create(resultBuilder.ToString());
}
}
}

//CheckListBoxItemsModelBinder.cs
using System.Linq;
using System.Text.RegularExpressions;

namespace System.Web.Mvc
{
public class CheckListBoxItemsModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var result = new CheckListBoxItems();
string modelName = bindingContext.ModelName;
string regexKeyPattern = "^" + modelName + @"\[.+?\]\.Key$";
var keyRegex = new Regex(regexKeyPattern, RegexOptions.IgnoreCase);
var keys = controllerContext.RequestContext.HttpContext.Request.Form.AllKeys.Where(x => keyRegex.IsMatch(x));
foreach (string key in keys)
{
var valueSubmittedForKey = bindingContext.ValueProvider.GetValue(key);
bindingContext.ModelState.SetModelValue(key, valueSubmittedForKey);

string valueKey = key.Substring(0, key.Length - 4) + ".Selected";
var valueSubmittedForValueKey = bindingContext.ValueProvider.GetValue(valueKey);
bindingContext.ModelState.SetModelValue(valueKey, valueSubmittedForValueKey);

var checkListBoxItem = new CheckListBoxItem();
result.Add(checkListBoxItem);
checkListBoxItem.Key = valueSubmittedForKey.AttemptedValue;
checkListBoxItem.Selected = valueSubmittedForValueKey != null;
}
return result;
}
}
}

No comments: