Search code examples
c#asp.net-mvclocalizationnumber-formattingglobalization

How to handle decimal input values in a globalized MVC app


Currently trying to rework a .NET 4.8 MVC application to support globalization (needs to support both English and French). So far, everything has been working fine.

The issue, however, is with decimal input. This is a financial application, so it is important that I deal with input boxes that are set to type="number". Strictly speaking, I know that the value 15000,25 is not considered a number, and will not convert implicitly to a decimal (the backing field data type that I use in my view model). With this in mind, I have a custom model binder:

public class DecimalModelBinder : DefaultModelBinder
{
    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);

        if (valueProviderResult == null)
        {
            return base.BindModel(controllerContext, bindingContext);
        }

        if (string.IsNullOrEmpty(valueProviderResult.AttemptedValue))
        {
            return null;
        }

        var result = decimal.TryParse(valueProviderResult.AttemptedValue, NumberStyles.Currency, CultureInfo.InvariantCulture, out decimal covertedOutput);

        if (result)
        {
            return covertedOutput;
        }

        return null;
    }
}

This model binder is then registered in the Global.asax file. The above appears to work to an extent...especially in Firefox, in that I can enter 15000,25 into a form input and what happens on the backend is that 15000.25 is recorded in the database.

This is a different story in chrome, as

  1. I cannot enter 15000,25 as a value, only decimal point based numbers and
  2. on the reload from the server - chrome will try and take 15000.25 and parse it as 15000,25 in the input...which fails, with the error:

The specified value "15000,25" cannot be parsed, or is out of range

My question is this - should I force users to always deal with true decimal based numbers? So stop them from entering 15000,25 but allow 15000.25? or is there a way around this / best practice that I'm just not seeing?

Here is the decimal field that I'm working with. Both the form control and the view model property:

@Html.TextBoxFor(model => model.SellAmount, "{0:f2}", new {min = "0", type = "number", step = "1000" })
public decimal? SellAmount { get; set; }

Solution

  • So from doing a bunch of research, I managed to come up with a solution.

    1. Use type=text instead of number
    2. Use a custom input by extending MvcHtmlString

    An issue I was having was that the browser would try and insert it's own localized value into the text box during view re-renders. This would cause chrome to raise the following error:

    The specified value "{value}" cannot be parsed, or is out of range

    To prevent this from happening, I extended the MVC input control:

      public static class InputExtensions
      {
         public static MvcHtmlString NumberInput<TModel, TProperty>(this HtmlHelper<TModel> helper
                , Expression<Func<TModel, TProperty>> expression, string format = null)
         {
            // Get decimal value from the model
            var value = (decimal)ModelMetadata.FromLambdaExpression(expression, helper.ViewData).Model;
        
            // Render the input, this stops the browser from inserting it's own 'localized' format as we ignore the culture with ultureInfo.InvariantCulture
            helper.TextBoxFor(expression, format, new { pattern = @"[0-9]+([\.][0-9]{1,2})?", @Value = value.ToString(CultureInfo.InvariantCulture) });
         }
      }
    

    You can just call the extension from the view:

     @Html.NumberInput(model => model.Input.SellAmount, "{0:f2}")
    

    Doing the above means that I can force the user into using a 000.00 format for decimals, elsewhere, I just let the UICulture take over when displaying the values (in labels etc.)

    This cures a lot of the localization headaches I was having.