Search code examples
asp.net-mvcglobalization

How to set decimal separators in ASP.NET MVC controllers?


I'm working with the NerdDinner application trying to teach myself ASP.NET MVC. However, I have stumbled upon a problem with globalization, where my server presents floating point numbers with a comma as the decimal separator, but Virtual Earth map requires them with dots, which causes some problems.

I have already solved the issue with the mapping JavaScript in my views, but if I now try to post an edited dinner entry with dots as decimal separators the controller fails (throwing InvalidOperationException) when updating the model (in the UpdateModel() metod). I feel like I must set the proper culture somewhere in the controller as well, I tried it in OnActionExecuting() but that didn't help.


Solution

  • I have just revisited the issue in a real project and finally found a working solution. Proper solution is to have a custom model binder for the type decimal (and decimal? if you're using them):

    using System.Globalization;
    using System.Web.Mvc;
    
    public class DecimalModelBinder : DefaultModelBinder
    {
        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);
    
            string modelName = bindingContext.ModelName;
            string attemptedValue = bindingContext.ValueProvider.GetValue(modelName)?.AttemptedValue;
    
            // in decimal? binding attemptedValue can be Null
            if (attemptedValue != null)
            {
                // 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, StringComparison.Ordinal) == -1
                    && attemptedValue.IndexOf(alternateSeperator, StringComparison.Ordinal) != -1)
                {
                    attemptedValue = attemptedValue.Replace(alternateSeperator, wantedSeperator);
                }
    
                try
                {
                    if (bindingContext.ModelMetadata.IsNullableValueType && string.IsNullOrWhiteSpace(attemptedValue))
                    {
                        return null;
                    }
    
                    result = decimal.Parse(attemptedValue, NumberStyles.Any);
                }
                catch (FormatException e)
                {
                    bindingContext.ModelState.AddModelError(modelName, e);
                }
            }
    
            return result;
        }
    }
    

    Then in Global.asax.cs in Application_Start():

    ModelBinders.Binders.Add(typeof(decimal), new DecimalModelBinder());
    ModelBinders.Binders.Add(typeof(decimal?), new DecimalModelBinder());
    

    Note that code is not mine, I actually found it at Kristof Neirynck's blog here. I just edited a few lines and am adding the binder for a specific data type, not replacing the default binder.