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

我是研究僧i 提交于 2019-11-27 03:35:45
Pawel Krakowiak

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.

Nick Berardi

Set this in your web.config

  <system.web>
    <globalization uiCulture="en" culture="en-US" />

You appear to be using a server that is setup with a language that uses comma's instead of decimal places. You can adjust the culture to one that uses the comma's in a way that your application is designed, such as en-US.

Can you parse the text using the invariant culture - sorry, I don't have the NerdDinner code in fornt of me, but if you are passing in dot-separated decimals than the parsing should be OK if you tell it to use the invariant culture. E.g.

 float i = float.Parse("0.1", CultureInfo.InvariantCulture);

Edit. I suspect that this is a bug in the NerdDinner code by the way, along the same lines as your previous problem.

I have a different take on this, you might like it. What I don't like about the accepted answer is it doesn't check for other characters. I know there will be a case where the currency symbol will be in the box because my user doesn't know better. So yeah I can check in javascript to remove it, but what if for some reason javascript isn't on? Then extra characters might get through. Or if someone tries to spam you passing unknown characters through... who knows! So I decided to use a regex. It's a bit slower, tiny fraction slower - for my case it was 1,000,000 iterations of the regex took just under 3 seconds, while around 1 second to do a string replace on a coma and period. But seeing as I don't know what characters might come through, then I am happy for this slightest of performance hits.

public class DecimalModelBinder : DefaultModelBinder
{
    public override object BindModel(ControllerContext controllerContext,
                                     ModelBindingContext bindingContext)
    {
        string modelName = bindingContext.ModelName;
        string attemptedValue =
            bindingContext.ValueProvider.GetValue(modelName).AttemptedValue;

        if (bindingContext.ModelMetadata.IsNullableValueType
                && string.IsNullOrWhiteSpace(attemptedValue))
        {
            return null;
        }

        if (string.IsNullOrWhiteSpace(attemptedValue))
        {
            return decimal.Zero;
        }

        decimal value = decimal.Zero;
        Regex digitsOnly = new Regex(@"[^\d]", RegexOptions.Compiled);
        var numbersOnly = digitsOnly.Replace(attemptedValue, "");
        if (!string.IsNullOrWhiteSpace(numbersOnly))
        {
            var numbers = Convert.ToDecimal(numbersOnly);
            value = (numbers / 100m);

            return value;
        }
        else
        {
            if (bindingContext.ModelMetadata.IsNullableValueType)
            {
                return null;
            }

        }

        return value;
    }
}

Basically, remove all characters that are not digits, for a string that isn't empty. Convert to decimal. Divide by 100. Return result.

Works for me.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!