Интеграция между пользовательским DecimalModelBinder и IValidatableObject.Validate
Я работаю с серверной стороной проверки MVC, и мне нужно использовать несколько пользовательских атрибутов. Я хотел бы реализовать интерфейс ValidatableObject, потому что я думаю, что это более простой способ, чем написание нескольких пользовательских атрибутов.
Чтобы вызвать ValidationContext, я должен использовать пользовательский механизм связывания моделей, и я следую инструкциям Дэвида Хейни в его статье Trigger IValidatableObject.Validate, когда ModelState.IsValid имеет значение false
поэтому я положил в global.asax
ModelBinderProviders.BinderProviders.Clear();
ModelBinderProviders.BinderProviders.Add(new ForceValidationModelBinderProvider());
а потом в классе
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Linq;
using System.Web.Mvc;
/// <summary>
/// A custom model binder to force an IValidatableObject to execute the Validate method, even when the ModelState is not valid.
/// </summary>
public class ForceValidationModelBinder : DefaultModelBinder
{
protected override void OnModelUpdated(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
base.OnModelUpdated(controllerContext, bindingContext);
ForceModelValidation(bindingContext);
}
private static void ForceModelValidation(ModelBindingContext bindingContext)
{
// Only run this code for an IValidatableObject model
IValidatableObject model = bindingContext.Model as IValidatableObject;
if (model == null)
{
// Nothing to do
return;
}
// Get the model state
ModelStateDictionary modelState = bindingContext.ModelState;
// Get the errors
IEnumerable<ValidationResult> errors = model.Validate(new ValidationContext(model, null, null));
// Define the keys and values of the model state
List<string> modelStateKeys = modelState.Keys.ToList();
List<ModelState> modelStateValues = modelState.Values.ToList();
foreach (ValidationResult error in errors)
{
// Account for errors that are not specific to a member name
List<string> errorMemberNames = error.MemberNames.ToList();
if (errorMemberNames.Count == 0)
{
// Add empty string for errors that are not specific to a member name
errorMemberNames.Add(string.Empty);
}
foreach (string memberName in errorMemberNames)
{
// Only add errors that haven't already been added.
// (This can happen if the model's Validate(...) method is called more than once, which will happen when there are no property-level validation failures)
int index = modelStateKeys.IndexOf(memberName);
// Try and find an already existing error in the model state
if (index == -1 || !modelStateValues[index].Errors.Any(i => i.ErrorMessage == error.ErrorMessage))
{
// Add error
modelState.AddModelError(memberName, error.ErrorMessage);
}
}
}
}
}
/// <summary>
/// A custom model binder provider to provide a binder that forces an IValidatableObject to execute the Validate method, even when the ModelState is not valid.
/// </summary>
public class ForceValidationModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(Type modelType)
{
return new ForceValidationModelBinder();
}
}
Это прекрасно работает... Но тут возникает вопрос... Я также добавлю к этому связующему определенное поведение в случае двойного и двойного? введите для проверки числа в этом формате 1.000.000.000, поэтому я просматривал эти ресурсы Рейли и Хаака https://gist.github.com/johnnyreilly/5135647
using System;
using System.Globalization;
using System.Web.Mvc;
public class CustomDecimalModelBinder : 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
{
//Check if this is a nullable decimal and a null or empty string has been passed
var isNullableAndNull = (bindingContext.ModelMetadata.IsNullableValueType &&
string.IsNullOrEmpty(valueResult.AttemptedValue));
//If not nullable and null then we should try and parse the decimal
if (!isNullableAndNull)
{
actualValue = double.Parse(valueResult.AttemptedValue, NumberStyles.Any, CultureInfo.CurrentCulture);
}
}
catch (FormatException e)
{
modelState.Errors.Add(e);
}
bindingContext.ModelState.Add(bindingContext.ModelName, modelState);
return actualValue;
}
}
Затем, как было предложено в комментарии Хейни, я заменил стандартное DecimalModelBinder на CustomModelBinder в global.asax таким образом
ModelBinders.Binders.Remove(typeof(double));
ModelBinders.Binders.Remove(typeof(double?));
ModelBinders.Binders.Add(typeof(double?), new CustomDecimalModelBinder());
ModelBinders.Binders.Add(typeof(double), new CustomDecimalModelBinder());
Но я не могу понять, почему.. CustomDecimalModelBinder не запускается... Так что на данный момент мой workarounf должен комментировать 4 строки выше в global.asax и добавить в пользовательский класс ModelBinder переопределение BindModel в способ принять двойной и двойной? в ит-культуре
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
//if (bindingContext.ModelName == "commercialQty")
if (bindingContext.ModelType == typeof(double?) || bindingContext.ModelType == typeof(double))
{
ValueProviderResult valueResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
ModelState modelState = new ModelState { Value = valueResult };
object actualValue = null;
try
{
//Check if this is a nullable decimal and a null or empty string has been passed
var isNullableAndNull = (bindingContext.ModelMetadata.IsNullableValueType &&
string.IsNullOrEmpty(valueResult.AttemptedValue));
//If not nullable and null then we should try and parse the decimal
if (!isNullableAndNull)
{
actualValue = double.Parse(valueResult.AttemptedValue, NumberStyles.Any, CultureInfo.CurrentCulture);
}
}
catch (FormatException e)
{
modelState.Errors.Add(e);
}
bindingContext.ModelState.Add(bindingContext.ModelName, modelState);
return actualValue;
}
else
{
return base.BindModel(controllerContext, bindingContext);
}
}
Таким образом, ValidationContext с моим customValidation работает, и мне также удается проверить двойные типы пользовательским способом.
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
var results = new List<ValidationResult>();
var fieldPreliminaryCostNum = new[] { "preliminaryCostNum" };
var fieldPreliminaryCostAmount = new[] { "preliminaryCostAmount" };
var fieldPreliminaryVoucherNum = new[] { "preliminaryVoucherNum" };
var fieldCodiceIva = new[] { "codiceIva" };
var fieldContoRicavi = new[] { "contoRicavi" };
var fieldContoAnticipi = new[] { "contoAnticipi" };
//per la obbligatorietà di preliminary cost num, preliminary voucher num e preliminary cost amount è sufficiente
//il flag additional oppure occorre anche verificare che il voucher type code sia final?
if (flagAdditional == BLCostanti.fAdditional && preliminaryCostNum == null)
{
results.Add(new ValidationResult(BLCostanti.labelCosto + "preliminaryCostNum ", fieldPreliminaryCostNum));
}
if (flagAdditional == BLCostanti.fAdditional && preliminaryCostAmount == null)
{
results.Add(new ValidationResult(BLCostanti.labelCosto + "preliminaryCostAmount ", fieldPreliminaryCostAmount));
}
if (flagAdditional == BLCostanti.fAdditional && preliminaryVoucherNum == null)
{
results.Add(new ValidationResult(BLCostanti.labelCosto + "preliminaryVoucherNum ", fieldPreliminaryVoucherNum));
//inoltre il preliminary deve essere approvato!
if (! BLUpdateQueries.CheckPreliminaryVoucherApproved(preliminaryVoucherNum) )
{
results.Add(new ValidationResult(BLCostanti.labelCosto + "preliminaryVoucherNum non approvato", fieldPreliminaryVoucherNum));
}
}
if (costPayReceiveInd == BLCostanti.attivo && String.IsNullOrWhiteSpace(codiceIva))
{
//yield return new ValidationResult("codiceIva obbligatorio", fieldCodiceIva);
results.Add(new ValidationResult(BLCostanti.labelEditableFields + "codiceIva ", fieldCodiceIva));
}
if ((sapFlowType == BLCostanti.girocontoAcquisto || sapFlowType == BLCostanti.girocontiVendita)
&& String.IsNullOrWhiteSpace(contoRicavi))
{
results.Add(new ValidationResult(BLCostanti.labelEditableFields + "conto Ricavi ", fieldContoRicavi));
}
if ((sapFlowType == BLCostanti.girocontoAcquisto || sapFlowType == BLCostanti.girocontiVendita)
&& String.IsNullOrWhiteSpace(contoAnticipi))
{
results.Add(new ValidationResult(BLCostanti.labelEditableFields + "conto Anticipi ", fieldContoAnticipi));
}
return results;
}
Любая лучшая идея приветствуется!