views:

448

answers:

2

I would like to do complex validation on my form that contains a list of objects.

My form contains a list of, let's say, MyObjects. MyObject consists of a double amount and a MyDate which is just a wrapper around DateTime.

public class MyObject
{
    public MyDate Date { get; set; } //MyDate is wrapper around DateTime
    public double Price { get; set; }
}

The form...

<input type="text" name="myList[0].Date" value="05/11/2009" />
<input type="text" name="myList[0].Price" value="100,000,000" />

<input type="text" name="myList[1].Date" value="05/11/2009" />
<input type="text" name="myList[1].Price" value="2.23" />

Here is my Action

public ActionResult Index(IList<MyObject> myList)
{
   //stuff
}

I want to allow the user to enter in 100,000,000 for a Price and for the custom model binder to strip the ',' so it can convert to a double. Likewise, I need to convert the 05/11/2009 to a MyDate object. I thought about creating a MyObjectModelBinder but dont know what to do from there.

ModelBinders.Binders[typeof(MyObject)] = new MyObjectModelBinder();

Any help appreciated.

A: 

You're definitely going down the right path. When I did this, I made an intermediate view model that took Price as a string, because of the commas. I then converted from the view model (or presentation model) to a controller model. The controller model had a very simple constructor that accepted a view model and could Convert.ToDecimal("12,345,678.90") the price value.

Jarrett Meyer
+1  A: 

Here's a sample implementation of a custom model binder:

public class MyObjectModelBinder : DefaultModelBinder
{
    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        // call the base method and let it bind whatever properties it can
        var myObject = (MyObject)base.BindModel(controllerContext, bindingContext);
        var prefix = bindingContext.ModelName;
        if (bindingContext.ValueProvider.ContainsKey(prefix + ".Price"))
        {
            string priceStr = bindingContext.ValueProvider[prefix + ".Price"].AttemptedValue;
            // priceStr = 100,000,000 or whatever the user entered
            // TODO: Perform transformations on priceStr so that parsing works
            // Note: Be carefull with cultures
            double price;
            if (double.TryParse(priceStr, out price))
            {
                myObject.Price = price;
            }
        }

        if (bindingContext.ValueProvider.ContainsKey(prefix + ".Date"))
        {
            string dateStr = bindingContext.ValueProvider[prefix + ".Date"].AttemptedValue;
            myObject.Date = new MyDate();
            // TODO: Perform transformations on dateStr and set the values 
            // of myObject.Date properties
        }

        return myObject;
    }
}
Darin Dimitrov
Thankyou, I was doing something similar myself. If validation fails do I simply use AddModelError on the bindingContext? I also have 5 other string properties on MyObject. Is there a better way of looping through the properties rather than doing if contains key prefix + etc... ?
David Liddle
Yes, if there's a validation failure you use `AddModelError` and `SetModelValue` on the bindingContext. As far as the other properties are concerned you can do the following: `var myObject = (MyObject)base.BindModel(controllerContext, bindingContext);` and let the default model binder do the instantiation and bind whatever it can instead of just newing `MyObject` by hand.
Darin Dimitrov
I've updated my post to reflect this.
Darin Dimitrov
It's much better to create MyDateModelBinder instead of binding each property separately in a big large binder. DefaultModelBinder will automatically use MyDateModelBinder for properties of type MyDate.
queen3
@queen3, that depends, `MyDate` is a custom defined type, imagine it doesn't have public setters and/or default public constructor. The default model binder has no clue how to instantiate this class.
Darin Dimitrov
When I add base.BindModel then it will add 'A value is required' as it tries to bind to the double value incorrectly. Also, i've noticed if an error occurs the value for Price or MyDate is not kept.
David Liddle
@David, try declaring the `Price` property in `MyObject` as `Nullable<double>` instead of `double`.
Darin Dimitrov