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.