Custom model binder to avoid decimal separator problems

I bumped into an issue while trying to get the open source ASP.NET MVC project NerdDinner running on my laptop. The Create action in the DinnersController just wouldn’t work. This was due to Latitude and Longitude values being set with a different decimal separator than my server expected. It got "0.0" instead of "0,0" which the Double.parse function didn’t understand. A client-side JavaScript wrote these values into hidden fields, so it was a bit difficult for me to understand at first. I wrote a custom model binder to solve this for all fields of type double at once.

As I’m still learning ASP.NET MVC, my solution may not be the optimal one.

Create a custom model binder that will only react differently to double values.

using System;
using System.Globalization;
using System.Web.Mvc;

namespace NerdDinner.Helpers {
  public class CustomModelBinder : DefaultModelBinder {

    public CustomModelBinder()
      : base() {
    }

    public override object BindModel(ControllerContext controllerContext, 
      ModelBindingContext bindingContext) {
      
      object result = null;

      // Don't do this here!
      // It might do bindingContext.ModelState.AddModelError
      // and there is no RemoveModelError!
      // 
      // result = base.BindModel(controllerContext, bindingContext);

      if (bindingContext.ModelType == typeof(double)) {

        string modelName = bindingContext.ModelName;
        string attemptedValue = bindingContext.ValueProvider[modelName].AttemptedValue;

        // Depending on cultureinfo the NumberDecimalSeparator can be "," or "."
        // Both "." and "," should be accepted, but aren't.
        string wantedSeperator = NumberFormatInfo.CurrentInfo.NumberDecimalSeparator;
        string alternateSeperator = (wantedSeperator == "," ? "." : ",");

        if (attemptedValue.IndexOf(wantedSeperator) == -1 
          && attemptedValue.IndexOf(alternateSeperator) != -1) {
          attemptedValue = attemptedValue.Replace(alternateSeperator, wantedSeperator);
        }

        try {
          result = double.Parse(attemptedValue, NumberStyles.Any);
        }
        catch (FormatException e) {
          bindingContext.ModelState.AddModelError(modelName, e);
        }

      }
      else {
        result = base.BindModel(controllerContext, bindingContext);
      }

      return result;
    }
  }
}

Change the DefaultBinder in Global.asax.

void Application_Start() {
  //[SNIP]
  ModelBinders.Binders.DefaultBinder = new CustomModelBinder();
}

Comments are closed.