Valeurs décimales avec séparateur en milliers Asp.Net MVC
j'ai une classe modale personnalisée qui contient un membre décimal et une vue pour accepter l'entrée pour cette classe. Tout a bien fonctionné jusqu'à ce que j'ajoute javascripts pour formater le numéro à l'intérieur du contrôle d'entrée. Le format de code de format le nombre saisi avec séparateur de milliers", " lorsque la mise au point floue.
le problème est que la valeur décimale à l'intérieur de ma classe modale n'est pas bind/parsed bien avec le séparateur de milliers. ModelState.IsValid retourne false quand je l'ai testé avec " 1,000.00" mais c'est valable pour "100.00" sans aucun changement.
Pourriez-vous partager avec moi si vous avez une solution pour cela?
Merci d'avance.
Classe Échantillon
public class Employee
{
public string Name { get; set; }
public decimal Salary { get; set; }
}
Exemple De Contrôleur
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);
}
}
Vue D'Échantillon
<% using (Html.BeginForm())
{ %>
Name: <%= Html.TextBox("Name")%><br />
Salary: <%= Html.TextBox("Salary")%><br />
<button type="submit">Save</button>
<% } %>
j'ai essayé une solution de contournement avec Custom ModelBinder comme Alexander l'a suggéré. Le problème résolu. Mais la solution ne va pas bien avec Mise en œuvre d'IDataErrorInfo. La valeur du salaire devient nulle lorsque 0 est entré à cause de la validation. Toute suggestion, s'il vous plaît? Do Asp.Net les membres de L'équipe de MVC viennent à stackoverflow? Puis-je avoir un peu d'aide de vous?
code mis à jour avec le modèle personnalisé Binder comme Alexander suggéré
Modèle 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;
}
}
Catégorie D'Employés
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 réponses
il semble qu'il y ait toujours des solutions de rechange d'une forme ou d'une autre à trouver afin de rendre le binder de modèle par défaut heureux! Je me demande si vous pourriez créer une propriété "pseudo" qui est utilisée seulement par le classeur modèle? (Remarque, ce n'est pas élégant. Moi-même, je semble recourir à des trucs semblables de plus en plus souvent simplement parce qu'ils fonctionnent et ils obtiennent le travail "fait"...) Notez aussi, si vous utilisiez un "ViewModel" séparé (que je recommande pour ceci), vous pourriez mettre ce code dans là, et laissez votre modèle de domaine propre et propre.
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., après avoir tapé ceci, je regarde en arrière maintenant et j'hésite même à poster ceci, c'est tellement laid! Mais si vous pensez qu'il pourrait vous être utile, je vous laisse décider...
bonne chance!
-Mike
la raison derrière cela est, que dans ConvertSimpleType dans Valeurproviderresult.cs un TypeConverter est utilisé.
le convertisseur de type pour la décimale ne supporte pas un séparateur de mille. Lisez ici à ce sujet: http://social.msdn.microsoft.com/forums/en-US/clr/thread/1c444dac-5d08-487d-9369-666d1b21706e
Je n'ai pas encore vérifié, mais à ce post ils ont même dit que le CultureInfo passé dans TypeConverter n'est pas utilisé. Il sera toujours Invariant.
string decValue = "1,400.23";
TypeConverter converter = TypeDescriptor.GetConverter(typeof(decimal));
object convertedValue = converter.ConvertFrom(null /* context */, CultureInfo.InvariantCulture, decValue);
Donc je suppose que vous devez utiliser une solution de contournement. Pas sympa...
je n'aimais pas les solutions ci-dessus et est venu avec ceci:
dans mon modelbinder personnalisé, je remplace fondamentalement la valeur par la valeur invariante de la culture si elle est une décimale et puis passe le reste du travail au binder modèle par défaut. La valeur brute étant un tableau me semble étrange, mais c'est ce que j'ai vu / volé dans le code original.
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);
}
Avez-vous essayé de le convertir en Décimal dans le contrôleur? Ceci devrait faire l'affaire:
chaîne _val = "1 000 habitants.00"; Décimal _decVal = Convert.Todécimal (_val); Console.WriteLine(_decVal.ToString ());
Hey j'ai eu une dernière pensée... Cela s'appuie sur la réponse de Naweed, mais vous permettra tout de même d'utiliser le classeur de modèle par défaut. Le concept est d'intercepter la forme postée, de modifier certaines des valeurs qu'elle contient, puis de passer la collection de formes [modifiées] à la méthode UpdateModel (modèle par défaut liant)... J'utilise une version modifiée de ce gestion des cases à cocher / booléens, pour éviter la situation où quoi que ce soit d'autre que "vrai" ou "faux" provoque une exception non accompagnée/silencieuse dans le modèle de classeur.
(vous voudriez bien sûr reformuler ceci pour être plus réutilisable, pour peut-être traiter avec décimales)
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., Je suis peut-être confus sur mes méthodes de NVC, mais je pense que ça va marcher.
Encore une fois, bonne chance!
-Mike
j'implémente le validateur personnalisé, en ajoutant la validité du groupement. Le problème (que j'ai résolu en code ci-dessous)est que la méthode parse supprimer tous les milliers séparateur, donc aussi 1,2,2 est considéré valide.
Voici mon classeur pour décimal
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;
}
}
}
pour décimale? nullable vous devez ajouter un peu de code avant
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;
}
}
}
vous devez créer un classeur similaire pour double, double?, float, float? (le code est le même pour DecimalModelBinder et DecimalNullableModelBinder; vous avez juste besoin de remplacer le type dans 2 point où il y a"décimal").
Puis mondiale.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());
cette solution fonctionne très bien du côté du serveur, comme la partie client en utilisant jQuery globalize et ma correction signalée ici https://github.com/globalizejs/globalize/issues/73#issuecomment-275792643