Десятичные значения с разделителем тысяч в Asp.Net MVC - PullRequest
13 голосов
/ 16 июня 2009

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

Проблема в том, что десятичное значение внутри моего модального класса плохо связывается / анализируется с разделителем тысяч. 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

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
}

Ответы [ 6 ]

15 голосов
/ 16 июня 2009

Причина этого в том, что в 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);

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

7 голосов
/ 22 августа 2009

Мне не понравились решения выше, и я придумал это:

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

        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 голосов
/ 18 июня 2009

Кажется, что всегда есть обходные пути той или иной формы, чтобы порадовать связующего по умолчанию модель по умолчанию! Интересно, не могли бы вы создать псевдо-свойство, которое используется только компоновщиком модели? (Обратите внимание, это ни в коем случае не элегантно. Сам я, похоже, все чаще и чаще прибегаю к подобным трюкам просто потому, что они работают, и они выполняют свою работу "..." «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

0 голосов
/ 29 января 2017

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

0 голосов
/ 18 июня 2009

Эй, у меня была еще одна мысль ... Это основано на ответе 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)) {
        // ...
    }
}

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

0 голосов
/ 16 июня 2009

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

string _val = "1,000.00"; Десятичный _decVal = Convert.ToDecimal (_val); Console.WriteLine (_decVal.ToString ());

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...