I'm new to MVC, but I've been all over this, read all the documentation and all the questions and all the blog posts I can find, and all I'm doing is getting completely wrapped around the axle.
I'm trying to make a "create" Action and View. My data entry is relatively straight forward, and common: I have a drop down list and a text box. In my case, I'm creating a user contact channel, and the drop down box chooses between email and textmsg, and the text box then enters the relevant contact information, either a well formed email address, or a mobile phone number.
Here's a (slightly simplified form of) my View page:
<tr>
<td><%= Html.DropDownList("ChannelDescription", Model.ChannelDescription, "Select a Channel", new { id = "ChannelDDL", onchange="ChannelDDLChanged()" })%>
<br />
<%= Html.ValidationMessage("ChannelDescription", "Please Select a Channel") %>
</td>
<td>
<%= Html.TextBox("SubscriberNotificationAddr") %> <br />
<%= Html.ValidationMessage("SubscriberNotificationAddr", "Please enter a contact address or number") %>
</td>
</tr>
I'm using a strongly typed ViewData model, rather than using the ViewDataDictionary. The ChannelDescription element is a SelectList, which is initialized with the list of choices and no selection.
The initial display of the form, the data entry into the form, and the extraction of the data from the form by the controller goes fine.
My problem is if the data contains an error, such as a mal-formed email address or cell phone number, and I have to return to the view, I have not been successful in getting the drop down list selection redisplayed. The ChannelDescription element is recreated in the controller with the user's choice as the selected item. I have set breakpoints on that line of the View, and verified that the selected element of the list of items has the Selected property set to true, but it still displays the default "Select a Channel".
This seems like it would be a very common situation, and shouldn't be this hard. What am I doing wrong?
FYI, this is with MVC 1.0 (Release), Windows 7, and VS 2008, running under Firefox 3.5.2.
After viewing the answer above, I wanted to check it out, because all the examples I had seen had, indeed, used ViewDataDictionary, rather than a strongly typed ViewDataModel.
So I did some experiments. I constructed a very simple view that used a plain ViewDataDictionary, and passed values in by named keys. It persisted the selected item just fine. Then I cut and pasted that View (and controller) to another one, changing only what was necessary to switch to a strongly typed ViewData Model. Lo, and behold, it also persisted the selected item.
So what else was different between my simple test and my application? In my test, I had used simply "Html.DropDownList("name", "optionLabel")". However, in my application, I had needed to add HTML attributes, and the only overloads available that included HtmlAttributes also include the select List.
It turns out that the DropDownList overload with a select list parameter is broke! Looking at the downloaded MVC source code, when DropDownList is called with just a name, or a name and an optionLabel, it ends up retrieving the target select list from the ViewData, and then invoking the private SelectInternal method by the following call:
return SelectInternal(htmlHelper, optionLabel, name, selectList, true /* usedViewData */, false /* allowMultiple */, (IDictionary<string, object>)null /* htmlAttributes */);
However, if it's called with a selectList parameter, it ends up with the following:
return SelectInternal(htmlHelper, optionLabel, name, selectList, false /* usedViewData */, false /* allowMultiple */, htmlAttributes);
The difference is that in the first one (which will work correctly) the "usedViewData" parameter is true, while in the second one, it is false. Which is actually okay, but exposes an internal defect in the SelectInternal routine.
If usedViewData is false, it gets a object variable "defaultValue" from the ViewData model.
However, defaultValue is used as though it is either a string or an array of strings, when, in fact what is returned from the ViewData is a SelectList. (IEnumerable<SelectListItem>
).
If usedViewData is true, then defaultValue will be either null or a string.
Then if defaultValue is not null, it ends up going into a block of code which contains this:
foreach (SelectListItem item in selectList) {
item.Selected = (item.Value != null) ? selectedValues.Contains(item.Value) : selectedValues.Contains(item.Text);
newSelectList.Add(item);
selectList is the original selectList that was passed in, so the item is a SelectListItem (string Text, string Value, and bool Selected). But selectedValues was derived from the defaultValue, and becomes a List of SelectLists, not a List of strings. So for each of the items, it's setting the Selected flag based on whether the selectedValues list "Contains" the item.Value. Well, a List of SelectLists is never going to "Contain" a string, so the item.Selected never gets set. (Correction: actually, after more tracing with the debugger, I found that selectedValues is derived from the defaultValue by a "ToString()" call. So it actually is a list of strings, but instead of containing the values that we want, it contains "System.Web.Mvc.SelectList" - the result of applying "ToString()" to complex object like a SelectList. The result is still the same - we're not going to find the value we're looking for in that list.)
It then substitutes the newly constructed "newSelectList" for the original "selectList", and proceeds to build the HTML from it.
As cagdas (I apologize for butchering your name, but I don't know how to make those characters on my US Keyboard) said above, I think I'll have to build my own method to use in place of the DropDownList HtmlHelper. I guess since this release 1 and Release2 is in Beta 2, we can't really expect any bug fixes unless we do it ourselves right?
BTW, if you've followed me this far, this code is in src\SystemWebMvc\Mvc\Html\SelectExtensions.cs, at around line 116-136
I had some discussions with Brad Wilson, from the MVC team, and he explained to me that I was misunderstanding how the DropDownList helper method should be used (a misunderstanding that I think might be fairly common, from what I've read).
Basically, EITHER give it the SelectList in the named parameter of the ViewModel, and let it build the drop down list from that with the appropriate items selected, OR give it the SelectList as a separate parameter and let the named parameter of the ViewModel be just the value strings for the selected item(s). If you give it a SelectList parameter, then it expects the named value to be a string or list of strings, NOT a SelectList.
So, now your ViewModel ends up having two elements for one conceptual item in the view (the dropdown list). Thus, you might have a model that has
string SelectedValue {get; set;}
SelectList DropDownElements { get; set;}
Then you can pre-populate the DropDownElements with the choices, but in your model view binding, you just need to deal with SelectedValue element. It seems to work pretty well for me when I do it that way.
Yes, I too had so many problems getting DropDownList to respect the selected item I've given to it.
Please check my answer in this question. As far as I can remember, that was the only way I could get it to work. By passing the list via ViewData.
FYI, I stopped using that HtmlHelper method. I'm now simply outputting the <select>
and <option>
tags myself with a loop and setting the selected
property of the option
tag by checking it myself.