Wilsonhut
Deal with it or don't
Monthly Archives: September 2010
A Better ToSelectListItems
[Updated: A Betterer ToSelectListItems]
What I needed: Select lists in my ASP.NET MVC app where the options are created from data.
Suppose you have a State class and an IEnumerable of states in your ViewModel that end up looking something like so:
public class State
{
public string Name { get; set; }
public int StateId { get; set; }
}
public class AddressViewModel
{
public IEnumerable<State> StateList { get; set; }
public int SelectedStateId { get; set; }
}
(What’s the reference to State doing up in my viewModel? We’ll fix that)
Then, in your controller…
var viewModel = new AddressViewModel
{
//TODO: make this real-live data
StateList = new[]
{
new State {Name = “Alabama”, StateId = 1},
new State {Name = “Alaska”, StateId = 2},
new State {Name = “Arizona”, StateId = 3},
new State {Name = “Arkansas”, StateId = 4},
},
SelectedStateId = 2,
};
Then, in your view… (Yuk)
<%= Html.DropDownListFor(model => model.SelectedStateId,
Model.StateList.Select(state => new SelectListItem
{
Text=state.Name,
Value=state.StateId.ToString(),
Selected = state.StateId == Model.SelectedStateId
})) %>
…or…
<%= Html.DropDownListFor(model => model.SelectedStateId, new SelectList(Model.StateList, “StateId”, “Name”, Model.SelectedStateId)) %>
And then, if you want to add a blank option, it gets even worse!
I kinda liked the ToSelectListItems extension methods, like the one described in http://stackoverflow.com/questions/2389328/how-to-write-generic-ienumerableselectlistitem-extension-method (The first result when Googled)
But I wanted a couple of things different.
a) I don’t want my ViewModel and Controller to have a dependency on System.Web.Mvc.
b) I want to choose the ‘selected’ option later, in the view. Why? Because what do you do if you have Two drop-downs that use the same data? State is a perfect example. Billing Address and Shipping Address will both use the same list, and I don’t want to have two copies of SelectListItem enumerables just so that they can have two different states “selected”.
c) I wanted an easy way to conditionally add a blank option at the top.
d) I wanted even MORE type safety. (with the ToSelectListItems extension method, you have to call ‘ToString’ on the Value everytime, which leads me to my next reason)
e) I want less typing.
So Here’s what I came up with.
public class DropDownViewModel<TValue>
{
private readonly IEnumerable<DropDownItem<TValue>> _dropDownOptions;
internal DropDownViewModel(IEnumerable<DropDownItem<TValue>> dropDownOptions)
{
_dropDownOptions = dropDownOptions;
}
public IEnumerable<SelectListItem> GetSelectListItems(TValue selectedValue)
{
return GetSelectListItems(selectedValue, false);
}
public IEnumerable<SelectListItem> GetSelectListItems(TValue selectedValue, bool addEmpty)
{
if (addEmpty)
{
yield return new SelectListItem();
}
foreach (var item in _dropDownOptions)
{
yield return new SelectListItem
{
Text = item.Text,
Value = item.Value.ToString()
};
}
}
}
//Replace this class with a Tuple<string, TValue> for C# 4.
internal class DropDownItem<TValue>
{
public string Text { get; set; }
public TValue Value { get; set; }
}
public static class Extensions
{
public static DropDownViewModel<TValue> ToDropDownViewModel<T, TValue>(
this IEnumerable<T> items,
Func<T, string> textSelector,
Func<T, TValue> valueSelector)
{
return new DropDownViewModel<TValue>(items
.Select(item =>
new DropDownItem<TValue>
{
Text = textSelector(item),
Value = valueSelector(item),
}));
}
}
Now, your AddressViewModel will look like this:
public class AddressViewModel
{
public int SelectedStateId { get; set; }
public DropDownViewModel<int> StateList { get; set; }
}
…And the AddressViewModel initialization will look like this:
var viewModel = new AddressViewModel
{
//TODO: make this real-live data
StateList = new[]
{
new State {Name = “Alabama”, StateId = 1},
new State {Name = “Alaska”, StateId = 2},
new State {Name = “Arizona”, StateId = 3},
new State {Name = “Arkansas”, StateId = 4},
}.ToDropDownViewModel(s => s.Name, s=> s.StateId),
SelectedStateId = 2,
};
Notice how I can now use the s.StateId without calling ToString on it. To accomplish this, the DropDownViewModel has to have the <TValue> provided, in this case, it’s an int.
Then, the View:
<%= Html.DropDownListFor(model => model.SelectedStateId,
Model.StateList.GetSelectListItems(Model.SelectedStateId)) %>
Or if I want to allow the user to de-select the state to leave it blank, I use this overload:
<%= Html.DropDownListFor(model => model.SelectedStateId,
Model.StateList.GetSelectListItems(Model.SelectedStateId, true)) %>
If I want to use the Model.StateList more than once on a page, I’m free to do so.
Maybe an improvement would be to replace the ‘addEmpty’ boolean with a parameter for supplying the ‘Empty’ value for the blank SelectListItem. I can see where you’d want to pass Guid.Empty into that.