views:

958

answers:

3

I'm not sure if this is a bug in the DefaultModelBinder class or what. But UpdateModel usually doesn't change any values of the model except the ones it found a match for. Take a look at the following:

[AcceptVerbs(HttpVerbs.Post)]
public ViewResult Edit(List<int> Ids)
{
    // Load list of persons from the database
    List<Person> people = GetFromDatabase(Ids);
 // shouldn't this update only the Name & Age properties of each Person object
 // in the collection and leave the rest of the properties (e.g. Id, Address)
 // with their original value (whatever they were when retrieved from the db)
    UpdateModel(people, "myPersonPrefix", new string[] { "Name", "Age" });
    // ...
}

What happens is UpdateModel creates new Person objects, assign their Name & Age properties from the ValueProvider and put them in the argument List<>, which makes the rest of the properties set to their default initial value (e.g. Id = 0) so what is going on here?

+6  A: 

UPDATE: I stepped through mvc source code (particularly DefaultModelBinder class) and here is what I found:

The class determines we are trying to bind a collection so it calls the method: UpdateCollection(...) which creates an inner ModelBindingContext that has a null Model property. Afterwards, that context is sent to the method BindComplexModel(...) which checks the Model property for null and creates a new instance of the model type if that is the case.

That's what causes the values to be reset.

And so, only the values that are coming through the form/query string/route data are populated, the rest remains in its initialized state.

I was able to make very few changes to UpdateCollection(...) to fix this problem.

Here is the method with my changes:

internal object UpdateCollection(ControllerContext controllerContext, ModelBindingContext bindingContext, Type elementType) {
IModelBinder elementBinder = Binders.GetBinder(elementType);

// build up a list of items from the request
List<object> modelList = new List<object>();
for (int currentIndex = 0; ; currentIndex++) {
 string subIndexKey = CreateSubIndexName(bindingContext.ModelName, currentIndex);
 if (!DictionaryHelpers.DoesAnyKeyHavePrefix(bindingContext.ValueProvider, subIndexKey)) {
  // we ran out of elements to pull
  break;
 }
 // **********************************************************
 // The DefaultModelBinder shouldn't always create a new
 // instance of elementType in the collection we are updating here.
 // If an instance already exists, then we should update it, not create a new one.
 // **********************************************************
 IList containerModel = bindingContext.Model as IList;
 object elementModel = null;
 if (containerModel != null && currentIndex < containerModel.Count)
 {
     elementModel = containerModel[currentIndex];
 }
     //*****************************************************
 ModelBindingContext innerContext = new ModelBindingContext() {
  Model = elementModel, // assign the Model property
  ModelName = subIndexKey,
  ModelState = bindingContext.ModelState,
  ModelType = elementType,
  PropertyFilter = bindingContext.PropertyFilter,
  ValueProvider = bindingContext.ValueProvider
 };
 object thisElement = elementBinder.BindModel(controllerContext, innerContext);

 // we need to merge model errors up
 VerifyValueUsability(controllerContext, bindingContext.ModelState, subIndexKey, elementType, thisElement);
 modelList.Add(thisElement);
}

// if there weren't any elements at all in the request, just return
if (modelList.Count == 0) {
 return null;
}

// replace the original collection
object collection = bindingContext.Model;
CollectionHelpers.ReplaceCollection(elementType, collection, modelList);
return collection;

}

Marco M.
A: 

You just gave me an idea to dig into ASP.NET MVC 2 source code. I have been struggling with this for two weeks now. I found out that your solution will not work with nested lists. I put a breakpoint in the UpdateCollection method ,and it never gets hit. It seems like the root level of model needs to be a list for this method to be called

This is in short the model I have..I also have one more level of generic lists, but this is just a quick sample..

public class Borrowers
{
   public string FirstName{get;set;}
   public string LastName{get;set;}
   public List<Address> Addresses{get;set;}
}

I guess that, I will need to dig deeper to find out what is going on.

UPDATE: The UpdateCollection still gets called in asp.net mvc 2, but the problem with the fix above is related to this HERE

+2  A: 

Rudi Breedenraed just wrote an excellent post describing this problem and a very helpful solution. He overrides the DefaultModelBinder and then when it comes across a collection to update, it actually updates the item instead of creating it new like the default MVC behavior. With this, UpdateModel() and TryUpdateModel() behavior is consistent with both the root model and any collections.

thekaido