+2  A: 

Blind guess:

change:

<%= Html.TextBox("Complainants[" + i + "].Surname", complainant.Surname)%>

with:

<%= Html.TextBox("Complaint.Complainants[" + i + "].Surname",  
complainant.Surname)%>

Respectively - add "Complaint." before "Complainants[..."

EDIT:

This is NOT a right answer. Left it undeleted just because that might add some value until proper answer pops up.

EDIT2:

I might be wrong, but for me it seems there's problem with entity framework (or - with the way you use it). I mean - asp.net mvc manages to read values from request but can't initialize complainants collection.

Here it's written:

The InitializeRelatedCollection(TTargetEntity) method initializes an existing EntityCollection(TEntity) that was created by using the default constructor. The EntityCollection(TEntity) is initialized by using the provided relationship and target role names.

The InitializeRelatedCollection(TTargetEntity) method is used during deserialization only.

Some more info:

Exception:

  • InvalidOperationException

Conditions:

  • When the provided EntityCollection(TEntity) is already initialized.
  • When the relationship manager is already attached to an ObjectContext.
  • When the relationship manager already contains a relationship with this name and target role.

Somewhy InitializeRelatedCollection gets fired twice. Unluckily - i got no bright ideas why exactly. Maybe this little investigation will help for someone else - more experienced with EF. :)

EDIT3:
This isn't a solution for this particular problem, more like a workaround, a proper way to handle model part of mvc.

Create a viewmodel for presentation purposes only. Create a new domain model from pure POCOs too (because EF will support them in next version only). Use AutoMapper to map EFDataContext<=>Model<=>ViewModel.

That would take some effort, but that's how it should be handled. This approach removes presentation responsibility from your model, cleans your domain model (removes EF stuff from your model) and would solve your problem with binding.

Arnis L.
I've tried this but ModelState.IsValid is still returning false. ModelState Keys are populated with Compliants[0].Surname, Complainants[1].Surname, etc. The thing that is causing it to fail is there's a key called Complainants. This is what is giving me the error "The EntityCollection has already been initialized. The InitializeRelatedCollection method should only be called to initialize a new EntityCollection during deserialization of an object graph".
woopstash
It may be a problem when using EntityCollections. This link: http://www.distribucon.com/blog/ASPNETMVCRC1BindingAList.aspx breifly talks about it but I don't know enough about all of this to decide whether or not this is the reason it's not working.
woopstash
@woopstash Are you using entity framework?
Arnis L.
I have the same problem. The items in the collection get bound, but ModelState has the error in it. The bound collection looks fine but it's RelationshipSet is null. This may be a clue, or a red herring.
Richard
@Arnis L., yes I'm using EF. Due to the complexities and my resistance to workarounds, I'm going to go a different route. Thanks for your help
woopstash
I'm happy if that helped. :)
Arnis L.
Yeah, very not impressed w/ EF so far. Things that are easy w/ NHibernate and other ORMs seem like a steep mountain in Ef. That said, I almost always follow your "workaround" of using a viewmodel instead of the actual domain entity in any application of significance anyway, just b/c the layered architecture often means that objects have different shapes in different layers.
Paul
A: 

Any confirmed solution for this?

Chris Oliver
I believe I've tried the solution posed by Arnis, but I'll confirm whether or not it works. Stay tuned...
woopstash
A: 

I had the identical problem! In the end you will find out that the framework can't handle complex models.

I wrote a little binding component that can initialize the complex bindings on a post.

But basically what you have to do is what Arnis L. is telling.

Dejan
+1  A: 

I worked around the ModelBinding exception by doing the following:

// Remove the error from ModelState which will have the same name as the collection.
ModelState.Remove("Complaints"/*EntityCollection*/); 
if (ModelState.IsValid) // Still catches other errors.
{
    entities.SaveChanges(); // Your ObjectContext
}

The main drawback is that the exception is still thrown and that can be expensive at runtime. The elegant work around may be to create a wrapper around the existing DefaultBinder and prevent it from instantiating the EntityCollection again, which is already done by EF. Then binding that collection to the form values (FormCollection).

Keep in mind if you're binding more than one collection, you will need to remove the error for each collection.

In my experiment, the collection saved successfully as well as the root object in the graph which the collection was part of.

Hope that helps someone else.

Dax70
Is that the only difference from @woopstash's code and does your complaints collection definitely save? I don't think ApplyPropertyChanges will save navigation properties or collections. When I've tried it just saves the scaler values and the documentation seems to confirm this.
Richard
A: 
public ActionResult Edit([Bind(Exclude = "Complainants")]Complaint model)
{
  TryUpdateModel(model.Complainants, "Complainants");
  if (!ModelState.IsValid)
  {
      // return the pre populated model
      return View(model);
  }

}

This works for me!

I think that when Complaint object gets created then its 'Complainants' collection gets initialized (because of entity framework auto logic) and then model binder tries to create the collection itself as well, which causes the error. But when we try to update the model manually then collection is already initialized but model binder is not asked to initialize it again.

scorpio
A: 

To get this to work without case-by-case workarounds you need to create your own model binder and override method SetProperty:

public class MyDefaultModelBinder : DefaultModelBinder
{
    protected override void SetProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, object value)
    { 
        ModelMetadata propertyMetadata = bindingContext.PropertyMetadata[propertyDescriptor.Name]; 
        propertyMetadata.Model = value;
        string modelStateKey = CreateSubPropertyName(bindingContext.ModelName, propertyMetadata.PropertyName);

        // Try to set a value into the property unless we know it will fail (read-only 
        // properties and null values with non-nullable types)
        if (!propertyDescriptor.IsReadOnly) { 
        try {
            if (value == null)
            {
            propertyDescriptor.SetValue(bindingContext.Model, value);
            }
            else
            {
            Type valueType = value.GetType();

            if (valueType.IsGenericType && valueType.GetGenericTypeDefinition() == typeof(EntityCollection<>))
            {
                IListSource ls = (IListSource)propertyDescriptor.GetValue(bindingContext.Model);
                IList list = ls.GetList();

                foreach (var item in (IEnumerable)value)
                {
                list.Add(item);
                }
            }
            else
            {
                propertyDescriptor.SetValue(bindingContext.Model, value);
            }
            }

        }
        catch (Exception ex) {
            // Only add if we're not already invalid
            if (bindingContext.ModelState.IsValidField(modelStateKey)) { 
            bindingContext.ModelState.AddModelError(modelStateKey, ex); 
            }
        } 
        }
    }
}

Don't forget to register your binder in Global.asax:

ModelBinders.Binders.DefaultBinder = new MyDefaultModelBinder();
Jake E