Reputation: 7435
I'm experimenting with custom ModelMetadataProvider. It would appear that some of the html helpers like TextBoxFor use these just fine. However, in other cases like DropDownListFor, they favor ViewData instead. For example, looking at some reflected code I see:
bool flag = false;
if (selectList == null)
{
selectList = SelectExtensions.GetSelectData(htmlHelper, name);
flag = true;
}
object defaultValue = allowMultiple ? htmlHelper.GetModelStateValue(fullHtmlFieldName, typeof (string[])) : htmlHelper.GetModelStateValue(fullHtmlFieldName, typeof (string));
if (defaultValue == null && !string.IsNullOrEmpty(name))
{
if (!flag)
defaultValue = htmlHelper.ViewData.Eval(name);
else if (metadata != null)
defaultValue = metadata.Model;
}
Note all the different attempts to get "defaultValue". Using metadata.Model is dead last. Why the separation here? If you trace that code thru you eventually end up at a call to ViewData.Eval, which as a fall back just uses reflection to get the value out of the model anyway. Is there such a thing as a custom ViewData provider to bridge that gap?
EDIT: I'm beginning to lean toward the idea that this is a bug in the framework.
Consider two pieces of code:
@Html.DropDownListFor(model => model.ErrorData.Shift, Model.ShiftOptions, new { @class = "form-control" })
The code above passes in the options "Model.ShiftOptions". Because of this it doesn't pass the condition "selectList==null" and consequently "flag" is never set and instead proceeds to try to get the default value from only the type via reflection (the Eval call).
However with this code:
@{ ViewData[Html.NameFor(m => m.ErrorData.Shift).ToString()] = Model.ShiftOptions;}
@Html.DropDownListFor(model => model.ErrorData.Shift,null, new { @class = "form-control" })
..."flag" is now satisfied and the default value is now retrieved metadata.Model. Why would different mechanisms for providing the list options change (or even influence for that matter) where the default value is retrieved from?
Edit #2
Warning: The above ViewData "fix" does not work if the DropDownListFor is called in an editor template (EditorFor) for a complex type. The NameFor call will return the name of the property INCLUDING the outer context that the EditorFor was called from, ie MyViewModel.ErrorData.Shift. However, the code for DropDownListFor in the orginal snip at the top looks for a ViewData item WITHOUT the original context, ie ErrorData.Shift. They both use
ExpressionHelper.GetExpressionText((LambdaExpression) expression)
However, NameOf uses html.Name on that result. When DDLF finally gets around to generating its name, it does something similar so it's name is correct, but it makes no sense that it doesn't include it's full context when looking for a view data option.
Upvotes: 0
Views: 324
Reputation:
All the HtmlHelper
methods for generating form controls first check if there is a value for the property in ModelState
(the GetModelStateValue()
method) to handle the case where the form has been submitted with an invalid value and the view is returned (refer the 2nd part of this answer for an explanation of why this is the default behavior).
In the case where you use DropDownList()
, for example
@Html.DropDownList("xxx", null, "--Please select--")
where xxx
is IEnumerable<SelectListItem>
that has been added as a ViewBag
property, the value of selectList
is null
and the code in the first if
block is executed and the value of flag
is true
(and note also that the model may or may not have a property named xxx
, meaning that metadata
might be null
)
alternatively, if you used the strongly typed DropDownListFor()
method, for example
@Html.DropDownListFor(m => m.SomeProperty, Model.SomePropertyList, "--Please select--")
the value of selectList
is not null
(assuming that SomePropertyList
is IEnumerable<SelectListItem>
and is not null
) and the value of flag
is false
So the various checks are just taking into account the different ways that you can use either DropDownList()
or DropDownListFor()
to generate a <select>
element, and whether you are binding to a model property or not.
Side note: The actual code (from the private static MvcHtmlString SelectInternal() method) is bool usedViewData = false;
, not bool flag = false;
Upvotes: 1