0

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.

b_levitt
  • 7,059
  • 2
  • 41
  • 56

1 Answers1

1

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;

Community
  • 1
  • 1
  • Thanks for the great answer. However, I'm not sure what GetModelStateValue is doing or when it's underlying dictionary is created, but it doesn't seem to reflect what is in the metadata. In my metadata provider's CreateMetadata method I can set the returned ModelMetadata.Model property == "test". Then when calling "TextBoxFor", the text boxes are all filled with "test". This does not satisfy DropDownListFor for some reason and it continues to call ViewData.Eval which essentially just tries to get the default value for the property purely based on type. – b_levitt May 16 '16 at 14:28
  • Did you read the answer I linked to?. `GetModelStateValue()` is always called first to get the value from `ModelState` (e.g. if you post you model back to a controller method, the submitted values are added to `ModelState` and then if you return the view, the values in `ModelState` are checked before anything else) –  May 17 '16 at 00:05
  • No, I applogize, I didn't notice the link until now. But I'm not sure it is relevant. AFTER the retrieval from model state fails (which I'm guessing I could force anyway with ModelState.Clear), it continues and calls ViewData.Eval (which just boils down to a reflection call on the type) INSTEAD of getting a value from metadata? Why does DropDownList favor the type when the other helpers favor the metadata? – b_levitt May 17 '16 at 13:23
  • Because it needs to take into account all the various possible uses of `DropDownList()` and `DropDownListFor()` - for example, you could (cringe) use `DropDownList("Categories")` where `Categories` is from `ViewBag.Catagories = new SelectList(....)` and there may not be model property named `Categories` (i.e there is no `ModelMetaData`), and then there is `DropDownList("Categories", (SelectList)ViewBag.Categories)` and `DropDownList("CategoryID", (SelectList)ViewBag.Categories) etc. –  May 17 '16 at 13:32
  • Also its not clear what you mean by _all the other helpers favor the metadata). For example `@Html.TextBoxFor()` uses `tagBuilder.MergeAttribute("value", attemptedValue ?? ((useViewData) ? htmlHelper.EvalString(fullName, format) : valueParameter)` - i.e. it uses the static `EvalString()` method of `HtmlHelper` which in turn calls `return Convert.ToString(ViewData.Eval(key), CultureInfo.CurrentCulture);` –  May 17 '16 at 13:45
  • See my new edit. Also see the first comment regarding favoring the metadata (ie ModelMetadata.Model="Test" in CreateMetadata.) – b_levitt May 17 '16 at 15:56