Custom model binder for child collection

2019-07-27 11:47发布

问题:

I've searched high and low so hopefully someone can help me. I have a class:

public class Person
{
    public string Name { get; set; }
    public ICollection<Toys> { get; set; }
}

I have a controller method:

public ActionResult Update(Person toycollector)
{
....
}

I want to bind to the collection. I realize I will only get the IDs but I will deal with that in my controller. I just need to be able to flip through the collection of IDs. I started writing a model binder:

public class CustomModelBinder : DefaultModelBinder
    {
        protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor)
        {
            if (propertyDescriptor.PropertyType == typeof(ICollection<Toys>))
            {
                //What do I do here???
            }
}

So how do I build the collection of Toys from the values passed into my method? Thanks!

EDIT: Looks like I couldn't post this answer to my own question so I'll just edit my post. looks like all you have to do is parse out the data and add it to the model like so:

if (propertyDescriptor.PropertyType == typeof(ICollection)) {

            var incomingData = bindingContext.ValueProvider.GetValue("Edit." + propertyDescriptor.Name + "[]");
            if (incomingData != null)
            {
                ICollection<Toy> toys = new List<Toy>();
                string[] ids = incomingData.AttemptedValue.Split(',');
                foreach (string id in ids)
                {
                    int toyId = int.Parse(id);
                    toys.Add(new Toy() { ToyID = toyId });
                }
                var model = bindingContext.Model as Person;
                model.Toys = toys;
            }
            return;
        }

回答1:

You shouldn't need a custom model binder for this.

Refer to this post by Phil Haack for the full implementation, but the basic idea is that for each item in the collection, you make a form field that follows the following naming convention:

Toys[index].<FieldName>

So, for example, if you wanted to bind 3 Toy objects:

<input type="hidden" name="Toys[0].Id" value="1" />
<input type="hidden" name="Toys[1].Id" value="2" />
<input type="hidden" name="Toys[2].Id" value="3" />

The important part is that all index values are accounted for and no indexes are skipped. For example, if you have a form value Toys[1].<FieldName> you must also have a Toys[0].<FieldName> value.

All this being said, depending on what exactly you need to accomplish it may be easier to simply bind to a collection of Ids rather than whole objects. You can let your controller action translate the Ids to actual models.

If you prefer the simpler, Id-only approach, all you need to do is make a string/int/guid (whatever your id is) collection object in your request model and then create 1 or more fields all having the same name for each Id value. The default model binder will automatically handle creating the collection from the values in the request.



回答2:

Building on the above answer for n amount of toys, you must use a for loop to index correctly, like so:

@for(var i = 0; i < Model.Toys.Count; i++)
{
    @Html.HiddenFor(m => m.Toys[i].Id)
}

Then default model binding will auto-bind the collection on post.