How to read property data annotation value in .NET MVC - asp.net-mvc-3

I Just starting out w/ ASP.NET MVC 3 and I am trying to render out the following HTML for the string properties on a ViewModel on the create/edit view.
<input id="PatientID" name="PatientID" placeholder="Patient ID" type="text" value="" maxlength="30" />
Each value ties back to the property on the ViewModel, id & name are the property name, placeholder is the Display attribute, value is the value of the property, and maxlength is the StringLength attribute.
Instead of typing out the above HTML w/ the correct values for each of my string properties I thought I would try to create an EditorTemplate by the name of SingleLineTextBox and use UIHint on my string properties or pass the name of the view when I call EditFor. So far so good, except I can't figure out how to get the maxlength value off the StringLength attribute.
Here is the code I have so far:
<input id="#ViewData.ModelMetadata.PropertyName" name="#ViewData.ModelMetadata.PropertyName" placeholder="#ViewData.ModelMetadata.DisplayName" type="text" value="#ViewData.Model" maxlength="??" />
As you can see, not sure how to set maxlength value. Anyone know how?
Also, am I going about this the best way? As I said before I could just write out the plain HTML myself for each property on the page. I've looked at using TextBoxFor it wasn't setting the maxlength and was adding a bunch of validation markup to the HTML output because of the StringLength attribute which I do not want. Another option I saw was extensions/helpers off the HTML class.

A full code sample for tvanfosson's answer:
Model:
public class Product
{
public int Id { get; set; }
[MaxLength(200)]
public string Name { get; set; }
EditorTemplates\String.cshtml
#model System.String
#{
var metadata = ViewData.ModelMetadata;
var prop = metadata.ContainerType.GetProperty(metadata.PropertyName);
var attrs = prop.GetCustomAttributes(false);
var maxLength = attrs.OfType<System.ComponentModel.DataAnnotations.MaxLengthAttribute>().FirstOrDefault();
}
<input id=#Html.IdForModel()#(metadata.IsRequired ? " required" : "")#(maxLength == null ? "" : " maxlength=" + maxLength.Length) />
HTML output:
<input id=Name maxlength=200 />
Ugly but it works. Now let's abstract it and clean it up a bit. Helper class:
public static class EditorTemplateHelper
{
public static PropertyInfo GetPropertyInfo(ViewDataDictionary viewData)
{
var metadata = viewData.ModelMetadata;
var prop = metadata.ContainerType.GetProperty(metadata.PropertyName);
return prop;
}
public static object[] GetAttributes(ViewDataDictionary viewData)
{
var prop = GetPropertyInfo(viewData);
var attrs = prop.GetCustomAttributes(false);
return attrs;
}
public static string GenerateAttributeHtml(ViewDataDictionary viewData, IEnumerable<Delegate> attributeTemplates)
{
var attributeMap = attributeTemplates.ToDictionary(t => t.Method.GetParameters()[0].ParameterType, t => t);
var attrs = GetAttributes(viewData);
var htmlAttrs = attrs.Where(a => attributeMap.ContainsKey(a.GetType()))
.Select(a => attributeMap[a.GetType()].DynamicInvoke(a));
string s = String.Join(" ", htmlAttrs);
return s;
}
}
Editor Template:
#model System.String
#using System.ComponentModel.DataAnnotations;
#using Brass9.Web.Mvc.EditorTemplateHelpers;
#{
var metadata = ViewData.ModelMetadata;
var attrs = EditorTemplateHelper.GenerateAttributes(ViewData, new Delegate[] {
new Func<StringLengthAttribute, string>(len => "maxlength=" + len.MaximumLength),
new Func<MaxLengthAttribute, string>(max => "maxlength=" + max.Length)
});
if (metadata.IsRequired)
{
attrs.Add("required");
}
string attrsHtml = String.Join(" ", attrs);
}
<input type=text id=#Html.IdForModel() #attrsHtml />
So you pass in an array of Delegates, and for each entry use a Func<AttributeTypeGoesHere, string>, and then return whatever HTML string you wanted for each attribute.
This actually decouples well - you can map only the attributes you care about, you can map different sets for different parts of the same HTML, and the final usage (like #attrsHtml) doesn't harm readability of the template.

Instead of the StringLength attribute (because it's a validator attribute not a metadata provider) you can use the AdditionalMetadata attribute. Sample usage:
public class ViewModel
{
[AdditionalMetadata("maxLength", 30)]
public string Property { get; set; }
}
Basically it puts the value 30 under the key maxLength in the ViewData.ModelMetadata.AdditionalValues dictionary. So you can use it your EditorTemplate:
<input maxlength="#ViewData.ModelMetadata.AdditionalValues["maxLength"]" id="#ViewData.ModelMetadata.PropertyName" name="#ViewData.ModelMetadata.PropertyName" placeholder="#ViewData.ModelMetadata.DisplayName" type="text" value="#ViewData.Model" />

To do this you'll need to create your own HtmlHelper extension and use reflection to get at the attributes on the model property. Look at the source code at http://codeplex.com/aspnet for the existing ...For() HtmlHelper extensions. You'll need to get the PropertyInfo object for the model property using the expression that is passed in as the argument. They have several helper classes that should serve as templates for this. Once you have that, use the GetCustomAttributes method on the PropertyInfo to find the StringLength attribute and extract it's value. Since you'll be using a TagBuilder to create the input, add the length as an attribute via the TagBuilder.
...
var attribute = propInfo.GetCustomAttributes(typeof(StringLengthAttribute),false)
.OfType<StringLengthAttribute>()
.FirstOrDefault();
var length = attribute != null ? attribute.MaximumLength : 20; //provide a default
builder.Attributes.Add("maxlength",length);
...
return new MvcHtmlString( builder.ToString( TagRenderMode.SelfClosing ) );
}
See my comment on why I think this is a bad idea.

An much simpler solution is to implement a custom DataAnnotationsModelMetadataProvider like this:
internal class CustomModelMetadataProvider : DataAnnotationsModelMetadataProvider
{
protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName)
{
ModelMetadata modelMetadata = base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName);
var maxLengthAttribute = attributes.OfType<MaxLengthAttribute>().SingleOrDefault();
if (maxLengthAttribute != null)
{
modelMetadata.AdditionalValues.Add("maxLength", maxLengthAttribute.Length);
}
return modelMetadata;
}
}
In the template you can simply use:
object maxLength;
ViewData.ModelMetadata.AdditionalValues.TryGetValue("maxLength", out maxLength);

You can get the StringLength Validator from within an Editor Template, here are some examples:
https://jefferytay.wordpress.com/2011/12/20/asp-net-mvc-string-editor-template-which-handles-the-stringlength-attribute/
What I used and tested, as a result from the article above can be seen in my answer below (tested with MVC 5, EF 6) :
ASP.NET MVC 3 - Data Annoation and Max Length/Size for Textbox Rendering
Without being specific, I've personally had some mixed results with attempts to implement some other approaches, and I don't find either claimed method particular long; however, I did think some of the other approached looked a little "prettier".

#using System.ComponentModel.DataAnnotations
#model string
#{
var htmlAttributes = ViewData["htmlAttributes"] ?? new { #class = "checkbox-inline" };
var attributes = HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes);
if (!attributes.ContainsKey("maxlength"))
{
var metadata = ViewData.ModelMetadata;
var prop = metadata.ContainerType.GetProperty(metadata.PropertyName);
var attrs = prop.GetCustomAttributes(false);
var maxLength = attrs.OfType<MaxLengthAttribute>().FirstOrDefault();
if (maxLength != null)
{
attributes.Add("maxlength", maxLength.Length.ToString());
}
else
{
var stringLength = attrs.OfType<StringLengthAttribute>().FirstOrDefault();
if (stringLength != null)
{
attributes.Add("maxlength", stringLength.MaximumLength.ToString());
}
}
}
}
#Html.TextBoxFor(m => m, attributes)

Related

MVC3 Set Default value for Textbox from model

I have a text box created using
#Html.TextBoxFor(m => m.Model1.field1, new { #class = "login-input", #name="Name", #Value = "test" })
I want to change the default value of this textbox from "text" to a value stored in a model field. How do i set a model field as the the value attribute? Say the Name of the model to be called is Model2 and the attribute is field2. How do I set the value of the textbox to field2?
You must first write an Extension Method like this:
public class ObjectExtensions
{
public static string Item<TItem, TMember>(this TItem obj, Expression<Func<TItem, TMember>> expression)
{
if (expression.Body is MemberExpression)
{
return ((MemberExpression)(expression.Body)).Member.Name;
}
if (expression.Body is UnaryExpression)
{
return ((MemberExpression)((UnaryExpression)(expression.Body)).Operand).Member.Name;
}
if (expression.Body is ParameterExpression)
{
return expression.Body.Type.Name;
}
throw new InvalidOperationException();
}
}
It will extract the Name of property when you write sth like this: #Html.TextBoxFor(m => m.Model1.field1)
then you can use it like this:
Html.TextBoxFor(m => m.Model1.field1,
new { #class = "login-input",
#name="Name",
#value = Model.Item(m => m.Model1.field1) })
If you don't want to call m => m.Model1.field1 again, you must declare your version of TextBoxFor method which is more complicate, but If you want I can provide you the details.
This is a sample from my code base on Github:
public static class HtmlHelperExtensionForEditorForDateTime
{
public static MvcHtmlString Editor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression)
{
ModelMetadata metadata = ModelMetadata.FromLambdaExpression(expression, html.ViewData);
string propname = html.ViewData.Model.Item(expression);
string incomingValue = null;
var httpCookie = html.ViewContext.RequestContext.HttpContext.Request.Cookies["lang"];
if (metadata.Model is DateTime && (httpCookie.IsNull() || httpCookie.Value == Cultures.Persian))
incomingValue = PersianCalendarUtility.ConvertToPersian(((DateTime)metadata.Model).ToShortDateString());
if (string.IsNullOrEmpty(incomingValue))
return html.TextBox(propname, null, new { #class = "datepicker TextField" });
return html.TextBox(propname, incomingValue, new { #class = "datepicker TextField"});
}
}
In your controller, set the value of field1 before passing it to the view... and that will set the value automatically. If you have the value in another field, just do:
model.field1 = model.field2;
in your controller... and that way the models have consistent data.
If you don't need/want that the default value being actually the value of the textbox, you could also use a PlaceHolder... and that way the user could see a value as a hint, but it will be not count as textbox content once you post the form.
#Html.TextBoxFor(m => m.Model1.field1, new { #class = "login-input", #name="Name", placeholder= "test" })
Remember that not all fieldnames in the HtmlAttributes need a "#"... #class is correct, but the others I don't think are needed.
You can set the default value inside your controller action just before passing the model to the view:
model.field1 = "Test"
return View(model)

How can I utilise my custom DisplayTemplate with non-text fields so that it doesn't override existing values?

I've been following this guide on creating custom display attributes (specifically extra html attributes) to apply to the properties in my ViewModel. I have overridden both String and Boolean in the EditorTemplates folder. The editor template checks to see if a value has been set/the display attribute has been used - and adds the additional html attributes.
I'm getting stuck on the Boolean override when performing an edit action though. Regardless of whether or not I apply the attribute to a string, the ViewModel always maps with the correct existing data. This isn't true with any other form input type, due to the way the templates have been written by changing the type attribute inside a TextBoxFor.
I've been writing this primarily because I have been digging into knockout, and wanted an easy way to apply the data-bind attribute to strongly-typed views - if there's a better way please let me know!
Attribute Code:
public class Knockout : Attribute
{
public string DataBind { get; set; }
public string InputType { get; set; }
/*
Example:
Knockout("checked: showUploader", "checkbox")
Adds the HTML attributes data-bind="checked: showUploader" type="checkbox"
*/
public Knockout(string dataBind, string inputType)
{
this.DataBind = dataBind;
this.InputType = inputType;
}
public Dictionary<string, object> OptionalAttributes()
{
var options = new Dictionary<string, object>();
if(!string.IsNullOrWhiteSpace(DataBind))
{
options.Add("data-bind", DataBind);
}
if (!string.IsNullOrWhiteSpace(InputType))
{
options.Add("type", InputType);
}
return options;
}
}
Template Code
#using CumbriaMD.Infrastructure.ViewModels.DisplayAttributes
#{
var key = "Knockout";
}
#if (ViewData.ModelMetadata.AdditionalValues.ContainsKey(key))
{
var knockout = ViewData.ModelMetadata.AdditionalValues[key] as Knockout;
#Html.TextBoxFor(model => model, knockout.OptionalAttributes())
}
else
{
/*
When the attribute is not present, the default action is the following - which seems to
be overriding the data mapped from the database:
*/
#Html.TextBoxFor(model => model, new { type="checkbox" })
}
Found the answer nested in this beauty of a question!
My working template for boolean values now looks like:
#using CumbriaMD.Infrastructure.ViewModels.DisplayAttributes
#{
var key = "Knockout";
bool? value = null;
if(ViewData.Model != null)
{
value = Convert.ToBoolean(ViewData.Model, System.Globalization.CultureInfo.InvariantCulture);
}
}
#if (ViewData.ModelMetadata.AdditionalValues.ContainsKey(key))
{
var knockout = ViewData.ModelMetadata.AdditionalValues[key] as Knockout;
#Html.CheckBox("", value ?? false, knockout.OptionalAttributes())
}
else
{
#Html.CheckBox("", value ?? false, new { #class = "check-box" })
}

What's DisplayAttribute.GroupName property used for?

I'm trying to figure out the valid usages of DisplayAttribute.GroupName property.
MSDN says:
A value that is used to group fields in the UI.
but I wouldn't call it a comprehensive explanation. It makes me think that GroupName can be used to create groupboxes around certain fields. But then the remark:
Do not use this property to get the value of the GroupName property.
Use the GetDescription method instead. A null value or empty string is
valid.
seems to contradict it.
So what is this property for and should I use it (probably with custom template or custom ModelMetadataProvider) in order to render groupboxes around my fields?
In the MVC RTM source code there is no sign of usage.
The "GetDescription" remark might be a copy/paste error in the documentation (each string property seems to have a GetXXX counterpart that returns a localizable value), so it should be most probably "GetGroupName" in this case.
UPDATE:
I would use it exactly for that: group fields together that belong together from the UI point-of-view. As this is just data annotation on the model, it declares only that these fields belong to one logical group "somehow" on the UI, the but concrete presentation details depend on the "UI engine" that displays the model based on the metadata.
I think the most meaningful way to "render" this on the UI is exactly what you said: wrapping the grouped fields into a section or fieldset.
Of course there might be future extensions of MVC or other custom extensions that do some kind of grouping on the UI "automatically" (without writing custom code that examines the metadata and generates the sections) based on this attribute property. But I'm quite sure that such an extension would do something very similar that you would do currently.
I ended up writing this class to make the GroupName more easily accessible:
public class ExtendedDataAnnotationsModelMetadataProvider : DataAnnotationsModelMetadataProvider
{
public const string Key_GroupName = "GroupName";
protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName)
{
ModelMetadata modelMetadata = base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName);
DisplayAttribute displayAttribute = attributes.OfType<DisplayAttribute>().FirstOrDefault();
if (displayAttribute != null)
modelMetadata.AdditionalValues[ExtendedDataAnnotationsModelMetadataProvider.Key_GroupName] = displayAttribute.GroupName;
return modelMetadata;
}
}
And this extension method:
public static string GetGroupName(this ModelMetadata modelMetadata)
{
if (modelMetadata.AdditionalValues.ContainsKey(ExtendedDataAnnotationsModelMetadataProvider.Key_GroupName))
return (modelMetadata.AdditionalValues[ExtendedDataAnnotationsModelMetadataProvider.Key_GroupName] as string);
return null;
}
Source: http://bradwilson.typepad.com/blog/2010/01/why-you-dont-need-modelmetadataattributes.html
How About This !!! Must Work :
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Web;
using System.Web.Mvc;
namespace System.Web.Mvc
{
public static class DisplayGroup
{
public static MvcHtmlString DisplayGroupName(this HtmlHelper helper, string groupName)
{
return MvcHtmlString.Create(groupName);
}
public static MvcHtmlString DisplayGroupNameFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression)
{
var type = typeof(TModel);
PropertyInfo propertyInfo = null;
var member = (MemberExpression)expression.Body;
var property = (PropertyInfo)member.Member;
var name = property.Name;
var metadataTypeInfo = type.GetCustomAttribute<MetadataTypeAttribute>();
if (metadataTypeInfo != null)
{
var metadataType = metadataTypeInfo.MetadataClassType;
propertyInfo = metadataType.GetProperties().Where(x => x.Name == name).FirstOrDefault();
if (propertyInfo == null)
{
propertyInfo = type.GetProperties().Where(x => x.Name == name).FirstOrDefault();
}
}
else
{
propertyInfo = type.GetProperties().Where(x => x.Name == name).FirstOrDefault();
}
string output = "";
var dattr = propertyInfo.GetCustomAttribute<DisplayAttribute>();
if (dattr != null)
{
if (dattr.GroupName == null)
{
output = propertyInfo.Name;
}
else
{
output = dattr.GroupName;
}
}
else
{
output = propertyInfo.Name;
}
return MvcHtmlString.Create(output);
}
}
}
public class MyModel
{
[Display(Name = "Number",GroupName="Invoice")]
string InvNo { get; set; }
}
and then simply write :
#Html.DisplayGroupNameFor(x => x.InvNo)
Note :
NameSpace should be : System.Web.Mvc
Update :
The cool thing is that , if you have a MetaDataType class defined for your dataAnnotation , then also this will work as expected.

MVC Model Binding a Complex Type to a Simple Type and Vice Versa

Here's a scenario:
I have an autocomplete plugin (custom) that keeps a hidden field of JSON objects (using a specific struct).
I've created an Html helper that helps me easily bind to a specific custom model (basically, it has a JSON property that is for two-way binding and a property that lets me deserialize the JSON into the appropriate struct):
public class AutoCompleteModel {
public string JSON { get; set; }
public IEnumerable<Person> People {
get {
return new JavaScriptSerializer().Deserialize<Person>(this.JSON);
}
set {
this.JSON = new JavaScriptSerializer().Serialize(value);
}
}
}
This works great and I can model bind using the default binder #Html.Autocomplete(viewModel => viewModel.AutoCompleteModelTest). The HTML helper generates HTML like:
<input type="text" id="AutoCompleteModelTest_ac" name="AutoCompleteModelTest_ac" value="" />
<input type="hidden" id="AutoCompleteModelTest_JSON" name="AutoCompleteModelTest.JSON" value="{JSON}" />
The problem is this is not the best way for consumers. They have to manually set the People property to an array of Person structs. In my data layer, my domain objects probably will not be storing the full struct, only the person's ID (a corporate ID). The autocomplete will take care of looking up the person itself if only given an ID.
The best scenario will be to call it like this:
#Html.Autocomplete(domainObject => domainObject.PersonID) or
#Html.Autocomplete(domainObject => domainObject.ListOfPersonIDs
I would like it to work against the string property AND against the custom AutoCompleteModel. The autocompleter only updates a single hidden field, and that field name is passed back on postback (the value looks like: [{ "Id":"12345", "FullName":"A Name"},{ "Id":"12347", "FullName":"Another Name" }]).
The problem is, of course, that those domain object properties only have an ID or array of IDs, not a full Person struct (so cannot be directly serialized into JSON). In the HTML helper, I can transform those property values into a struct, but I don't know how to transform it back into a simple type on POST. The solution I need would transform an ID into a new Person struct on page load, serializing it into the hidden field. On POST, it would deserialize the generated JSON back into a simple array of IDs.
Is a custom model binder the solution I need? How can I tell it to work both with a custom model AND simple types (because I don't want it applied to EVERY string property, just need it to deal with the values given by the HTML helper).
I figured it out, it's possible!
To clarify, I needed to: transform a string or string array (of IDs) into a JSON structure for my hidden field value, then on post back, deserialize the JSON in the hidden field and transform the struct back into a simple string or string array (of IDs) for my domain object's property.
Step 1: Create a HTML helper
I had done this already, but only for accepting my custom AutoCompleteModel type. I needed one for a string and an Enumerable of string type.
All I did was generate my Person struct(s) from the value of the property and serialize them into JSON for the hidden field the Autocompleter uses (this is an example of the string helper, I also have a nearly identical one for IEnumerable<string>):
public static MvcHtmlString AutoComplete<TModel>(
this HtmlHelper<TModel> htmlHelper,
Expression<Func<TModel, string>> idProp)
where TModel : class
{
TModel model = htmlHelper.ViewData.Model;
string id = idProp.Compile().Invoke(model);
string propertyName = idProp.GetPropertyName();
Person[] people = new Person[] {
new Person() { ID = id }
};
// Don't name the textbox the same name as the property,
// otherwise the value will be whatever the textbox is,
// if you care.
MvcHtmlString textBox = htmlHelper.TextBox(propertyName + "_ac", string.Empty);
// For me, the JSON is the value I want to postback
MvcHtmlString hidden = htmlHelper.Hidden(propertyName, new JavaScriptSerializer().Serialize(people));
return MvcHtmlString.Create(
"<span class=\"AutoComplete\">" +
textBox.ToHtmlString() +
hidden.ToHtmlString() +
"</span>");
}
Usage: #Html.AutoComplete(model => model.ID)
Step 2: Create a custom model binder
The crux of my issue was that I needed this binder to only apply to certain properties, and they were strings or string arrays.
I was inspired by this article because it used Generics. I decided, hey, we can just ask people what property they want to apply the binder for.
public class AutoCompleteBinder<T> : DefaultModelBinder
where T : class
{
private IEnumerable<string> PropertyNames { get; set; }
public AutoCompleteBinder(params Expression<Func<T, object>>[] idProperties)
{
this.PropertyNames = idProperties.Select(x => x.GetPropertyName());
}
protected override object GetPropertyValue(
ControllerContext controllerContext,
ModelBindingContext bindingContext,
PropertyDescriptor propertyDescriptor,
IModelBinder propertyBinder)
{
var submittedValue = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (submittedValue != null && this.PropertyNames.Contains(propertyDescriptor.Name))
{
string json = submittedValue.AttemptedValue;
Person[] people = new JavaScriptSerializer().Deserialize<Person[]>(json);
if (people != null && people.Any())
{
string[] IDs = people.Where(x => !string.IsNullOrEmpty(x.ID)).Select(x => x.ID).ToArray();
bool isArray = bindingContext.ModelType != typeof(string) &&
(bindingContext.ModelType == typeof(string[]) ||
bindingContext.ModelType.HasInterface<IEnumerable>());
if (IDs.Count() == 1 && !isArray)
return IDs.First(); // return string
else if (IDs.Count() > 0 && isArray)
return IDs.ToArray(); // return string[]
else
return null;
}
else
{
return null;
}
}
return base.GetPropertyValue(controllerContext, bindingContext, propertyDescriptor, propertyBinder);
}
}
GetPropertyName() (translate LINQ expression into a string, i.e. m => m.ID = ID) and HasInterface() are just two utility methods I have.
Step 3: Register
Register the binder on your domain objects and their properties in Application_Start:
ModelBinders.Binders.Add(typeof(Employee), new AutoCompleteBinder<Employee>(e => e.ID, e => e.TeamIDs));
It's only a little bit annoying to have to register the binder for specific properties, but it's not the end of the world and provides a nice, smooth experience working with my autocompleter.
Any comments are welcome.

ViewBag property value in DropDownListFor instead of Model property value

We found strange behaviour in DropDownListFor (ASP.NET MVC3 release). It selects ViewBag property value instead of Model property value in dropdown.
Model:
public class Country {
public string Name { get; set; }
}
public class User {
public Country Country { get; set; }
}
Controller Index action:
ViewBag.CountryList = new List<Country> { /* Dropdown collection */
new Country() { Name = "Danmark" },
new Country() { Name = "Russia" } };
var user = new User();
user.Country = new Country(){Name = "Russia"}; /* User value */
ViewBag.Country = new Country() { Name = "Danmark" }; /* It affects user */
return View(user);
View:
#Html.EditorFor(user => user.Country.Name)
#Html.DropDownListFor(user => user.Country.Name,
new SelectList(ViewBag.CountryList, "Name", "Name", Model.Country), "...")
It will show text box with "Russia" value and dropdown with "Danmark" value selected instead of "Russia".
I didn't find any documentation about this behaviour. Is this behaviour normal? And why is it normal? Because it is very hard to control ViewBag and Model properties names.
This sample MVC3 project sources
I'm not so sure why this decision was made, but it was happened because MVC framework tried to use the ViewData-supplied value before using the parameter-supplied value. That's why ViewBag.Country override parameter-supplied value Model.Country.
That was how it was written in MVC framework in the private method SelectInternal.
object defaultValue = (allowMultiple) ? htmlHelper.GetModelStateValue(fullName, typeof(string[])) : htmlHelper.GetModelStateValue(fullName, typeof(string));
// If we haven't already used ViewData to get the entire list of items then we need to
// use the ViewData-supplied value before using the parameter-supplied value.
if (!usedViewData) {
if (defaultValue == null) {
defaultValue = htmlHelper.ViewData.Eval(fullName);
}
}
if (defaultValue != null) {
IEnumerable defaultValues = (allowMultiple) ? defaultValue as IEnumerable : new[] { defaultValue };
IEnumerable<string> values = from object value in defaultValues select Convert.ToString(value, CultureInfo.CurrentCulture);
HashSet<string> selectedValues = new HashSet<string>(values, StringComparer.OrdinalIgnoreCase);
List<SelectListItem> newSelectList = new List<SelectListItem>();
foreach (SelectListItem item in selectList) {
item.Selected = (item.Value != null) ? selectedValues.Contains(item.Value) : selectedValues.Contains(item.Text);
newSelectList.Add(item);
}
selectList = newSelectList;
}
This code defaultValue = htmlHelper.ViewData.Eval(fullName); tried to get the value from ViewData and if it can get the value, it will override the supplied parameter selectList with new list.
Hope it can help. Thanks.
side-node: ViewBag is just a dynamic wrapper class of ViewData.
The following line from your action method is what is confusing the code:
ViewBag.Country = new Country() { Name = "Danmark" }; /* It affects user */
That's because the html helpers look into a few different places to pick up values for the generated controls. In this case ViewData["Country"] is clashing with ModelState["Country"] Rename that property to something else and everything should work.

Resources