Mike Simmons
Mike Simmons

Reputation: 1298

MVC 3 ModelBinding to a collection with ModelBinderAttribute

Is it possible to bind to a collection using the ModelBinderAttribute?

Here's my action method parameter:

[ModelBinder(typeof(SelectableLookupAllSelectedModelBinder))] List<SelectableLookup> classificationItems

And here's my custom model binder:

public class SelectableLookupAllSelectedModelBinder : IModelBinder
{
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var model = bindingContext.Model as SelectableLookup ??
                             (SelectableLookup)DependencyResolver.Current.GetService(typeof(SelectableLookup));

        model.UId = int.Parse(bindingContext.ValueProvider.GetValue(bindingContext.ModelName).AttemptedValue);
        model.InitialState = true;
        model.SelectedState = true;

        return model;
    }
}

And here's the posted JSON data for this parameter:

"classificationItems":["19","20","21","22"]}

And here's how the ValueProvider sees it:

viewModel.classificationItems[0] AttemptedValue = "19" viewModel.classificationItems[1] AttemptedValue = "20" viewModel.classificationItems[2] AttemptedValue = "21" viewModel.classificationItems[3] AttemptedValue = "22"

This isn't currently working because firstly there's a prefix ("viewModel") which i can sort out, but secondly bindingContext.ModelName is "classificationItems" which is the name of the parameter being bound to and not the indexed item in the list, ie "classificationItems[0]"

I should add that when i declare this binder as a global ModelBinder in global.asax it works fine...

Upvotes: 0

Views: 889

Answers (1)

Daniel J.G.
Daniel J.G.

Reputation: 35042

Your custom model binder is being used for the whole List, not just for each particular item. As you are writing a new binder from scratch by implementing IModelBinder you would need to deal with adding all of the items to the List and the list prefexies, etc. This is not trivial code, check the DefaultModelBinder here.

Instead, you could extend the DefaultModelBinder class, letting it to work as usual and then setting those 2 properties as true:

public class SelectableLookupAllSelectedModelBinder: DefaultModelBinder
{

    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        //Let the default model binder do its work so the List<SelectableLookup> is recreated
        object model = base.BindModel(controllerContext, bindingContext);

        if (model == null)
            return null; 

        List<SelectableLookup> lookupModel = model as List<SelectableLookup>;
        if(lookupModel == null)
            return model;

        //The DefaultModelBinder has already done its job and model is of type List<SelectableLookup>
        //Set both InitialState and SelectedState as true
        foreach(var lookup in lookupModel)
        {
            lookup.InitialState = true;
            lookup.SelectedState = true;
        }

        return model;          
    }

The prefix could be dealt with by adding a bind attribute to the action parameter like [Bind(Prefix="viewModel")]

So in the end your action method parameter would look like:

[Bind(Prefix="viewModel")]
[ModelBinder(typeof(SelectableLookupAllSelectedModelBinder))] 
List<SelectableLookup> classificationItems

Upvotes: 2

Related Questions