Metadata vs ViewData in HtmlHelper (DropDownListFor Bug?) - asp.net-mvc

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.

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;

Related

ModelState binding custom array of checkboxes

ViewModel Binding is working, the object passed back to the edit controller contains the correct values, which is a list of selected options. However, ModelState binding is not working, the model state AttemptedValues exist, but aren't being reloaded into the fields.
I have a model with the following properties
class Model
{
public List<string> AvailableValues { get; set; }
public List<string> SelectedValues { get; set; }
}
But in my view I have some categorization, so I can't do a direct foreach.
foreach (var category in CatgoryList.Categories)
{
foreach (var available in Model.AvailableValues.Where(x => category.AvailableValues.Contains(x))
{
var check = Model.SelectedValues!= null && Model.SelectedValues.Contains(available.Id);
check &= (ViewData.ModelState["SelectedValues"] != null) && ViewData.ModelState["SelectedValues"].Value.AttemptedValue.Contains(available.Id);
<input type="checkbox" name="SelectedValues" id="available.Id" value="available.Id" checked="#check"/>#available.FriendlyName<br>
}
}
The ModelState does contain SelectedValues from the previous post, but it doesn't auto-bind, because I have a custom field for the checkboxes.
This code is smelly
Is there a better way to get the data to load from the Attempted Value
EDIT:
Ok, so my question wasn't clear enough, let me clarify.
On a validate, I'm retuning the same view if there was an error.
The modelstate is holding the previously entered values in ModelState["field"].Value.AttemptedValue.
With fields created using the helpers, TextboxFor, CheckboxFor, etc, these values are automatically filled in.
However, when using the normal reflexes for checkbox binding, only the values of the checked checkboxes are returned in the data object passed back to the controller. This means I'm not using the logic that fills values in from the ModelState.
What I've done is dig through the modelstate myself for the attempted values, because they do exist under the field name "SelectedValues". But I have to manually apply them. The value there looks like this.
ModelState["SelectedValues"] = "Value1;Value2;Value4"
Is there a better way to get the data to load from the Attempted Value in the model state.
The primary "smell" (to use your term) I see here is that the code you have in the nested foreach is written directly in your view (*.cshtml), but code of that complexity should be in your Controller action.
You should calculate and generate all the data your view will need in the controller, and then pass that data through to the view using Model (looks like you are already doing that) and you can also use the ViewBag to pass additional data not contained in your Model. Then the view is just responsible to generate the HTML.
That's the other problem I see with your code - you are referencing the ViewData.ModelState which is highly unusual to see in a view. The ModelState should be examined in the controller before you even decide which view to render.
It looks like maybe you are just passing data through ViewData.ModelState that should actually be passed through ViewData/ViewBag.
You can read more about passing data to a view here.
Ok, so basically, I couldn't find anything that will do this for me. The default Html helper methods just don't cover this scenario.
So, I wrote an extension method.
Basically it pulls in the enumerator from the model using the expression you send to it, just like any other helper, but you also send the entry in the list you want to build a checkbox against.
It ends up looking like this.
#Html.CheckboxListEntryFor(x => x.SelectedEntries, AvailableEntries[i].Id)
The method does the following
Get the propertyInfo for the list and check if selected entries contains the values.
Check if the ModelState is invalid, if so, overwrite the checked value with the modelstate entry
build an html checkbox that uses the property name as the name and id of the checkbox, and sets checked based on the previous steps.
public static MvcHtmlString CheckboxListEntryFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper,
Expression<Func<TModel, TProperty>> expression, string entryValue)
{
PropertyInfo info = GetPropertyInfo(typeof (TModel), expression);
var enumerator = info.GetValue(htmlHelper.ViewData.Model);
var check = enumerator != null && ((IList) enumerator).Contains(entryValue);
if (!htmlHelper.ViewData.ModelState.IsValid)
{
check = htmlHelper.ViewData.ModelState[info.Name] != null &&
htmlHelper.ViewData.ModelState[info.Name].Value.AttemptedValue.Contains(entryValue);
}
var fieldString = String.Format(
"<input type=\"checkbox\" name=\"{0}\" id =\"{1}\" value=\"{1}\"{2}/>",
info.Name, entryValue, check ? " checked=\"checked\"" : string.Empty);
return MvcHtmlString.Create(fieldString);
}

DropDownList not getting selected - MVC4

I have the DropDownList in view as below,
#Html.DropDownListFor(model => model.RoleID, (IEnumerable<SelectListItem>)ViewBag.RoleID, new { #class = "dropdownlistCustom" })
#Html.DropDownList("RoleID", (IEnumerable<SelectListItem>)ViewBag.RoleID, new { #class = "dropdownlistCustom" })
#Html.ValidationMessageFor(model => model.RoleID)
Tried both DropDownList and DropDownListFor, both are not getting the correct SelectedValue.
From Controller, I am setting the selectedValue param (see Last parameter of SelectList()) .
public ActionResult Edit(int id = 0)
{
UserDetail userDetail=db.UserDetails.Find(id);
if(userDetail!=null)
{
ViewBag.RoleID = new SelectList(db.Roles.Where(r => r.RoleStatus == "A"), "RoleID", "RoleName", userdetail.RoleID);
return View(userdetail);
}
}
Model :
[Display(Name = "Name Of the Role")]
public int RoleID { get; set; }
[ForeignKey("RoleID")]
public virtual Role Roles { get; set; }
First of all I would encourage you not to use ViewBag, but view models to pass your select list (or data for select list) along from controllers to views.
Second of all, I would encourage you to look at this example or this example (preferably both) of DropDownListFor usage. I think you over-complicating SelectList. Less simple it is, less confused you will be as to why is it not working as expected. Just take existing example and change variable names to yours.
Another big tip why not to use ViewBag is that when you build your view model you can build a select list right there in the controller where you fetch your data from wherever. Then in the view you will only need to use pre-built view model property where SelectList argument is provided - cleaner and nicer. View should do least manipulations with data, but only to present it in the right format in the right way in the right place.
Just look at those examples I provided, I am sure you smart enough to figure it out how to use DropDownListFor properly, its very very easy, you just need to provide right parameters at the right places, see what others do and do the same.
Hope this helps, please keep me posted about your progress.
That's because you're not using DropDownListFor correctly. The function definition you're after is:
DropDownListFor(lambda, selectList, *selectedItem*, htmlAttributes)
RoleId should be passed as the selectedItem parameter, so you're missing the selectList parameter. MVC doesn't create that for you automatically with DropDownListFor, you have to tell it what select list to use by either creating it inline, passing it into ViewBag or setting it on your model and specifying which property to use.
EDIT
Sorry. That's my bad. It's still early here and we lost an hour (daylight savings), so my brain is not all there yet. I mixed DropDownListFor and DropDownList. With DropDownListFor, the selected item comes from the set value on the model, but you still need to provide a proper SelectList. It appears you're trying to specify the selected item where you should be telling it what SelectList to use.
If you were to use DropDownList, then you would need to specify both the SelectList to use and the selected value.

TryUpdateModel - the model of type could not be updated

I'm using Telerik's MVC Grid to edit some records in MVC3, using Razor view.
I call the edit on the controller using the following code:
public ActionResult _CategoriesUpdate(int id)
{
WR_TakeAway_Menu_Categories category = db.WR_TakeAway_Menu_Categories.Where(c => c.ID == id).Single();
TryUpdateModel(category);
db.ApplyCurrentValues(category.EntityKey.EntitySetName, category);
db.ObjectStateManager.ChangeObjectState(category, EntityState.Modified);
db.SaveChanges();
Although this updates the records in the serer, it keeps the grid in edit mode because it was unable to update all the properties of the "category".
If I change TryUpdateModel to UpdateModel it throws an error saying "the model of type WR_TakeAway_Menu_Categories could not be updated"
Is there a better way of doing this, or some way to allow TryUpdateModel to return true to allow the grid to return to display mode?
Without seeing your WR_TakeAway_Menu_Categories class, I'm going to assume that you have some other classes as properties of your WR_TakeAway_Menu_Categories class.
If that is the case, you'll need to exclude the custom objects from the TryUpdateModel method and set those manually before hand.
For example:
db.Entry(category).Reference(c => c.CreatedByUser).CurrentValue = CreatedByUser;
db.Entry(category).Reference(c => c.LastUpdateByUser).CurrentValue = LastUpdateByUser;
This will set your "custom object" variables to the latest value. I have noticed that in some cases if you do not do it this way, and instead just set the property explicitly, the database record will not always get updated.
After you have manually updated the custom properties, then call the TryUpdateModel, excluding the properties that you set manually.
TryUpdateModel<WR_TakeAway_Menu_Categories>(category, null, null, new[] { "CreatedByUser", "LastUpdateByUser" });

ASP.NET MVC Issue with Using Reflection Created Objects with the Default Model Binder

I am having a weird issue in ASP.NET MVC with objects not being updated with UpdateModel when passed a formCollection. UpdateModel does not appear to be working properly when the object being updated is created through reflection.
Scenario: I have an application which has approximately 50 lookup tables--each of which includes exactly the same schema including typical fields like id, title, description, isactive, and createdon. Rather than build 50 views, I wanted to have a single view which could display the data from all of the lookup tables. I created an Interface called IReferenceEntity and implemented it in each of the POCOs representing my lookup tables.
Using this interface, I am able to easily populate a view with a record from the lookup table. (I pass the items to the view via the following.)
System.Web.Mvc.ViewPage<MyNamespece.IReferenceEntity>
From the database to the view, every thing works perfectly.
However, when I attempt to update the model on post, I am running into some problems.
If I explicitly declare an object reference like the following, every thing works perfectly and the values of my object are updated with the values from my form. Hence, I can then update the database.
AccountStatus a = new AccountStatus();
UpdateModel(a, formCollection.ToValueProvider());
Unfortunately, hard coding the object type would completely defeat the reason for using an interface.
(A primary objective of the application is to be able to dynamically add new tables such as lookup tables without having to do anything "special". This is accomplished by reflecting on the loaded assemblies and locating any classes which implement a specific interface or base class)
My strategy is to determine the concrete type of the object at postback and then create an instance of the type through reflection. (The mechanism I use to determine type is somewhat primitive. I include it as a hidden field within the form. Better ideas are welcome.)
When I create an instance of the object using reflection through any of the following methods, none of the objects are being updated by UpdateModel.
Type t = {Magically Determined Type}
object b = Activator.CreatorInstance(t);
UpdateModel(b, formCollection.ToValueProvider());
Type t = {Magically Determined Type}
var c = Activator.CreatorInstance(t);
UpdateModel(c, formCollection.ToValueProvider());
Type t = {Magically Determined Type}
IReferenceEntity d = Activator.CreatorInstance(t);
UpdateModel(d, formCollection.ToValueProvider());
Note: I have verified that the objects which are being created through relection are all of the proper type.
Does anyone have any idea why this might be happening? I am somewhat stumped.
If I was really "hard up", I could create factory object which would many instantiate any one of these reference entity/lookup objects. However, this would break the application's ability to allow for new lookup tables to be added and discovered transparently and is just not quite as clean.
Also, I could try deriving from an actual ReferenceEntity base class as opposed to an interface, but I am doubtful whether this would make any difference. The issue appears to be with using reflection created objects in the modelbinder.
Any help is appreciated.
Anthony
Augi answered this on ASP.NET forums. It worked with only a couple of minor modifications. Thank you Augi.
The problem is that [Try]UpdateModel methods allow to specify model type using generic parameter only so they don't allow dynamic model type specification. I have created issue ticket for this.
You can see TryModelUpdate method implementation here. So it's not difficult to write own overload:
public virtual bool TryUpdateModelDynamic<TModel>(TModel model, string prefix, string[] includeProperties, string[] excludeProperties, IDictionary<string, ValueProviderResult> valueProvider) where TModel : class
{
if (model == null)
{
throw new ArgumentNullException("model");
}
if (valueProvider == null)
{
throw new ArgumentNullException("valueProvider");
}
//Predicate<string> propertyFilter = propertyName => BindAttribute.IsPropertyAllowed(propertyName, includeProperties, excludeProperties);
IModelBinder binder = Binders.GetBinder( /*typeof(TModel)*/model.GetType());
ModelBindingContext bindingContext = new ModelBindingContext()
{
Model = model,
ModelName = prefix,
ModelState = ModelState,
//ModelType = typeof(TModel), // old
ModelType = model.GetType(),
// new
//PropertyFilter = propertyFilter,
ValueProvider = valueProvider
};
binder.BindModel(ControllerContext, bindingContext);
return ModelState.IsValid;
}
Does your IReferenceEntity contain setters on the properties as well as getters? I would think that the last sample would work if the interface had property setters, though you'd have to cast it to get it to compile.
Type t = {Magically Determined Type}
IReferenceEntity d = Activator.CreatorInstance(t) as IReferenceEntity;
UpdateModel(d, formCollection.ToValueProvider());
Normally the reason that it won't set a property on a class is because it can't find a public setter method available to use via reflection.
Just a quick "another thing to try":
UpdateModel(d as IReferenceEntity, formCollection.ToValueProvider());
Not sure if that will work, and I haven't tried it myself, but it's the first thing that came to mind.
If I get a chance later I'll peek at the Default Model Binder code and see if there's anything in there that is obvious...

How do I set a value for the default option with Html.DropDownList

I'm using ASP MVC RC1.
A form I'm using contains a dropdownlist which I have put in a view with this code.
<%= Html.DropDownList("areaid", (SelectList)ViewData["AreaId"], "Select Area Id")%>
However, when rendered, this is what I get
<select id="areaid" name="areaid">
<option value="">Select Area Id</option>
<option value="1">Home</option>
...
</select>
What I'd like is for the Select Area Id option to have a value of 0 and mark it as selected by default so it is consistent with the other values and I can validate whether or not an area has been chosen as it is a mandatory value. AreaId is an integer so when I currently click the form without touching the dropdownlist at all, MVC complains that "" is not an integer and gives me a binding error.
SO how do I set a value for the default option and then make it selected on the form?
Thanks, Dan
I think you have three four options. First when you are building your SelectList or enumerable of SelectItemList, prepend the selection with your option label and default value. Putting it at the top will make it the default if some other value isn't already chosen in the model. Second, you could build the select (and options) "by hand" in the view using a loop to create the options. Again, prepending your default selection if one isn't supplied in the model. Third, use the DropDownList extension, but modify the value of the first option using javascript after the page is loaded.
It doesn't seem to be possible to use the DropDownList extension to assign a value to an optionLabel as it is hard-coded to use string.Empty. Below is the relevant code snippet from http://www.codeplex.com/aspnet.
// Make optionLabel the first item that gets rendered.
if (optionLabel != null) {
listItemBuilder.AppendLine(ListItemToOption(new SelectListItem() { Text = optionLabel, Value = String.Empty, Selected = false }));
}
EDIT: Finally, the best way is to have your model take a Nullable value and mark it as required using the RequiredAttribute. I would recommend using a view-specific model rather than an entity model for the view. Since the value is Nullable, the empty string will work fine if posted back without choosing a value. Setting it as a required value will cause the model validation to fail with an appropriate message that the value is required. This will allow you to use the DropdownList helper as is.
public AreaViewModel
{
[Required]
public int? AreaId { get; set; }
public IEnumerable<SelectListItem> Areas { get; set; }
...
}
#Html.DropDownListFor( model => model.AreaId, Model.Areas, "Select Area Id" )
For MVC3, SelectList has an overload whereby you can define the selected value.
Function Create() As ViewResult
ViewBag.EmployeeId = New SelectList(db.Employees, "Id", "Name", 1)
Return View()
End Function
In this case, I happen to know that 1 is the id of the default list item I want, but presumably you could select the default via query or what ever floats your boat
Instead of passing the default item from the definition in the View, You can add the "Select Area" data at the 0th index of the List from the controller.
This way the Select Area data has an index value of 0.
I wanted to use the same SelectList for multiple drop downs and didn't want to duplicate the SelectList in the model so I just added a new Html Extension method that took in a value and set the selected item.
public static MvcHtmlString DropDownList(this HtmlHelper htmlHelper, string name, string value, IList<SelectListItem> selectList, object htmlAttributes)
{
IEnumerable<SelectListItem> items = selectList.Select(s => new SelectListItem {Text = s.Text, Value = s.Value, Selected = s.Value == value});
return htmlHelper.DropDownList(name, items, null /* optionLabel */, htmlAttributes);
}

Resources