Wilsonhut

Deal with it or don't

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.

Advertisements

5 responses to “A Better ToSelectListItems

  1. handcraftsman September 14, 2010 at 1:34 pm

    Much cleaner than the original.

    Now if you can separate the concerns of “getting the data” and “choosing the presentation control” you’ll be able to test the item conversion with unit tests and only need UI tests to verify the existence of the control.

    For example, if you have a convention for binding SelectedXId to XList then the UI can be simplified to:

    Model.DropDownListFor(x=>x.SelectedStateId)

    And you could still add blanks at the top with an extension like:

    .WithDefault(“”)

    This also allows you (or a non-coder) to easily change the display control to something else because getting the data into the right format isn’t the concern of the UI code anymore.

    Model.RadioButtonListFor(x=>x.SelectedStateId)

  2. oislek April 23, 2012 at 7:26 pm

    Hi, first off thanks for the effort.
    I am trying to adapt this to my viewmodels but I eventually get null reference exception when I use it in the Create view.

    My viewmodel is consisting of the following:
    public City City { get; set; }
    public DropDownViewModel StateList { get; set; }

    And City has a State.

    And when use it in my view as below, I get the error:
    @Html.DropDownListFor(model => model.City.StateID,
    Model.StateList.GetSelectListItems(Model.City.StateID,true))

    Because its a New City with no State, yet.

    Could you please direct me?

  3. wilsonhut April 30, 2012 at 2:40 am

    Is City the null reference? Maybe when the drop-down is being created, the City is still null…?

    • oislek May 1, 2012 at 7:55 am

      Yes the city is null, sorry I wanted to imply this by saying “eventually” above.
      How do use this in the “Create” view when there is no City?

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: