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" /> One<br/>
<input type="hidden" name="Items[B].Key" value="B"/>
<input type="checkbox" name="Items[B].Selected" value="true" checked /> Two<br/>
<input type="hidden" name="Items[C].Key" value="C"/>
<input type="checkbox" name="Items[C].Selected" value="true" checked /> Three<br/>
<input type="hidden" name="Items[D].Key" value="D"/>
<input type="checkbox" name="Items[D].Selected" value="true" /> 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} {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;
}
}
}
I’m used to using SubVersion with Tortoise SVN plugged into Windows Explorer and Ankh SVN as my Visual Studio version control integration. With Tortoise I can rename a folder and the change will be picked up, I can add new folders and delete folders and these changes will also be detected; and because Ankh SVN has the same features as Tortoise SVN I can do all of these things in the Visual Studio solution explorer too.
StarTeam doesn’t seem to do this. If I add a new folder in Windows Explorer I don’t expect my Visual Studio integration to pick it up, but I do expect the StarTeam client to pick it up, but it doesn’t. If I rename a folder in Visual Studio the plugin detects it, but I can’t see how to rename a folder in the StarTeam client. If I delete a folder in the Solution Explorer of Visual Studio then the plugin will register a delete operation for all of the files within the folder but not for the folder itself.
So it seems that as long as I am changing things within folders StarTeam copes okay; but if I want to delete a folder I have to remember to log into the StarTeam client, find the orphaned node in the version control project, and delete it manually. Worse still, if I add a new folder with Windows Explorer then StarTeam won’t tell me that there is a folder not in the version control view. I have to remember I added the folder (which might not happen if I have added it as part of a larger set of changes), find it’s parent node in the version control view, right-click the parent node to add the folder.
This is terrible! Version control clients are supposed to be there to save me from having to remember all my changes and manually register them with a server. In addition to this, unless I am wrong it seems that in the StarTeam client you cannot register a set of delete operations + new file operations + content changed operations in a single check-in.
Give me Subversion + TortoiseSVN + AnkhSVN any day!