Подтвердить что ты не робот

Десятичные значения с разделителем тысяч в Asp.Net MVC

У меня есть собственный модальный класс, который содержит десятичный член и представление, чтобы принять запись для этого класса. Все работало хорошо, пока я не добавил javascripts для форматирования номера внутри элемента управления вводом. Формат формата форматирует введенный номер с помощью разделителя тысяч "," при размытии фокуса.

Проблема заключается в том, что десятичное значение внутри моего модального класса не связывается и не разбирается с тысячным разделителем. ModelState.IsValid возвращает false, когда я тестировал его с "1,000.00", но он действителен для "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>

<% } %>

Я попробовал обходное решение с Custom ModelBinder, как предложил Александр. Задача решена. Но решение не подходит для реализации IDataErrorInfo. Значение зарплаты становится нулевым, если 0 введено из-за проверки. Любое предложение, пожалуйста? Члены команды Asp.Net MVC приходят в stackoverflow? Могу ли я немного помочь вам?

Обновленный код с настраиваемой моделью Binder, как предложил Александр

Модель 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
}
4b9b3361

Ответ 1

Кажется, что всегда найдутся способы обхода пути, чтобы найти подходящую связку по умолчанию. Интересно, можете ли вы создать свойство "псевдо", которое используется только для привязки модели? (Заметьте, это отнюдь не изящно. Я, кажется, прибегаю к подобным трюкам, как это все чаще и чаще, просто потому, что они работают, и они получают работу "сделано"...) Также обратите внимание, если вы использовали отдельный "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; }
    }
}

P.S., после того, как я набрал это, я сейчас оглядываюсь назад и даже не решаюсь опубликовать это, это так уродливо! Но если вы думаете, что это может быть полезно, я позволю вам решить...

Удачи!
-Mike

Ответ 2

Причина этого заключается в том, что в ConvertSimpleType в ValueProviderResult.cs используется TypeConverter.

TypeConverter для десятичной дроби не поддерживает тысячу разделителей. Прочтите здесь: http://social.msdn.microsoft.com/forums/en-US/clr/thread/1c444dac-5d08-487d-9369-666d1b21706e

Я еще не проверял, но на этом посту они даже сказали, что CultureInfo, переданная в TypeConverter, не используется. Он всегда будет инвариантным.

           string decValue = "1,400.23";

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

Итак, я думаю, вы должны использовать обходной путь. Не приятно...

Ответ 3

Мне не понравились вышеприведенные решения и придумали следующее:

В моем пользовательском modelbinder я в основном заменяю значение значением инварианта культуры, если оно является десятичным, а затем передает остальную часть работы стандартным связующим объектам. Необработанное значение, представляющее собой массив, кажется мне странным, но это то, что я видел/украл в исходном коде.

        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);
    }

Ответ 4

Вы пытались преобразовать его в Decimal в контроллер? Это должно сделать трюк:

строка _val = "1,000.00"; Decimal _decVal = Convert.ToDecimal(_val); ЕЫпе (_decVal.ToString());

Ответ 5

Эй, у меня была еще одна мысль... Это основано на ответе Навида, но все равно позволит вам использовать стандартную привязку модели. Концепция состоит в том, чтобы перехватить опубликованную форму, изменить некоторые из значений в ней, а затем передать коллекцию формы [измененной] в метод UpdateModel (метод привязки модели по умолчанию)... Я использую модифицированную версию этого для связанных с checkboxes/booleans, чтобы избежать ситуации, когда что-либо, кроме "истинного" или "ложного", вызывает необработанное/молчащее исключение в привязке модели.

(Разумеется, вы захотите реорганизовать это, чтобы быть более пригодным для повторного использования, возможно, иметь дело со всеми десятичными знаками)

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)) {
        // ...
    }
}

P.S., меня могут смутить мои методы NVC, но я думаю, что они будут работать.

Опять же, удачи!
-Mike

Ответ 6

Я реализую пользовательский валидатор, добавляя действительность группировки. Проблема (которую я решил в коде ниже) заключается в том, что метод 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;
    }
 }
}

Для десятичного числа? nullable вам нужно добавить небольшой код перед

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; вам нужно просто заменить тип в 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