Asp.Net MVC中带千位分隔符的十进制值

13

我有一个自定义的模型类,其中包含一个十进制数成员和一个视图来接受此类的输入。一切都运作良好,直到我添加了JavaScript来格式化输入控件内的数字。当焦点失去时,格式化代码将输入的数字以千位分隔符“,”格式化。

问题是我的模态类中的十进制值无法与千位分隔符正确绑定/解析。当我测试它使用“1,000.00”时,ModelState.IsValid返回false,但对于“100.00”而言没有任何更改,则有效。

如果您有任何解决方法,能否与我分享?

提前致谢。

示例类

public class Employee
{
    public string Name { get; set; }
    public decimal Salary { get; set; }
}

示例控制器

public class EmployeeController : Controller
{
    [AcceptVerbs(HttpVerbs.Get)]
    public ActionResult New()
    {
        return View();
    }

    [AcceptVerbs(HttpVerbs.Post)]
    public ActionResult New(Employee e)
    {
        if (ModelState.IsValid) // <-- It is retruning false for values with ','
        {
            //Subsequence codes if entry is valid.
            //
        }
        return View(e);
    }
}

示例视图

<% using (Html.BeginForm())
   { %>

    Name:   <%= Html.TextBox("Name")%><br />
    Salary: <%= Html.TextBox("Salary")%><br />

    <button type="submit">Save</button>

<% } %>

我尝试了Alexander建议的Custom ModelBinder解决方法,问题得到解决。但是该解决方案与IDataErrorInfo实现不兼容。由于验证,当输入0时,工资值变为null。有什么建议吗? Asp.Net MVC团队成员会来stackoverflow吗?我能从你们那里得到一点帮助吗?

更新后的代码,使用Alexander建议的Custom Model Binder

模型绑定器(Model Binder)

public class MyModelBinder : DefaultModelBinder {

    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) {
        if (bindingContext == null) {
            throw new ArgumentNullException("bindingContext");
        }

        ValueProviderResult valueResult;
        bindingContext.ValueProvider.TryGetValue(bindingContext.ModelName, out valueResult);
        if (valueResult != null) {
            if (bindingContext.ModelType == typeof(decimal)) {
                decimal decimalAttempt;

                decimalAttempt = Convert.ToDecimal(valueResult.AttemptedValue);

                return decimalAttempt;
            }
        }
        return null;
    }
}

员工类

    public class Employee : IDataErrorInfo {

    public string Name { get; set; }
    public decimal Salary { get; set; }

    #region IDataErrorInfo Members

    public string this[string columnName] {
        get {
            switch (columnName)
            {
                case "Salary": if (Salary <= 0) return "Invalid salary amount."; break;
            }
            return string.Empty;
        }
    }

    public string Error{
        get {
            return string.Empty;
        }
    }

    #endregion
}

你的意思是在提交表单之前删除“,”吗?我认为这对于这种情况会起作用。但问题是我简化了案例以便更好地理解。实际上,我在类中有许多小数成员,并且我将不得不为此程序创建许多新的类似类。感谢您的时间。 - user123517
2
如果可能的话,我更愿意在服务器端修复它。 - user123517
1
我认为你应该为此编写自定义模型绑定器或在客户端剥离分隔符。 - Alexander Prokofyev
嗨亚历山大,我写了一个自定义模型绑定器。问题解决了。谢谢。但是自定义模型绑定器与IDataErrorInfo的实现不太兼容。有什么想法吗? - user123517
1
请查看Haacked的这篇文章:http://haacked.com/archive/2011/03/19/fixing-binding-to-decimals.aspx - VinnyG
显示剩余2条评论
6个回答

15
它背后的原因是,在ValueProviderResult.cs文件的ConvertSimpleType方法中使用了TypeConverter。
decimal类型的TypeConverter不支持千位分隔符。 这里阅读相关信息:http://social.msdn.microsoft.com/forums/en-US/clr/thread/1c444dac-5d08-487d-9369-666d1b21706e 我还没有检查,但在那篇帖子中,他们甚至说传递给TypeConverter的CultureInfo没有被使用。它将始终为Invariant。
           string decValue = "1,400.23";

        TypeConverter converter = TypeDescriptor.GetConverter(typeof(decimal));
        object convertedValue = converter.ConvertFrom(null /* context */, CultureInfo.InvariantCulture, decValue);

所以我猜你必须使用一些变通方法。不太好看......


7

我不喜欢上面的解决方案,所以想出了这个:

在我的自定义模型绑定器中,如果值是一个十进制数,我基本上会用与文化无关的值替换它,然后将其余的工作交给默认的模型绑定器。 原始值是一个数组,这对我来说很奇怪,但这是我在原始代码中看到/借鉴的。

        public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        if(bindingContext.ModelType == typeof(decimal) || bindingContext.ModelType==typeof(Nullable<decimal>))
        {
            ValueProviderResult valueProviderResult = bindingContext.ValueProvider[bindingContext.ModelName];
            if (valueProviderResult != null)
            {
                decimal result;
                var array = valueProviderResult.RawValue as Array;
                object value;
                if (array != null && array.Length > 0)
                {
                    value = array.GetValue(0);
                    if (decimal.TryParse(value.ToString(), out result))
                    {
                        string val = result.ToString(CultureInfo.InvariantCulture.NumberFormat);
                        array.SetValue(val, 0);
                    }
                }
            }
        }
        return base.BindModel(controllerContext, bindingContext);
    }

当然,我可以进行一些重构以包括其他数字类型。 - user81129
你需要将以下代码添加到Global.asax的启动事件中:ModelBinders.Binders.DefaultBinder = new CustomModelBinder(); - mahdi gh

4

看起来总是可以找到某种形式的解决方法,以使默认模型绑定器满意!我想知道是否可以创建一个仅由模型绑定器使用的“伪”属性?(注意,这绝不是优雅的解决方案。我自己似乎越来越经常地采用类似的技巧,因为它们有效并且可以完成工作...)还要注意,如果您正在使用单独的“ViewModel”(我建议这样做),您可以将此代码放在其中,并使您的域模型保持整洁。

public class Employee
{
    private decimal _Salary;
    public string MvcSalary // yes, a string. Bind your form values to this!
    {
        get { return _Salary.ToString(); }
        set
        { 
            // (Using some pseudo-code here in this pseudo-property!)
            if (AppearsToBeValidDecimal(value)) {
                _Salary = StripCommas(value);
            }
        }
    }
    public decimal Salary
    {
        get { return _Salary; }
        set { _Salary = value; }
    }
}

顺便说一下,我打完这段话后回头看了一眼,现在甚至有些犹豫是否要发布它,因为它实在太丑陋了!但如果你认为它可能有用,那就由你决定吧...

祝你好运!
-Mike


Mike,我喜欢你的解决方法。虽然它给我们的类添加了更多的属性,但它仍然解决了问题。我想我们别无选择,只能接受这些。谢谢!顺便说一下,我很想看到MVC团队成员对此发表评论。我是不是太雄心勃勃了? - user123517
我为需要验证的小数添加了“伪”属性。而对于其他不需要验证的小数属性,我仍然保留了自定义模型绑定器。 - user123517
我认为,这个解决方法结合自定义模态绑定器足够简单和好,可以解决问题。非常感谢,Mike。 - user123517
它能够工作并且是最快的解决方案,但这很疯狂。我尝试过更改我的文化设置,但默认的模型绑定器无法将“1,000”转换为十进制数,而Convert.ToDecimal可以... - ptutt

1
我实现了自定义验证器,添加了分组的有效性。问题(在下面的代码中解决)是parse方法会删除所有的千位分隔符,所以1,2,2也被认为是有效的。
这是我的十进制绑定器。
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Web.Mvc;

namespace EA.BUTruck.ContactCenter.Model.Extensions
{
 public class DecimalModelBinder : IModelBinder
 {
    public object BindModel(ControllerContext controllerContext,
        ModelBindingContext bindingContext)
    {
        ValueProviderResult valueResult = bindingContext.ValueProvider
            .GetValue(bindingContext.ModelName);
        ModelState modelState = new ModelState { Value = valueResult };
        object actualValue = null;
        try
        {
            var trimmedvalue = valueResult.AttemptedValue.Trim();
            actualValue = Decimal.Parse(trimmedvalue, CultureInfo.CurrentCulture);

            string decimalSep = CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator;
            string thousandSep = CultureInfo.CurrentCulture.NumberFormat.NumberGroupSeparator;

            thousandSep = Regex.Replace(thousandSep, @"\u00A0", " "); //used for culture with non breaking space thousand separator

            if (trimmedvalue.IndexOf(thousandSep) >= 0)
            {
                //check validity of grouping thousand separator

                //remove the "decimal" part if exists
                string integerpart = trimmedvalue.Split(new string[] { decimalSep }, StringSplitOptions.None)[0];

                //recovert double value (need to replace non breaking space with space present in some cultures)
                string reconvertedvalue = Regex.Replace(((decimal)actualValue).ToString("N").Split(new string[] { decimalSep }, StringSplitOptions.None)[0], @"\u00A0", " ");
                //if are the same, it is a valid number
                if (integerpart == reconvertedvalue)
                    return actualValue;

                //if not, could be differences only in the part before first thousand separator (for example original input stirng could be +1.000,00 (example of italian culture) that is valid but different from reconverted value that is 1.000,00; so we need to make a more accurate checking to verify if input string is valid


                //check if number of thousands separators are the same
                int nThousands = integerpart.Count(x => x == thousandSep[0]);
                int nThousandsconverted = reconvertedvalue.Count(x => x == thousandSep[0]);

                if (nThousands == nThousandsconverted)
                {
                    //check if all group are of groupsize number characters (exclude the first, because could be more than 3 (because for example "+", or "0" before all the other numbers) but we checked number of separators == reconverted number separators
                    int[] groupsize = CultureInfo.CurrentCulture.NumberFormat.NumberGroupSizes;
                    bool valid = ValidateNumberGroups(integerpart, thousandSep, groupsize);
                    if (!valid)
                        throw new FormatException();

                }
                else
                    throw new FormatException();

            }


        }
        catch (FormatException e)
        {
            modelState.Errors.Add(e);
        }

        bindingContext.ModelState.Add(bindingContext.ModelName, modelState);
        return actualValue;
    }
    private bool ValidateNumberGroups(string value, string thousandSep, int[] groupsize)
    {
        string[] parts = value.Split(new string[] { thousandSep }, StringSplitOptions.None);
        for (int i = parts.Length - 1; i > 0; i--)
        {
            string part = parts[i];
            int length = part.Length;
            if (groupsize.Contains(length) == false)
            {
                return false;
            }
        }

        return true;
    }
 }
}

对于可空的十进制数,您需要在之前添加一些代码

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Web.Mvc;

namespace EA.BUTruck.ContactCenter.Model.Extensions
{
 public class DecimalNullableModelBinder : IModelBinder
 {
    public object BindModel(ControllerContext controllerContext,
        ModelBindingContext bindingContext)
    {
        ValueProviderResult valueResult = bindingContext.ValueProvider
            .GetValue(bindingContext.ModelName);
        ModelState modelState = new ModelState { Value = valueResult };
        object actualValue = null;
        try
        {
             //need this condition against non nullable decimal
             if (string.IsNullOrWhiteSpace(valueResult.AttemptedValue))
                return actualValue;
            var trimmedvalue = valueResult.AttemptedValue.Trim();
            actualValue = Decimal.Parse(trimmedvalue,CultureInfo.CurrentCulture);

            string decimalSep = CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator;
            string thousandSep = CultureInfo.CurrentCulture.NumberFormat.NumberGroupSeparator;

            thousandSep = Regex.Replace(thousandSep, @"\u00A0", " "); //used for culture with non breaking space thousand separator

            if (trimmedvalue.IndexOf(thousandSep) >=0)
            {
                //check validity of grouping thousand separator

                //remove the "decimal" part if exists
                string integerpart = trimmedvalue.Split(new string[] { decimalSep }, StringSplitOptions.None)[0];

                //recovert double value (need to replace non breaking space with space present in some cultures)
                string reconvertedvalue = Regex.Replace(((decimal)actualValue).ToString("N").Split(new string[] { decimalSep }, StringSplitOptions.None)[0], @"\u00A0", " ");
                //if are the same, it is a valid number
                if (integerpart == reconvertedvalue)
                    return actualValue;

                //if not, could be differences only in the part before first thousand separator (for example original input stirng could be +1.000,00 (example of italian culture) that is valid but different from reconverted value that is 1.000,00; so we need to make a more accurate checking to verify if input string is valid


                //check if number of thousands separators are the same
                int nThousands = integerpart.Count(x => x == thousandSep[0]);
                int nThousandsconverted = reconvertedvalue.Count(x => x == thousandSep[0]);

                if(nThousands == nThousandsconverted)
                {
                    //check if all group are of groupsize number characters (exclude the first, because could be more than 3 (because for example "+", or "0" before all the other numbers) but we checked number of separators == reconverted number separators
                    int[] groupsize = CultureInfo.CurrentCulture.NumberFormat.NumberGroupSizes;
                    bool valid = ValidateNumberGroups(integerpart, thousandSep, groupsize);
                    if (!valid)
                        throw new FormatException();

                }
                else
                    throw new FormatException();

            }


        }
        catch (FormatException e)
        {
            modelState.Errors.Add(e);
        }

        bindingContext.ModelState.Add(bindingContext.ModelName, modelState);
        return actualValue;
    }

    private bool ValidateNumberGroups(string value, string thousandSep, int[] groupsize)
    {
        string[] parts = value.Split(new string[] { thousandSep }, StringSplitOptions.None);
        for(int i = parts.Length-1; i > 0; i--)
        {
            string part = parts[i];
            int length = part.Length;
            if (groupsize.Contains(length) == false)
            {
                return false;
            }
        }

        return true;
    }
 }

}

您需要为double、double?、float和float?创建类似的绑定器(代码与DecimalModelBinder和DecimalNullableModelBinder相同;只需要在其中"type"出现的2个位置上替换类型即可)。

接下来,在global.asax中进行设置。

ModelBinders.Binders.Add(typeof(decimal), new DecimalModelBinder());
ModelBinders.Binders.Add(typeof(decimal?), new DecimalNullableModelBinder());
ModelBinders.Binders.Add(typeof(float), new FloatModelBinder());
ModelBinders.Binders.Add(typeof(float?), new FloatNullableModelBinder());
ModelBinders.Binders.Add(typeof(double), new DoubleModelBinder());
ModelBinders.Binders.Add(typeof(double?), new DoubleNullableModelBinder());

这个解决方案在服务器端运行良好,客户端部分使用jquery globalize以及我在此处报告的修复https://github.com/globalizejs/globalize/issues/73#issuecomment-275792643

0

嘿,我还有一个想法......这是建立在Naweed的回答之上,但仍然可以让您使用默认模型绑定器。这个概念是拦截发布的表单,修改其中一些值,然后将[修改后]的表单集合传递给UpdateModel(默认模型绑定器)方法......我使用了一个修改版,用于处理复选框/布尔值,以避免除“true”或“false”之外的任何情况在模型绑定器中引起未处理/静默异常。

(您当然希望将其重构为更可重用的形式,以处理所有小数)

public ActionResult myAction(NameValueCollection nvc)
{
    Employee employee = new Employee();
    string salary = nvc.Get("Salary");
    if (AppearsToBeValidDecimal(salary)) {
        nvc.Remove("Salary");
        nvc.Add("Salary", StripCommas(salary));
    }
    if (TryUpdateModel(employee, nvc)) {
        // ...
    }
}

顺便说一句,我可能对我的NVC方法感到困惑,但我认为这些方法会起作用。


顺便说一句,如果你发现无法像我上面发布的那样修改nvc(例如,我想到你实际上无法修改Request.Form),那么可以查看我第一段中提供的链接,了解如何重新构建一个新的字典。嗯。 - Funka

0
你尝试在控制器中将它转换为十进制了吗?这应该可以解决问题:
string _val = "1,000.00"; Decimal _decVal = Convert.ToDecimal(_val); Console.WriteLine(_decVal.ToString());

嗨,我正在使用默认模型绑定器来完成工作。请看一下我刚刚包含的代码并给我建议。感谢您的时间。 - user123517

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接