0

My model contains an array of zip code items (IEnumerable<SelectListItem>). It also contains an array of selected zip codes (string[]).

In my HTML page, I want to render each selected zip code as a drop down with all the zip code options. My first attempt did not work:

@foreach (var zip in Model.ZipCodes) {
    Html.DropDownList( "ZipCodes", Model.ZipCodeOptions )
}

I realized that although that would produce drop downs with the right "name" attribute, it wouldn't know which element of ZipCodes holds the value for that particular box, and might just default to the first one.

My second attempt is what really surprised me. I explicitly set the proper SelectListItem's Selected property to true, and it still rendered a control with nothing selected:

@foreach (var zip in Model.ZipCodes) {
    Html.DropDownList( "ZipCodes", Model.ZipCodeOptions.Select( x => (x.Value == zip) ? new SelectListItem() { Value = x.Value, Text = x.Text, Selected = true } : x ) )
}

There, it's returning a new IEnumerable<SelectListitem> that contains all the original items, unless it's the selected item, in which case that element is a new SelectListItem with it's Selected property set to true. That property is not honored at all in the final output.

My last attempt was to try to use an explicit index on the string element I wanted to use as the value:

@{int zipCodeIndex = 0;}
@foreach (var zip in Model.ZipCodes) {
    Html.DropDownList( "ZipCodes[" + (zipCodeIndex++) + "]", Model.ZipCodeOptions )
}

That doesn't work either, and probably because the name is no longer "ZipCodes", but "ZipCodes[x]". I also received some kind of read-only-collection error at first and had to change the type of the ZipCodes property from string[] to List<string>.

In a forth attempt, I tried the following:

@for (int zipCodeIndex = 0; zipCodeIndex < Model.ZipCodes.Count; zipCodeIndex++)
{
    var zip = Model.ZipCodes[zipCodeIndex];
    Html.DropDownListFor( x => x.ZipCodes[zipCodeIndex], Model.ZipCodeOptions )
}

That produces controls with id like "ZipCodes_1_" and names like "ZipCodes[1]", but does not select the right values. If I explicitly set the Selected property of the right item, then this works:

@for (int zipCodeIndex = 0; zipCodeIndex < Model.ZipCodes.Count; zipCodeIndex++)
{
    var zip = Model.ZipCodes[zipCodeIndex];
    Html.DropDownListFor( x => x.ZipCodes[zipCodeIndex], Model.ZipCodeOptions.Select( x => (x.Value == zip) ? new SelectListItem() { Value = x.Value, Text = x.Text, Selected = true } : x ) ) 
}

However, the problem with that approach is that if I add a new drop downs in JavaScript and give them all the name "ZipCodes", then those completely override all the explicitly indexed ones, which never make it to the server. It doesn't seem to like mixing the plain "ZipCodes" name with explicit array elements "ZipCodes[1]", even though they map to the same variable when either is used exclusively.

In the U.I., user's can click a button to add a new drop down and pick another zip code. They're all named ZipCodes, so they all get posted to the ZipCodes array. When rendering the fields in the loop above, I expect it to read the value of the property at the given index, but that doesn't work. I've even tried remapping the SelectListItems so that the proper option's "Selected" property is true, but it still renders the control with nothing selected. What is going wrong?

tereško
  • 58,060
  • 25
  • 98
  • 150
Triynko
  • 18,766
  • 21
  • 107
  • 173
  • I want all the 'select' elements to have the same name 'ZipCodes', so that I can add more dynamically without worrying about parsing and making indexed names contiguous. I think this should work out of the box as with my second attempt, but it does not, because for some reason, the DropDownList helper simply does not honor the Selected property of the SelectListItems. I think it's a bug. – Triynko Feb 29 '16 at 05:02

1 Answers1

1

The reason you first 2 snippets do not work is that ZipCodes is a property in your model, and its the value of your property which determines what is selected (not setting the selected value in the SelectList constructor which is ignored). Since the value of ZipCodes is an array of values, not a single value that matches one of the option values, a match is not found and therefore the first option is selected (because something has to be). Note that internally, the helper method generates a new IEnumerable<SelectListItem> based on the one you provided, and sets the selected attribute based on the model value.

The reason you 3rd and 4th snippets do not work, is due to a known limitation of using the DropDownListFor() method, and to make it work, you need to use an EditorTemplate and pass the SelectList to the template using AdditionalViewData, or construct a new SelectList in each iteration of the loop (as per your last attempt). Note that all it needs to be is

for(int i = 0; i < Model.ZipCodes.Length; i++)
{
    @Html.DropDownListFor(m => m.ZipCodes[i], 
        new SelectList(Model.ZipCodeOptions, "Value", "Text", Model.ZipCodes[i]))
}

If you want to use just a common name (without indexers) for each <select> element using the DropDownList() method, then it needs to be a name which does not match a model property, for example

foreach(var item in Model.ZipCodes)
{
    @Html.DropDownList("SelectedZipCodes", 
        new SelectList(Model.ZipCodeOptions, "Value", "Text", item))
}

and then add an additional parameter string[] SelectedZipCodes in you POST method to bind the values.

Alternatively, use the for loop and DropDownListFor() method as above, but include a hidden input for the indexer which allows non-zero based, non consecutive collection items to be submitted to the controller and modify you script to add new items using the technique shown in this answer

Note an example of using the EditorTemplate with AdditionalViewData is shown in this answer

Community
  • 1
  • 1