1

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.

Brendan Green
  • 11,676
  • 5
  • 44
  • 76
mflair2000
  • 315
  • 3
  • 13
  • Your post doesn't have `[ValidateAntiForgeryToken]` – Yuriy Faktorovich Apr 13 '15 at 19:24
  • I forgot to include, but it should be there in example – mflair2000 Apr 13 '15 at 19:25
  • Check your posted data to see why it's not going into the model. Maybe the names are different or whatever. It lives in `Request.Form`. The names should match properties within your model. – Yuriy Faktorovich Apr 13 '15 at 19:34
  • When i see the request.form or stream reader, i see the field properties coming back from the partial, but not the ViewModel or collection of objects i was expecting. – mflair2000 Apr 13 '15 at 20:26
  • Are the names of the properties the same as the properties in the ViewModel? Does the ViewModel have an empty constructor? – Yuriy Faktorovich Apr 13 '15 at 20:47
  • Think about it from the perspective of the individual form fields. This might help: http://stackoverflow.com/questions/4949991/binding-collections-in-mvc – Trey Gramann Apr 13 '15 at 21:03
  • Why is the view calling itself and then calling a `PartialView` ? Are all those partial views the same , besides the model that is being passed into it ? –  Apr 13 '15 at 21:14
  • Your implementation will be generating controls with duplicate `id` attributes (invalid html) and `name` attributes (no indexers) which cannot be bound to a collection. And the fact you are rendering different partials for the same type means you could not bind the item anyway without a custom `ModelBinder` anyway. –  Apr 13 '15 at 22:12
  • The collection is actually a polymorphic collection, so the types are not the same. I simplified this as, i am checking the type and casting before sending to the Partial. ie. (ItemOne)currentItem – mflair2000 Apr 13 '15 at 23:01
  • @mflair2000, The fact that they are different types means that this will never bind on post back unless you create a custom `ModelBinder` (even if you did fix the naming issue) –  Apr 13 '15 at 23:25
  • Thanks, a custom ModelBinder got me on my way here. – mflair2000 Apr 14 '15 at 12:50

3 Answers3

3

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)
Community
  • 1
  • 1
  • Correct, the duplicate names and ids of the derived types were causing the problems. I did use a custom model binder as well as editorTemplates here. – mflair2000 Apr 14 '15 at 12:54
1

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)
-1

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)
mflair2000
  • 315
  • 3
  • 13