MVC Custom model binder to parse custom text formats

In this post I will discuss a very interesting issue I ran into our MVC application that uses globalization to render numbers, dates etc. based on user's settings and selection. Application has a page where users enter some numbers in a grid and then submit for processing. All of a sudden the application started to run into problem. When the form was posted back all the numbers in model were ZERO. When I started to investigate the issue I found that there were few things that we did not test in the past.

  • The numbers on client side were being formatted according to user's culture settings. For example a value of 2000 was being formatted to 2,000 after user had entered value in text box.
  • Text boxes were not tested for very large numbers.

Based on this new information, I put a break point in POST method to investigate how the model was being presented to the method when form was posted back. As you can see from the screen shot below that value are ZERO.

Next I investigated raw form values passed from client to server as part of HTTP POST process. Following screen shot shows how these raw values looked like.

As you can see from the highlighted text that text box value of 2,000 has been passed to the server. But when MVC framework executed model binding for these value, the converter failed to convert these values. The problem is the default converter. The property for this amount value is of type int. If you will try to parse 2,000 to integer using Convert and/or Parse methods, you will end up an exception complaining that string is not in correct format. This is exactly what is going in default model binder. When it tried to convert that amount value, it failed and set the value to default value 0.

Custom Model Binding

Since default model binder is not working for my implementation. I had to look into developing a custom model binder. There are two few options that I have for MVC application.

  • Implement a model binder that affects all instance of type int in the application
  • Implement a model binder for that object model only

I opted for later option because I did affect rest of the application because of an issue with one input screen. But if your application wants to same behavior across the board then you can definitely implement custom model binder for data type instead of the object type.

Following code snippet shows implementation of my custom model binder. I am only interested in one property of my model object. Therefore I am only overriding GetPropertyValue.

public class BidItemsBinder:DefaultModelBinder
{
    protected override object GetPropertyValue(ControllerContext controllerContext, 
        ModelBindingContext bindingContext, 
        PropertyDescriptor propertyDescriptor, 
        IModelBinder propertyBinder)
    {
        if (String.Compare
          (propertyDescriptor.Name, "BidCategories", StringComparison.OrdinalIgnoreCase) != 0)
        {
            return base.GetPropertyValue(controllerContext, bindingContext, 
                     propertyDescriptor, propertyBinder);
        }
        else
        {
            var formatter = new CultureInfo("en-US").NumberFormat;
            var bidCategories = new List<Models.BidCategory>();
            var form = controllerContext.HttpContext.Request.Form;
            var i = 0;
            while (!string.IsNullOrEmpty(form["BidCategories[" + i + "].CategoryId"]))
            {
                var category = new Models.BidCategory()
                    {
                        CategoryId = Convert.ToInt32(form["BidCategories[" + i + "].CategoryId"]),
                        BidItems = new List<Bid>()
                    };
                bidCategories.Add(category);
                var j = 0;
                while (!string.IsNullOrEmpty(fom["BidCategories[" + i + "].BidItems[" + j + "].ItemId"]))
                {
                    var amount = form["BidCategories[" + i + "].BidItems[" + j + "].BidAmount"];
                    const NumberStyles numberStyle = NumberStyles.Integer | NumberStyles.AllowThousands |
                                                     NumberStyles.AllowCurrencySymbol;
                    var bidItem = new Models.Bid()
                        {
                            ItemId = Convert.ToInt32(form["BidCategories[" + i + "].BidItems[" + j + "].ItemId"]),
                            BidAmount = Int32.Parse(amount, numberStyle, formatter)
                        };
                    category.BidItems.Add(bidItem);
                    j++;
                }
                i++;
            }
            return bidCategories;
        }
    }
}
    

The most important part of the implementation is one line of code BidAmount = Int32.Parse(amount, numberStyle, formatter). This line of code controls how parsing of the input text value is supposed to take place. I will extract highlights of this implementation below.

var formatter = new CultureInfo("en-US").NumberFormat;
const NumberStyles numberStyle = NumberStyles.Integer | NumberStyles.AllowThousands |
                                 NumberStyles.AllowCurrencySymbol;
BidAmount = Int32.Parse(amount, numberStyle, formatter)
    

First line of code is getting number format that is going to be used for parsing. For simplicity sake I have hard coded the culture to "en-US". In real application you will be getting this format bases on user's current culture settings. Second line of code sets up the flags that define what kind of number style is going to be parsed and what to expect in the input text string. As you can see from my example that I have indicated that number is going to be integer, expect thousand separator and expect currency symbol to be present in string as well.

Following code shows how this model binder is specified on method signature to let MVC framework know that our custom model binder is going to be used and not default implementation.

[HttpPost]
public ActionResult SubmitBid([ModelBinder(typeof(BidItemsBinder))]AuctionBids model)
{
  return View(model);
}
    

Once the custom model binder is in place, the POST process worked as expected. Following screen shot shows how the values looked like after model binding. As you can see, it has correct value in BidAmount property of the object.

View Demo

I hope this post helps in understanding some of the issues that you can run into when globalizing information rendered on the pages and how to overcome those issues. In subsequent posts, I will discuss more globalization related topics for MVC framework.

comments powered by Disqus

Blog Tags