mflair2000
mflair2000

Reputation: 315

Losing ViewModel on Partial View post back to controller

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

Answers (3)

mflair2000
mflair2000

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

user3559349
user3559349

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 EditorTemplates. 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

user1752532
user1752532

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

Related Questions