MVC View - Inconsistent model values - asp.net-mvc

I've encountered what seems to be a strange inconsistency in the values of properties in my model.
I have the following controller action...
<Route("news/edit")>
<HttpGet>
Function EditIndex(Optional filter As Models.NewsFilter = Nothing) As ActionResult
filter.Start = 0
filter.Count = 100
Return View("EditIndex", filter)
End Function
Most of the properties in the model come from the querystring, but my controller explicitly setting "Start" and "Count", and then passing the model to the view.
My view is declared like so...
#inherits System.Web.Mvc.WebViewPage(Of Models.NewsFilter)
The odd thing is that if I simply display the models properties within the view like...
Start=#Model.Start
Count=#Model.Count
They display the values as defined in my controller. However when I use the following...
#Html.EditorFor(Function(m) m.Start, New With {.class = "form-control"})
#Html.EditorFor(Function(m) m.Count, New With {.class = "form-control"})
The values displayed are the original values from the QueryString. It's ignoring the values that I've set in my controller.
I don't understand why this happens. Is it trying to be clever and automatically checking the QueryString parameters because it's within a form?

Related

ASP.NET MVC multiselect DropDownFor/listBoxFor [duplicate]

I have read many articles about using MultiSelectList and have yet to understand what is going wrong with my DropDownListFor. I have a ListBoxFor with the same View, ViewModel and data that works fine. I want to use the DropDownListFor because of its optionLabel parameter that ListBoxFor doesn't have.
When the View is first loaded, both the DropDownListFor and the ListBoxFor show the multiple selected items.
When the Submit button is clicked, the selected items collection is posted back to the Controller action okay and the view is refreshed with the ListBoxFor still showing both selected items but the DropDownListFor is only showing one selected item.
The controller action is constructing the MultiSelectList like this:
vm.TasksFilterGroup.Assignees = new MultiSelectList(employees, "Id", "FullName", new string[] { "51b6f06a-e04d-4f98-88ef-cd0cfa8a2757", "51b6f06a-e04d-4f98-88ef-cd0cfa8a2769" });
The View code looks like this:
<div class="form-group">
<label>ListBoxFor</label>
#Html.ListBoxFor(m => m.TasksFilterGroup.SelectedAssignees, Model.TasksFilterGroup.Assignees, new { #class = "form-control", multiple = "multiple" })
</div>
<div class="form-group">
<label>DropDownListFor</label>
#Html.DropDownListFor(m => m.TasksFilterGroup.SelectedAssignees, Model.TasksFilterGroup.Assignees, new { #class = "form-control", multiple = "multiple" })
</div>
Why does the DropDownListFor lose the multiple selection after Submit but the ListBoxFor doesn't?
As the names of the methods imply, DropDownListFor() is for creating a <select> (to select 1 option) and ListBoxFor() is for creating a <select multiple> (to select multiple options). While both methods share a lot of common code, they do produce different results.
Adding the multiple="multiple" attribute changes the display, but it does not change the functionality of the code executed by these methods.
If you inspect the source code, you will note that all the overloads of DropDownListFor() ultimately call the private static MvcHtmlString DropDownListHelper() method, and similarly ListBoxFor() ultimately calls the private static MvcHtmlString ListBoxHelper() method.
Both these methods call the private static MvcHtmlString SelectInternal() method, but the difference is that DropDownListHelper() passes allowMultiple = false while the ListBoxHelper() passes allowMultiple = true.
Within the SelectInternal() method, the key line of code is
object defaultValue = (allowMultiple) ? htmlHelper.GetModelStateValue(fullName, typeof(string[])) : htmlHelper.GetModelStateValue(fullName, typeof(string));
The value of defaultValue is then used when building html for the <option> elements and is used to set the selected attribute(s).
In the case of ListBoxFor(), the value of defaultValue will be the array defined by your SelectedAssignees property. In the case of DropDownListFor() it returns null because the value of your property cannot be cast to string (its an array).
Because defaultValue is null, none of the <option> elements have the selected attribute set and you lose model binding.
As a side note, if you were to set the values of SelectedAssignees in the GET method before you pass the model to the view, you will see that none of them are selected when using DropDownListFor() for the same reasons described above.
Note also that the code for generating the SelectList should just be
vm.TasksFilterGroup.Assignees = new SelectList(employees, "Id", "FullName" });
There is no point setting the 3rd parameter when using either the DropDownListFor() or ListBoxFor() methods because its the value of the property your binding to (SelectedAssignees) that determines which options are selected (the 3rd parameter is ignored by the methods). If you want the options matching those Guid values to be selected, then in the GET method, use
vm.TasksFilterGroup.SelectedAssignees= new string[]{ "51b6f06a-e04d-4f98-88ef-cd0cfa8a2757", "51b6f06a-e04d-4f98-88ef-cd0cfa8a2769" };

Why does the DropDownListFor lose the multiple selection after Submit but the ListBoxFor doesn't?

I have read many articles about using MultiSelectList and have yet to understand what is going wrong with my DropDownListFor. I have a ListBoxFor with the same View, ViewModel and data that works fine. I want to use the DropDownListFor because of its optionLabel parameter that ListBoxFor doesn't have.
When the View is first loaded, both the DropDownListFor and the ListBoxFor show the multiple selected items.
When the Submit button is clicked, the selected items collection is posted back to the Controller action okay and the view is refreshed with the ListBoxFor still showing both selected items but the DropDownListFor is only showing one selected item.
The controller action is constructing the MultiSelectList like this:
vm.TasksFilterGroup.Assignees = new MultiSelectList(employees, "Id", "FullName", new string[] { "51b6f06a-e04d-4f98-88ef-cd0cfa8a2757", "51b6f06a-e04d-4f98-88ef-cd0cfa8a2769" });
The View code looks like this:
<div class="form-group">
<label>ListBoxFor</label>
#Html.ListBoxFor(m => m.TasksFilterGroup.SelectedAssignees, Model.TasksFilterGroup.Assignees, new { #class = "form-control", multiple = "multiple" })
</div>
<div class="form-group">
<label>DropDownListFor</label>
#Html.DropDownListFor(m => m.TasksFilterGroup.SelectedAssignees, Model.TasksFilterGroup.Assignees, new { #class = "form-control", multiple = "multiple" })
</div>
Why does the DropDownListFor lose the multiple selection after Submit but the ListBoxFor doesn't?
As the names of the methods imply, DropDownListFor() is for creating a <select> (to select 1 option) and ListBoxFor() is for creating a <select multiple> (to select multiple options). While both methods share a lot of common code, they do produce different results.
Adding the multiple="multiple" attribute changes the display, but it does not change the functionality of the code executed by these methods.
If you inspect the source code, you will note that all the overloads of DropDownListFor() ultimately call the private static MvcHtmlString DropDownListHelper() method, and similarly ListBoxFor() ultimately calls the private static MvcHtmlString ListBoxHelper() method.
Both these methods call the private static MvcHtmlString SelectInternal() method, but the difference is that DropDownListHelper() passes allowMultiple = false while the ListBoxHelper() passes allowMultiple = true.
Within the SelectInternal() method, the key line of code is
object defaultValue = (allowMultiple) ? htmlHelper.GetModelStateValue(fullName, typeof(string[])) : htmlHelper.GetModelStateValue(fullName, typeof(string));
The value of defaultValue is then used when building html for the <option> elements and is used to set the selected attribute(s).
In the case of ListBoxFor(), the value of defaultValue will be the array defined by your SelectedAssignees property. In the case of DropDownListFor() it returns null because the value of your property cannot be cast to string (its an array).
Because defaultValue is null, none of the <option> elements have the selected attribute set and you lose model binding.
As a side note, if you were to set the values of SelectedAssignees in the GET method before you pass the model to the view, you will see that none of them are selected when using DropDownListFor() for the same reasons described above.
Note also that the code for generating the SelectList should just be
vm.TasksFilterGroup.Assignees = new SelectList(employees, "Id", "FullName" });
There is no point setting the 3rd parameter when using either the DropDownListFor() or ListBoxFor() methods because its the value of the property your binding to (SelectedAssignees) that determines which options are selected (the 3rd parameter is ignored by the methods). If you want the options matching those Guid values to be selected, then in the GET method, use
vm.TasksFilterGroup.SelectedAssignees= new string[]{ "51b6f06a-e04d-4f98-88ef-cd0cfa8a2757", "51b6f06a-e04d-4f98-88ef-cd0cfa8a2769" };

One Model field not populating when the rest of the Model is

I'm using an MVC 3 view which has a Model inherited:
Inherits="System.Web.Mvc.ViewPage<MyModel>"
I use html helpers to populate model properties:
<td><%=Html.TextBoxFor(i => Model.partA, new { #style = "width:25px;", #maxLength = 3})%></td>
<td><%=Html.TextBoxFor(t => Model.partB, new { #style = "width:35px;", #maxLength = 5 })%></td>
The problem is in my controller 'partB' gets the value from my TextBoxFor helper but 'partA' is empty. There's other parts of the model but I use this as an example.
It's the same model based on the same table in my entity framework. I checked the properties and they're both intergers with Nullable set to (none). In SQl Server MS, the table fields it's refering to appear to be the same also. Both are set (int, null).
I don't see what the difference is that causes partB to be populated and partA to be empty.
What else can I check?
OK, it was because I had a textbox also named 'partA' in a control being rendered in my page. The empty value in the control seemed to be overwriting 'partA' on my view.

View does not affect models changes

I have an get action, which has a couple of parameters. In view I have a form, that fills after get action. Action creates an instance of model using received parameters, but if parameter has special value then action set to model some default values despite parameters. After that action return view with model.
The problem is that after changing the model in View shows data of the parameters, and not from the modified model.
This happens because, by design, all HTML helpers first look at the ModelState when binding and after that in the model itself. So if you intend to modify some value that was part of the POST request you will need to remove it from ModelState first if you want this change to be reflected in the view:
[HttpPost]
public ActionResult SomeAction(MyViewModel model)
{
// We remove the Bar property that was part of the request because
// we modify its value here
ModelState.Remove("Bar");
model.Bar = "Some modified value";
return View(model);
}
This assumes that in the corresponding view you have an input field for it:
#Html.EditorFor(x => x.Bar)

Why is a value that I update in my model inside an MVC3 controller not rendered on the client?

I have a controller action UpdateCustomer(CustomerDto customer) that returns a PartialViewResult with a model that is also a CustomerDto:
[HttpPost]
public PartialViewResult UpdateCustomer(CustomerDto customer)
{
CustomerDto updatedCustomer = _customerService.UpdateCustomer(customer);
updatedCustomer.Name = "NotThePostedName";
return PartialView("CustomerData", updatedCustomer);
}
In my view, I have the following line:
#Html.TextBoxFor(model => model.Name)
So far, so good. In my view I do an asynchronous post to this action method, the model binder does its work and I can update a customer in the database. Then I want to render the updated customer to the client. For example, I'd like to change the customer name in my controller. However, what gets rendered is always the properties from the posted customer, not the properties from updatedCustomer.
I decided to include the MVC3 source code in my project to see what really happens. It appears to be a feature (bug?) of MVC3 that it always takes the value from ViewData.ModelState instead of the value from ViewData.Model.
This happens at lines 366-367 of System.Web.Mvc.Html.InputExtensions:
string attemptedValue =
(string) htmlHelper.GetModelStateValue(fullName, typeof(string));
tagBuilder.MergeAttribute("value",
attemptedValue ?? ((useViewData)
? htmlHelper.EvalString(fullName)
: valueParameter), isExplicitValue);
As you can see, attemptedValue comes from ModelState. It contains the old value for CustomerDto.Name (the value that was posted to the controller action).
If this is a feature, why does it work this way? And is there a way to work around it? I would expect that if I update my model, the update gets rendered, not the old value I posted.
Well yes it's a feature (ModelState is always checked before actual Model), you can clear the ModelState, or update just the value you need:
ModelState["Name"].Value = updatedCustomer.Name;

Resources