Hi i have an entity called User with 2 properties called UserName and Role (which is a reference to another entity called Role). I'm trying to update the UserName and RoleID from a form which is posted back. Within my postback action i have the following code:
var user = new User();
TryUpdateModel(user, "User", new string[] { "UserName, Role.RoleID" });
TryUpdateModel(user, new string[] { "User.UserName, User.Role.RoleID" });
However none of these updates the Role.RoleID property. If i try the following:
TryUpdateModel(user, "User", new string[] { "UserName, Role" });
TryUpdateModel(user);
The RoleID is updated but the RoleName property is validated aswell. That's why i'm trying to be more specific on which properties to update but i can't get any of the first examples to work.
I'd appreciate if someone could help. Thanks
Here's a complete solution to work with any relationship.
First place the following line of code in your Application_Start event:
ModelBinders.Binders.DefaultBinder = new CustomModelBinder();
Now you need to add the following class somewhere in your application:
public class CustomModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (bindingContext.ModelType.Namespace.EndsWith("Models.Entities") && !bindingContext.ModelType.IsEnum && value != null)
{
if (Utilities.IsInteger(value.AttemptedValue))
{
var repository = ServiceLocator.Current.GetInstance(typeof(IRepository<>).MakeGenericType(bindingContext.ModelType));
return repository.GetType().InvokeMember("GetByID", BindingFlags.Instance | BindingFlags.InvokeMethod | BindingFlags.Public, null, repository, new object[] { Convert.ToInt32(value.AttemptedValue) });
}
else if (value.AttemptedValue == "")
return null;
}
return base.BindModel(controllerContext, bindingContext);
}
}
Please note the above code may need to be modified to suit your needs. It affectively calls IRepository().GetByID(???). It will work when binding against any entities within the Models.Entities namespace and that have an integer value.
Now for the view there's one other bit of work you have to do to fix a bug in ASP.NET MVC 2. By default the Selected property on a SelectListItem is ignored so i have come up with my own DropDownListFor which allows you to pass in the selected value.
public static class SelectExtensions
{
public static MvcHtmlString DropDownListFor<TModel, TProperty>(this HtmlHelper<TModel> helper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> selectList, string selectedValue, string optionLabel)
{
return DropDownListFor(helper, expression, selectList, selectedValue, optionLabel, null);
}
public static MvcHtmlString DropDownListFor<TModel, TProperty>(this HtmlHelper<TModel> helper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> selectList, string selectedValue, string optionLabel, object htmlAttributes)
{
return DropDownListHelper(helper, ExpressionHelper.GetExpressionText(expression), selectList, selectedValue, optionLabel, new RouteValueDictionary(htmlAttributes));
}
/// <summary>
/// This is almost identical to the one in ASP.NET MVC 2 however it removes the default values stuff so that the Selected property of the SelectListItem class actually works
/// </summary>
private static MvcHtmlString DropDownListHelper(HtmlHelper helper, string name, IEnumerable<SelectListItem> selectList, string selectedValue, string optionLabel, IDictionary<string, object> htmlAttributes)
{
name = helper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(name);
// Convert each ListItem to an option tag
var listItemBuilder = new StringBuilder();
// Make optionLabel the first item that gets rendered
if (optionLabel != null)
listItemBuilder.AppendLine(ListItemToOption(new SelectListItem() { Text = optionLabel, Value = String.Empty, Selected = false }, selectedValue));
// Add the other options
foreach (var item in selectList)
{
listItemBuilder.AppendLine(ListItemToOption(item, selectedValue));
}
// Now add the select tag
var tag = new TagBuilder("select") { InnerHtml = listItemBuilder.ToString() };
tag.MergeAttributes(htmlAttributes);
tag.MergeAttribute("name", name, true);
tag.GenerateId(name);
// If there are any errors for a named field, we add the css attribute
ModelState modelState;
if (helper.ViewData.ModelState.TryGetValue(name, out modelState))
{
if (modelState.Errors.Count > 0)
tag.AddCssClass(HtmlHelper.ValidationInputCssClassName);
}
return tag.ToMvcHtmlString(TagRenderMode.Normal);
}
internal static string ListItemToOption(SelectListItem item, string selectedValue)
{
var tag = new TagBuilder("option") { InnerHtml = HttpUtility.HtmlEncode(item.Text) };
if (item.Value != null)
tag.Attributes["value"] = item.Value;
if ((!string.IsNullOrEmpty(selectedValue) && item.Value == selectedValue) || item.Selected)
tag.Attributes["selected"] = "selected";
return tag.ToString(TagRenderMode.Normal);
}
}
Now within your view you can say:
<%= Html.DropDownListFor(m => m.User.Role, Model.Roles, Model.User.Role != null ? Model.User.Role.RoleID.ToString() : "", "-- Please Select --")%>
<%= Html.ValidationMessageFor(m => m.User.Role, "*")%>
The Role property will automatically update when you call TryUpdateModel in your controller and you have to do no additional work to wire this up. Although it's alot of code initially i've found this approach saves heaps of code in the long term.
Hope this helps.
Consider calling TryUpdateModel twice, once for the User. Once for the Role. Then assign the role to the user.
var user = new User();
var role = new Role();
TryUpdateModel(user, new string[] { "UserName" });
TryUpdateModel(role, new string[] { "RoleID" });
user.Role = role;
See if that works.
Related
I am currently making a wizard in MVC (c#). But I have an if statement in my Wizard view that goes like this:
if (Model.Wizard.ClientDetails.GetStep() == Model.Wizard.CurrentStep)
{
#Html.PartialFor(x => x.Wizard.ClientDetails, "_Step");
}
else if (Model.Wizard.Preferences.GetStep() == Model.Wizard.CurrentStep)
{
#Html.PartialFor(x => x.Wizard.ClientPreferences, "_Step")
}
else if (Model.Wizard.ClientQuestions.GetStep() == Model.Wizard.CurrentStep)
{
#Html.PartialFor(x => x.Wizard.ClientQuestions, "_Step")
}
The wizards have been set up pretty generically except for this part of the view where I choose which partial to display. As you can see from the code above each if follows the same structure. The only part that changes is the Model.Wizard.**Property** part.
I wanted to try and remove this if statement so I don't have to worry about writing an if statement for each step I add to a new wizard.
I want to change the code to just something like this:
#Html.PartialFor(x => x.ExampleWizardTransaction.GetStepObject(), "_Step");
My current attempt for the GetStepObject method is as follows:
public static T GetStepObject<T>(this IWizardTransaction wizardTransaction)
where T : class, new()
{
var properties = wizardTransaction.GetType().GetProperties()
.Where(x => x.PropertyType.GetCustomAttributes(typeof(StepAttribute), true).Any());
PropertyInfo #object = properties.FirstOrDefault(x => ((StepAttribute)Attribute
.GetCustomAttribute(x.PropertyType, typeof(StepAttribute))).Step == wizardTransaction.CurrentStep);
}
The PropertyInfo #object part is correctly selecting the property info for the current step in the wizard. I need to be able to return the PropertyInfo #object PropertyInfo as its correct type with its current values and return it somehow.
Is this possible?
EDIT #1:
Existing PartialFor that works in normal scenarios.
public static MvcHtmlString PartialFor<TModel, TProperty>(
this HtmlHelper<TModel> helper, Expression<Func<TModel, TProperty>> expression, string partialViewName)
{
var name = ExpressionHelper.GetExpressionText(expression);
var model = ModelMetadata.FromLambdaExpression(expression, helper.ViewData).Model;
var viewData = new ViewDataDictionary(helper.ViewData)
{
TemplateInfo = new TemplateInfo { HtmlFieldPrefix = name }
};
return helper.Partial(partialViewName, model, viewData);
}
EDIT #2:
The reason the values are not getting binded is that the var name = ExpressionHelper.GetExpressionText(expression); part is returning a blank string. If I hard code the name variable to the actual property then the binding works. For example:
public static MvcHtmlString PartialFor<TModel, TProperty>(this HtmlHelper<TModel> helper,
Expression<Func<TModel, TProperty>> expression, string partialViewName)
{
var compiled = expression.Compile();
var result = compiled.Invoke(helper.ViewData.Model);
var name = ExpressionHelper.GetExpressionText(expression);
//Should be ExampleWizardTransaction.ClientDetails for this step but is blank
var viewData = new ViewDataDictionary(helper.ViewData)
{
TemplateInfo = new TemplateInfo
{
//HtmlFieldPrefix = name
HtmlFieldPrefix = "ExampleWizardTransaction.ClientDetails"
}
//Hard coded this to ExampleWizardTransaction.ClientDetails and the bindings now work
};
return helper.Partial(partialViewName, result, viewData);
}
It seems I need to be able to get the name of the wizard object and the current step object as a string value to pass into TemplateInfo.
I'm gonna take a wild guess at your class structures. Assuming your classes are something like this:
[AttributeUsage(AttributeTargets.Property, AllowMultiple =false)]
public class StepAttribute: Attribute
{
public StepEnum Step { get; set; }
}
public interface IWizardStep
{
}
public interface IWizardTransaction
{
}
public enum StepEnum
{
Previous,
CurrentStep
}
public class WizardStep: IWizardStep
{
public string StepName { get; set; }
public override string ToString()
{
return StepName;
}
}
public class Wizard : IWizardTransaction
{
[Step(Step = StepEnum.Previous)]
public WizardStep ClientDetails => new WizardStep() { StepName = "ClientDetails" };
[Step(Step = StepEnum.CurrentStep)]
public WizardStep ClientQuestions => new WizardStep() { StepName = "ClientQuestions" };
}
Assuming also this implementation of PartialFor method
public static MvcHtmlString PartialFor<TModel, TProperty>(this HtmlHelper<TModel> html,
Expression<Func<TModel, TProperty>> expression, string partialViewName)
{
var compiled = expression.Compile();
var result = compiled.Invoke(html.ViewData.Model);
return html.Partial(partialViewName, result);
}
Then this implementation of GetStepObject will work
public static TProperty GetStepObject<TProperty>(this IWizardTransaction wizardTransaction)
where TProperty : class
{
var properties = wizardTransaction.GetType().GetProperties()
.Where(x => x.GetCustomAttributes(typeof(StepAttribute), true).Any());
PropertyInfo #object = properties.FirstOrDefault(x =>
(x.GetCustomAttributes(typeof(StepAttribute), true).SingleOrDefault()
as StepAttribute).Step == StepEnum.CurrentStep);
return #object.GetValue(wizardTransaction) as TProperty;
}
With this implementation of a partial view named _Step.cshtml like this
#model PartialView.Models.WizardStep
#Model
Your view can call it like this
#model PartialView.Models.Wizard
#using PartialView.Models;
#{
ViewBag.Title = "Partial view calling";
}
#Html.PartialFor(m=>m.GetStepObject<WizardStep>(), "_Step")
And the visual result will be a blank page with the html text ClientQuestions
I'm creating a custom Html helper for handling a checkbox list. I know there is a ton of info on this topic online, but I haven't seen much on trying to generate the HTML the way I am below.
Desired Rendered HTML
<ul>
<li>
<label>
<input type="checkbox" name="Genres" value="SF" />
Science Fiction
</label>
</li>
<li>
<label>
<input type="checkbox" name="Genres" value="HR" />
Horror
</label>
</li>
<!-- more genres -->
</ul>
I am using distinct checkbox values rather than just booleans, because I want to take advantage of the fact that on form post, I would get a comma-separated list of selected values. For example, if Sci-Fi and Horror were both selected, I'd get "SF,HR".
View Model
public class MovieViewModel
{
public string Name { get;set; }
public string Year { get;set; }
public IEnumerable<string> Genres { get;set; }
public IEnumerable<SelectListItem> GenreOptions { get;set; }
}
View
In my view, I'd like to basically do this:
#Html.EditorFor(model => model.Name)
#Html.EditorFor(model => model.Year)
#Html.CheckBoxListFor(model => model.Genres, Model.GenreOptions)
Custom Html Helper
So, I started creating a custom Html helper, but I don't have a ton of experience with these, and this is where I need the help:
public static MvcHtmlString CheckboxListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper,
Expression<Func<TModel, TProperty>> expression,
IEnumerable<SelectListItem> items)
{
if (items == null) return null;
var itemsList = items.ToList();
if (!itemsList.Any()) return null;
// How do I get "Genres" name from the passed expression from viewmodel, without passing the hardcoded string "genres"?
var checkboxGroupName = expression.what????
var sb = new StringBuilder();
sb.Append("<ul>");
foreach (var item in itemsList)
{
var checkbox = $"<input type=\"checkbox\" name=\"{checkboxGroupName}\" value=\"{item.Value}\" />";
sb.Append($"<li class=\"checkbox\"><label>{checkbox} {HttpUtility.HtmlEncode(item.Text)}</label></li>");
}
sb.Append("</ul>");
return MvcHtmlString.Create(sb.ToString());
}
As you can see in the comment above, I don't know how to dynamically assign the viewmodel property name "Genres" to the checkboxes' name attribute, without hardcoding it. How do I get it from model => model.Genres expression?
To get the property name, use
var name = ExpressionHelper.GetExpressionText(expression);
which will give the fully qualified name, for example #CheckboxListFor(m => m.Movies.Genres, ....) will return Movies.Genres.
But to account for cases where your using an custom EditorTemplate for you model (which is a property of a parent model), then you also need to get the `HtmlFieldPrefix and if it exists, prepend it the name.
You current code does not give you correct binding, for example if Genres contains values matching the Value property of a SelectListItem, the associated checkbox should be checked when the view is rendered. Your code should be
public static MvcHtmlString CheckBoxListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> items)
{
if (items == null)
{
throw new ArgumentException("...");
}
var metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
var model = metadata.Model as IEnumerable<string>;
if (model == null)
{
throw new ArgumentException("...");
}
// Get the property name
var name = ExpressionHelper.GetExpressionText(expression);
// Get the prefix in case using a EditorTemplate for a nested property
string prefix = htmlHelper.ViewData.TemplateInfo.HtmlFieldPrefix;
if (!String.IsNullOrEmpty(prefix))
{
name = String.Format("{0}.{1}", prefix, name);
}
StringBuilder html = new StringBuilder();
foreach (var item in items)
{
StringBuilder innerHtml = new StringBuilder();
TagBuilder checkbox = new TagBuilder("input");
checkbox.MergeAttribute("type", "checkbox");
checkbox.MergeAttribute("name", name);
checkbox.MergeAttribute("value", item.Value);
if (model.Contains(item.Value))
{
checkbox.MergeAttribute("checked", "checked");
}
innerHtml.Append(checkbox.ToString());
TagBuilder text = new TagBuilder("span");
text.InnerHtml = item.Text;
innerHtml.Append(text.ToString());
TagBuilder label = new TagBuilder("label");
label.InnerHtml = innerHtml.ToString();
TagBuilder li = new TagBuilder("li");
li.AddCssClass("checkbox");
li.InnerHtml = label.ToString();
html.Append(li);
}
TagBuilder ul = new TagBuilder("ul");
ul.InnerHtml = html.ToString();
return MvcHtmlString.Create(ul.ToString());
}
You can get the name of the property passed in the Html Helper from the parameter of type Expression<Func<TModel,TProperty>> that is passed using the ModelMetaData class with the following lines of code:
var metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
var checkboxGroupName = metadata.PropertyName;
Here is an article explaining How to Create Custom Strongly Typed Html Helpers which might help you more.
I was following a question where the OP had something like this
[HttpGet]
public ActionResult Index() {
var options = new List<SelectListItem>();
options.Add(new SelectListItem { Text = "Text1", Value = "1" });
options.Add(new SelectListItem { Text = "Text2", Value = "2" });
options.Add(new SelectListItem { Text = "Text3", Value = "3" });
ViewBag.Status = options;
return View();
}
And then in the view was able to do something like this
#Html.DropDownList("Status", ViewBag.Status as SelectList)
My expectation was that the result of the cast would be null and I stated as much. I was corrected that it should work and it was demonstrated via .net fiddle. To my surprise the dropdownlist was populated with the items.
My question: How is it that when done in the view, List<SelectListItem> safely casts to SelectList
This was a good question. I looked into the matter further and, indeed, if the selectList parameter is null, then the name parameter is used to look up a key in ViewData.
I'm basing this on http://aspnetwebstack.codeplex.com/SourceControl/changeset/view/5cb74eb3b2f3#src/System.Web.Mvc/Html/SelectExtensions.cs
They even added a comment:
private static MvcHtmlString SelectInternal(this HtmlHelper htmlHelper, ModelMetadata metadata, string optionLabel, string name, IEnumerable<SelectListItem> selectList, bool allowMultiple, IDictionary<string, object> htmlAttributes)
{
...
// If we got a null selectList, try to use ViewData to get the list of items.
if (selectList == null)
{
selectList = htmlHelper.GetSelectData(name);
...
And later on, the name is used:
private static IEnumerable<SelectListItem> GetSelectData(this HtmlHelper htmlHelper, string name)
{
object o = null;
if (htmlHelper.ViewData != null)
{
o = htmlHelper.ViewData.Eval(name);
}
...
Good question #Nkosi. I had no idea this was possible.
I have a field called W2_Sent which is defined as (bit,null)
In my view I have the following which shows it as a checkbox:
<div class="editor-label" style="width: 10em">
#Html.Label("W2 Sent")
</div>
<div class="editor-field">
#Html.EditorFor(model => model.W2_Sent)
#Html.ValidationMessageFor(model => model.W2_Sent)
</div>
If I check it, I get an error
The value 'checked' is not valid for W2_Sent
[HttpPost]
public ActionResult Create(Employee emp)
{
foreach (ModelState modelState in ViewData.ModelState.Values)
{
foreach (ModelError error in modelState.Errors)
{
string s = "error";
}
}
I am able to trap the error within the foreach loop you see above..
Why am I getting value 'checked' is invalid though
For displaying checkboxes in forms you should always use #Html.CheckBox/CheckBoxFor instead of <input type="checkbox" name="gender" />. When you use #Html.CheckBox/CheckBoxFor ASP.NET MVC generates a hidden field which has a boolean value and that is what will be binded to your model property.
When you directly use the html part then browsers posts the value of the field as string "checked" if it is, and in model binding that throws the error.
I've done it like this. Write your own ExtensionMethod for CheckBoxFor.
The trick is the static values for "value" = "true" in the checkbox and the "value" "false" in the hidden field.
As mentioned before, a checkbox with a value false will not be sent back. In this case the value of the hidden field will be taken. When the checkbox is checked by the user, the new "true"-value will override the "false" from the hidden field.
public static MvcHtmlString CheckboxForMetro<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, int offset = 3)
{
TagBuilder tblabel = new TagBuilder("label");
tblabel.AddCssClass("checkbox offset" + offset.ToString());
TagBuilder tbinput = new TagBuilder("input");
tbinput.Attributes.Add("type", "checkbox");
tbinput.Attributes.Add("id", GetPropertyNameFromLambdaExpression(html, expression));
tbinput.Attributes.Add("name", GetPropertyNameFromLambdaExpression(html, expression));
tbinput.Attributes.Add("value", "true");
tbinput.MergeAttributes(GetPropertyValidationAttributes(html, expression, null));
if (GetPropertyValueFromLambdaExpression(html, expression) == "True") tbinput.Attributes.Add("checked", "checked");
TagBuilder tbhidden = new TagBuilder("input");
tbhidden.Attributes.Add("type", "hidden");
tbhidden.Attributes.Add("value", "false");
tbhidden.Attributes.Add("name", GetPropertyNameFromLambdaExpression(html, expression));
TagBuilder tbspan = new TagBuilder("span");
//tbspan.AddCssClass("span" + spanLabel.ToString());
tbspan.InnerHtml = GetPropertyDisplayNameFromLambdaExpression(html, expression);
tblabel.InnerHtml = tbinput.ToString() + tbspan.ToString() + tbhidden.ToString();
return new MvcHtmlString(tblabel.ToString());
}
It's an ExtensionMethod for the Metro UI CSS at http://metroui.org.ua
This is the code for getting the value, displayname, propertyname and validationAttributes
private static string GetPropertyDisplayNameFromLambdaExpression<TModel, TValue>(HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression)
{
ModelMetadata metadata = ModelMetadata.FromLambdaExpression(expression, html.ViewData);
string htmlFieldName = ExpressionHelper.GetExpressionText(expression);
return metadata.DisplayName ?? metadata.PropertyName ?? htmlFieldName.Split('.').Last() ?? "Geen tekst";
}
private static string GetPropertyValueFromLambdaExpression<TModel, TValue>(HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression)
{
string value = string.Empty;
TModel model = html.ViewData.Model;
if (model != null)
{
var expr = expression.Compile().Invoke(model);
if (expr != null)
{
value = expr.ToString();
}
}
return value;
}
private static string GetPropertyNameFromLambdaExpression<TModel, TValue>(HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression)
{
ModelMetadata metadata = ModelMetadata.FromLambdaExpression(expression, html.ViewData);
return metadata.PropertyName;
}
private static IDictionary<string, object> GetPropertyValidationAttributes<TModel, TValue>(HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, IDictionary<string, object> htmlAttributes)
{
ModelMetadata metadata = ModelMetadata.FromLambdaExpression(expression, html.ViewData);
IDictionary<string, object> validationAttributes = html.GetUnobtrusiveValidationAttributes(ExpressionHelper.GetExpressionText(expression), metadata);
if (htmlAttributes == null)
{
htmlAttributes = validationAttributes;
}
else
{
htmlAttributes = htmlAttributes.Concat(validationAttributes).ToDictionary(k => k.Key, v => v.Value);
}
return htmlAttributes;
}
I hope this will help someone else.
In my case, I was using some styling library that did not allow me to use the HTMLhelper. So, if it is useful for anyone, a straightforward solution was using jQuery when submitting the form, simply assigning the check value to the input, as shown below.
$("#myForm").submit(function (e) {
$("#myCheckBoxInput").val($("#myCheckBoxInput").prop("checked"))
})
There are some helpful extension methods for using displaying enums in dropdown lists. For example here and here.
But there is one problem that I encounter, which is that these helpers do not work if the enum is decorated with the Description attribute. The first example works perfectly with the Description attribute, but it doesn't set the selected value. The second example sets the selected value, but it doesn't use the description attribute. So I need to combine both methods into a working helper that does both correctly. I've a lot of variations to get it working but, no success so far. I've tried several ways to create a selectlist, but somehow it ignores the Selected property. In all my tests, the Selected property was set to true on one item, but this property is just ignored.
So any ideas are most welcome!
This is the latest code that I've tried:
public static IEnumerable<SelectListItem> ToSelectList(Type enumType, string selectedItem)
{
List<SelectListItem> items = new List<SelectListItem>();
foreach (var item in Enum.GetValues(enumType))
{
FieldInfo fi = enumType.GetField(item.ToString());
var attribute = fi.GetCustomAttributes(typeof(DescriptionAttribute), true).FirstOrDefault();
var title = attribute == null ? item.ToString() : ((DescriptionAttribute)attribute).Description;
var listItem = new SelectListItem
{
Value = ((int)item).ToString(),
Text = title,
Selected = selectedItem == item.ToString()
};
items.Add(listItem);
}
return items;
}
public static HtmlString EnumDropDownList2For<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> modelExpression)
{
var typeOfProperty = modelExpression.ReturnType;
if (!typeOfProperty.IsEnum)
throw new ArgumentException(string.Format("Type {0} is not an enum", typeOfProperty));
var value = htmlHelper.ViewData.Model == null
? default(TProperty)
: modelExpression.Compile()(htmlHelper.ViewData.Model);
return htmlHelper.DropDownListFor(modelExpression, ToSelectList(modelExpression.ReturnType, value.ToString()));
}
I had the same issue with enums which actually had custom value set
public enum Occupation
{
[Description("Lorry driver")] LorryDriver = 10,
[Description("The big boss")] Director = 11,
[Description("Assistant manager")] AssistantManager = 12
}
What I found is that when I use DropDownListFor(), it doesn't use the selected item from the SelectListItem collection to set the selected option. Instead it selects an option with a value which equals to the property I'm trying to bind to (m => m.Occupation) and for this it uses the enum's .ToString() not the enum's actual integer value. So what I ended up with is setting the SelectListItem's Value like so:
var listItem = new SelectListItem
{
Value = item.ToString(), // use item.ToString() instead
Text = title,
Selected = selectedItem == item.ToString() // <- no need for this
};
The helper method:
public static class SelectListItemsForHelper
{
public static IEnumerable<SelectListItem> SelectListItemsFor<T>(T selected) where T : struct
{
Type t = typeof(T);
if (t.IsEnum)
{
return Enum.GetValues(t).Cast<Enum>().Select(e => new SelectListItem { Value = e.ToString(), Text = e.GetDescription() });
}
return null;
}
public static string GetDescription<TEnum>(this TEnum value)
{
FieldInfo fi = value.GetType().GetField(value.ToString());
if (fi != null)
{
var attributes = (DescriptionAttribute[])fi.GetCustomAttributes(typeof(DescriptionAttribute), false);
if (attributes.Length > 0)
return attributes[0].Description;
}
return value.ToString();
}
}
In the view:
#Html.DropDownListFor(m => m.Occupation, SelectListItemsForHelper.SelectListItemsFor(Model.Occupation), String.Empty)
To summarize the solution that does work (at least for me):
I use the following set of helper methods
public static IEnumerable<SelectListItem> ToSelectList(Type enumType, string selectedItem)
{
List<SelectListItem> items = new List<SelectListItem>();
foreach (var item in Enum.GetValues(enumType))
{
var title = item.GetDescription();
var listItem = new SelectListItem
{
Value = item.ToString(),
Text = title,
Selected = selectedItem == item.ToString()
};
items.Add(listItem);
}
return items;
}
public static MvcHtmlString EnumDropDownListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression) where TModel : class
{
string inputName = GetInputName(expression);
var value = htmlHelper.ViewData.Model == null
? default(TProperty)
: expression.Compile()(htmlHelper.ViewData.Model);
return htmlHelper.DropDownList(inputName, ToSelectList(typeof(TProperty), value.ToString()));
}
public static string GetInputName<TModel, TProperty>(Expression<Func<TModel, TProperty>> expression)
{
if (expression.Body.NodeType == ExpressionType.Call)
{
MethodCallExpression methodCallExpression = (MethodCallExpression)expression.Body;
string name = GetInputName(methodCallExpression);
return name.Substring(expression.Parameters[0].Name.Length + 1);
}
return expression.Body.ToString().Substring(expression.Parameters[0].Name.Length + 1);
}
private static string GetInputName(MethodCallExpression expression)
{
MethodCallExpression methodCallExpression = expression.Object as MethodCallExpression;
if (methodCallExpression != null)
{
return GetInputName(methodCallExpression);
}
return expression.Object.ToString();
}
Usage:
#Html.EnumDropDownListFor(m => m.MyEnumType)
This works for enums with or without a description attribute, and sets the correct selected value.