0

I'm working with MVC custom validation server side and as i have to use several custom attribute. I'd like to implement the interface ValidatableObject because I think it is easier way then writing several custom attributes.

To force the ValidationContext I've to use a custom model binder and I've following the instructions by David Haney in his article Trigger IValidatableObject.Validate When ModelState.IsValid is false

so I've put in global.asax

 ModelBinderProviders.BinderProviders.Clear();
 ModelBinderProviders.BinderProviders.Add(new ForceValidationModelBinderProvider());

and then in a class

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

It works great... But here come the question.. I've also to add to this binder a specific behaviour in case of double and double? type to validate number in this format 1.000.000,000 so I was looking at these resources by Reilly and Haack 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;
    }
}

Then as suggested in the comment by Haney I've substituted the default DecimalModelBinder with the CustomModelBinder in the global.asax this way

  ModelBinders.Binders.Remove(typeof(double));
  ModelBinders.Binders.Remove(typeof(double?));

  ModelBinders.Binders.Add(typeof(double?), new CustomDecimalModelBinder());
  ModelBinders.Binders.Add(typeof(double), new CustomDecimalModelBinder());

But I can't understand why.. the CustomDecimalModelBinder doesn't fire... So at the moment my workarounf has been to comment the 4 row above in the global.asax And to add in the custom ModelBinder class the override of BindModel in a way to accept the double and double? in it-It culture

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


        }


    }

In this way the ValidationContext with my customValidation works and I also manage to validate double types in a custom way

  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;

       }

Any better idea is welcome!

effeffe
  • 99
  • 6
  • 15
  • 3
    Have you debugged `ModelBinders.Binders` and seen what the collection contains? My guess is the default binders are in play. You may need to remove the entries at the `double` and `double?` keys and then add your custom binders. – Haney Sep 03 '14 at 16:58
  • Thank you! Debugging as you guessed there is a DecimalModelBinder as default Binder for double type.. So now I've to remove this default binder and add the new one.. I try! – effeffe Sep 03 '14 at 17:11
  • Excuse me the right place to do this substitution could be the OnModelUpdated function? – effeffe Sep 03 '14 at 17:18
  • I think you'd probably only want to do this once, so in the startup via `Global.asax.cs` – Haney Sep 03 '14 at 17:19
  • Excuse me if I disturb again.. Now after the substitution I can see in ModelBinders.Binders my CustomDecimalModelBinder. The problem is that BindModel in CustomDecimalModelBinder is never hit (if I put a breakpoint). While if a comment ModelBinderProviders.BinderProviders.Clear(); ModelBinderProviders.BinderProviders.Add(new ForceValidationModelBinderProvider()); it fires. Can you help me to understand what is wrong? – effeffe Sep 04 '14 at 06:52
  • @Haney I've updated the question with a 'workaround' because I didn't manage to fire CustomDecimalModelBinder – effeffe Sep 04 '14 at 09:10

0 Answers0