Reputation: 315
I need to persist changes to a view model that contains a collection, but everytime i post back to the controller, i am losing my model bindings. I am fairly new to MVC so i may be missing something glaring here.
@{ Html.RenderAction("TabList", "TabController", new {Id = Model.Id}); }
I have a main container page that has a render action to a controller to return the first partial view.
[HttpGet]
public ViewResult TabList(Guid orderid)
{
// build the viewmodel
return View("ControlTabList", model);
}
From there iterate over the collection and different render partials based on the object type. (I have simplified the code here, as the items are polymorphic and have some type of downcasting)
@model TabListViewModel
@using (Html.BeginForm("UpdateItem", "TabController", FormMethod.Post, new {Id = "myForm"}))
{
@Html.AntiForgeryToken()
<input type="submit" value="Send" id="submitButton"/>
@for (int i = 0, c = this.Model.Count; i < c; i++)
{
var currentItem = this.Model.ElementAt(i);
@switch (currentItem.Code)
{
case "1":
Html.RenderPartial("Partials/ItemOne", currentItem);
break;
case "2":
Html.RenderPartial("Partials/ItemTwo",currentItem);
break;
default:
Html.RenderPartial("Partials/ItemThree",currentItem);
break;
}
}
}
When I post back to the controller my ViewModel will always be null.
[HttpPost]
public ActionResult UpdateItems(TabListViewModel model)
{
/* i will remove the redirect here, as the model above is always null*
}
Is there a reason why i am losing the bindings? I would like to save the entire collection, instead of individually saving each item in the collection.
Upvotes: 1
Views: 5146
Reputation: 315
I updated the following code using the EditorTemplate to get rid of the heavy switch statements i was using to iterate over the polymorphic collection of derived types.
@for (int i = 0, c = this.Model.Count; i < c; i++)
{
var currentItem = this.Model.ElementAt(i);
@Html.EditorFor(model => currentItem)
}
I then added the following prefix in all of my EditorTemplates to prevent duplicate name and ids of the controls.
@{
ViewData.TemplateInfo.HtmlFieldPrefix = Model.ProvinceCode;
}
I created a custom ModelBinder to intercept and bind the form data to create a ViewModel i can use.
[HttpPost]
public ActionResult UpdateItem([ModelBinder(typeof(ProvinceCustomModelBinder))]ProvinceVM model)
Upvotes: -1
Reputation:
There are 2 reasons why your implementation will fail.
First is the use of partials to display each item in the collection. If you inspect the html your generating you will see that the id
and name
attributes for each currentItem
are identical. Duplicate id
's are invalid html and duplicate name attributes means that you cannot bind back to a collection. Assuming currentItem
has a property string Name
, then the correct name would be <input name="Name[0]../>
, <input name="Name[1]../>
etc. Note the indexers in the name attribute, which allow you to bind to a collection.
Second, your TabListViewModel
appears to be collection of a base type where you have added derived types. When you post back, the DefaultModelBinder
will only initialize items of the base type since it has no way of knowing which derived type to initialize. From your last question, I assume the base type is ProvinceViewModel
and the derived types are QuebecViewModel
and OntarioViewModel
. Since ProvinceViewModel
is abstract
, it cant be initialized (no constructor) so your model will always be null. While it is possible to write a custom abstract ModelBinder
, as a self confessed newbie, this might be best left until you have a better knowledge of MVC and the model binding process (this article will help you get started)
The easiest way to solve this is with a view model containing collections of each type and use for
loops or custom EditorTemplate
s. For example
View model
public class ProvinceVM
{
public List<QuebecViewModel> QuebecProvinces { get; set; }
public List<OntarioViewModel> OntarioProvinces { get; set; }
}
Then create an EditTemplate
for each type
In /Views/Shared/EditorTemplates/QuebecViewModel.cshtml
@model QuebecViewModel
@Html.TextBoxFor(m => m.someProperty)
....
Then in the main view
@model ProvinceVM
@using (Html.BeginForm())
{
@Html.EditorFor(m => m.QuebecProvinces)
// Ditto for OntarioProvinces, or you can use a `for` loop as follows
for(int i = 0; i < Model.OntarioProvinces.Count; i++)
{
@Html.TextBoxFor(m => m.OntarioProvinces[i].someProperty)
....
}
}
Note that both options will generate controls such as
<input name="QuebecProvinces[0].someProperty" ..../>
<input name="QuebecProvinces[1].someProperty" ..../>
which will be correctly bound when you post back to
public ActionResult UpdateItem(ProvinceVM model) // suggest you use a more appropriate name (at least pluralize it)
Upvotes: 3
Reputation:
There seems to be way too much logic in the view. MVC was created to make use of Soc - separation of concerns ( however the view itself it not directly an observable object from the model ) . This enables a developer to write clear and precise code for each part of the system. It however, because of its flexibility as a framework , makes the decision of this up to the developer.
The idea is for clean views, light controllers and heavy classes.
You seem to be running in circles a bit here. From your code it seems to display a view that then returns a view that renders a partialview based on a parameter that is sent to it by the controller (which is building the viewmodel for the partialview ) which is received by the render action id property.
I think there are some clear considerations that need to be implemented for the codes maintainability
First off. It seems as though you want to achieve the display of a partialview. This partialview is determined based on a parameter, the Model.Id
. You do not say how this parameter is being injecting into the RenderAction
but that should not matter.
@{ Html.RenderAction("TabList", "TabController", new {Id = Model.Id}); }
This code above does not need to exist. It is calling itself. And if that is not the case ( i might not fully understand why you have that there ) then that should be replaced with the call to the partial view, With the main view that the code is sitting on being injected with the ModelId
parameter . In this use case that is the View TabList
itself.
[HttpGet]
// Model id is passed into the controller in which ever fashion it was
// used to pass into the RenderAction method
public ViewResult TabList(int modelId = 3) // allows for a default on 3
{
// build the viewmodel
return View();
}
"Build the viewmodel" is where the main code happens, this is the stuff that determines what is going to be displayed, and so because it is not directly a display element it needs to sit in the controller. Think of it like this. When the view gets its model, everything the view needs to display should already be in the model. And If you are using polymorphism then there is no reason to be writing switch cases inside of for loops in a view . Just no. That should be sent off to its corresponding interfaces
So to build the viewmodel could be something like this
//build viewmodel
TabListViewModel model = new TabListViewModel{
PartialStuff = dbcontext.entity.FirstOrDefault(_ => _.ModelId == modelId),
}
So now you have a viewmodel with objects that has been filtered based on the modelId , and now just return that model.
return View(model);
The View then declares that model
@model TabListViewModel
And you have one partial view that receives only the objects it needs based on the model
@Render.PartialView("_ItemStuff",Model.PartialStuff)
The partial view then has its model defined as "Partialstuff" and the form objects are created by that model . This allows for a strongly typed model that will pass values back to the controller.
@model PartialStuff
@using (Html.BeginForm("UpdateItem", "TabController", FormMethod.Post, new{Id = "myForm"}))
{
@Html.AntiForgeryToken()
<input type="submit" value="Send" id="submitButton"/>
// Add values with the model directly attached
@Html.TextBoxFor(_ => _.stuffFromTheModel)
Upvotes: 1