DefaultModelBinder Problem with nested levels + other binders
Asked Answered
K

1

7

I have what I would think is a somewhat normal situation where I need to bind form posts to an "order" model. This model has a few levels of information to it:

Order.Billing.FirstName
Order.Billing.Address.City
Order.Billing.Address.Country

Using the DefaultModelBinder, if I POST a form to an action that takes this Order model as the param, the following fields JustWork(TM):

<%=Html.TextBox("Billing.FirstName")%>
<%=Html.TextBox("Billing.Address.City")%>

This field does not:

<%=Html.TextBox("Billing.Address.Country")%>

The wrinkle I have is with the country property. In our case, Address.Country returns a Country class instance (ISO2/3/Name/Code logic). It is not a string. Not surprise that it doesn't work by default.

My first thought was to create a CountryModelBinder (inherit DefaultModelBinder) and ModelBinders.Binders.Add it to the type of Country. When I do that, CountryModelBinder never gets called in the scenerio above.

My second thought was to create an AddressModelBinder (inherit DefaultModelBinder) and bind it to our Address type. While that does get called, the SetProperty call for "Country" has an empty value, even though the form has posted a field called "Billing.Address.Country".

After some tinkering, it appears that the model binding behavior only calls CreateModel when the model is the top level class the action wants, and all other binders have their BindPropery/SetProperty called for child properties.

In other words, if I create model binders for Order, OrderAddress(Billing), Address, and Country. For the action that takes an order, only OrderModelBinder.CreateModel is called. ORderAddress and Address.BindProperty/SetProperty are called for some things, and sometimes SetProperty value argument is empty when it was clearly posted in a name that matches the other field property mappings.

It's easy enough to just add code to OrderModelBinder to pull Billing.Address.Country out of Request.Form. But I have multiple models that use Address and having all of them do that seems broken.

What am I missing here? Is there a way to have the CountryModelBinder actually get called in this case? I would think that the CountryModelBinder should get called when Billing.Address.Country is mapped to the Country property of the Address binder.

Karrikarrie answered 16/2, 2010 at 14:23 Comment(2)
this might help you: #2463006Sahara
I am having the same problem with a nested model structure, it seems to me that properties at the top level, and one level down are getting bound, but anything lower than that just seems to get ignored. Is this the normal behaviour of the model binder? Seems rather arbitrary behaviour.Gastronomy
E
0

I've tried doing what you've done here, appearntly on MVC3 it does indeed work if I provide a model binder for that type.

This is just a proof of concept to show that it DOES WORK, and shouldn't be seen as even close to production level code:

Models:

public class SimpleModel
    {
        public string Value { get; set; }
        public int Other { get; set; }
    }

    public class ComplexModel
    {
        public SimpleModel Complexity {get;set;}
        public string StrVal { get; set; }
    }

some binder:

public class MBinder : IModelBinder
        {
            public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
            {
                if ( bindingContext.ModelType == typeof(SimpleModel))
                {
                    var simpleModel= new SimpleModel();
                    simpleModel.Other = 1;
                    simpleModel.Value = controllerContext.HttpContext.Request.Form["Complexity"];

                    return cm;
                }
                return null;
            }
        }

in global asax:

ModelBinders.Binders.Add(typeof (SimpleModel), new MBinder());

code in View:

    @model ComplexModel

    @using ( Html.BeginForm() )
{ 
    <fieldset>
        @Html.LabelFor(x => x.Complexity)
        @Html.TextBoxFor(x => x.Complexity)
    </fieldset>

    <fieldset>
        @Html.LabelFor(x => x.StrVal)
        <br />
        @Html.EditorFor(x => x.StrVal)
    </fieldset>
    <input type="submit" />
}

Controller:

public ActionResult Index()
        {
            return View();
        }

        [HttpPost]
        public ActionResult Index(ComplexModel model)
        {
            return RedirectToAction("Index");

        }

BTW in MVC 3 a better option would be to use the IModelBinderProvider interface, but I just wanted to show something that would work.

Epley answered 28/2, 2011 at 18:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.