Search code examples
.netasp.net-mvc-3validationdata-annotationsmodel-validation

.Net Data annotation and model hierarchy


everyone! I'm confusing with implementing a piece of code to make work .net data annotation in asp.net mvc 3 with model with different required fields in several cases (6). I have a model:

  public class OpportunityModel
{
    public Guid OpportunityId { get; set; }

    [Display(Name = "Value")]
    [RegularExpression(@"^[-+]?\d{1,10}(\.\d{0,4})?$", ErrorMessage = "Must be a number")]
    public decimal? ActualValue { get; set; }  

    [Display(Name = "Name")]
    [Required(ErrorMessage = "Name is required")]
    public string Name { get; set; } 
    public string Product { get; set; } 

    [Display(Name = "Estimated Date")]
    public DateTime? EstimateDate { get; set; }


    public bool? Sales6ixFallDown { get; set; }


    [Display(Name = "Stage")]
    public Stages Sales6ixStage { get; set; }

    public DateTime? Sales6ixDateInBoard { get; set; }

    public DateTime? Sales6ixDateInCurrentStage { get; set; }

    public DateTime? Sales6ixNextAppointmentDate { get; set; }

    [Display(Name = "Description")]
    public string Description { get; set; }

    public string Sales6ixNextAppointmentDescription { get; set; }

    public int NewColumn { get; set; }

    public Guid? CustomerId { get; set; }

    public string CustomerName { get; set; }
}

What I need is the possibility dynamically change required fiefs in it. After some googling that's impossible and came to idea to use model inheritance. I mean: I have a base model like this:

  public class BaseOpportunityModel
{
    public Guid OpportunityId { get; set; }

    public virtual decimal? ActualValue { get; set; }  
    public virtual string Name { get; set; }  

    public string Product { get; set; } 

    public DateTime? EstimateDate { get; set; }

    public bool? Sales6ixFallDown { get; set; }


    [Display(Name = "Stage")]
    public Stages Sales6ixStage { get; set; }

    public DateTime? Sales6ixDateInBoard { get; set; }

    public DateTime? Sales6ixDateInCurrentStage { get; set; }

    public DateTime? Sales6ixNextAppointmentDate { get; set; }

    [Display(Name = "Description")]
    public string Description { get; set; }

    public string Sales6ixNextAppointmentDescription { get; set; }

    public int NewColumn { get; set; }

    public Guid? CustomerId { get; set; }

    public string CustomerName { get; set; }
}

where virtual properties are properties that may be or not a required fields. And then I have several derived model from base like this one:

  public class OpportunityModel0: BaseOpportunityModel
{
    [Display(Name = "Value")]
    [Required(ErrorMessage = "Name is required")]
    [RegularExpression(@"^[-+]?\d{1,10}(\.\d{0,4})?$", ErrorMessage = "Must be a number")]
    public override decimal? ActualValue { get; set; }  

    [Display(Name = "Name")]
    [Required(ErrorMessage = "Name is required")]
 public override string Name { get; set; }  

}

And then I be able to use in View and Controller base model BaseOpportunityModel. But I encountered follow problem:

  • Validation use annotation attributes from BaseOpportunityModel and ignore attributes in derived models.

What do I wrong? Can somebody steer me in the right direction or help me with this issue? Thanks in advance.


Solution

  • I figured out my problem with different reqiured validation of model with using custom RequiredIfValidator. So now I have just one model and one view. Here is code, may be some one find it useful:

    RequiredIfAttribute:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.ComponentModel.DataAnnotations;
    using System.Web.Mvc;
    
    namespace Infrastructure.Extensions
    {
        public class RequiredIfAttribute : ValidationAttribute, IClientValidatable
        {
            private RequiredAttribute _innerAttribute = new RequiredAttribute();
    
            public string DependentProperty { get; set; }
            public object TargetValue { get; set; }
    
            public RequiredIfAttribute(string dependentProperty, object targetValue)
            {
                this.DependentProperty = dependentProperty;
                this.TargetValue = targetValue;
            }
    
            protected override ValidationResult IsValid(object value, ValidationContext validationContext)
            {
                // get a reference to the property this validation depends upon
                var containerType = validationContext.ObjectInstance.GetType();
                var field = containerType.GetProperty(this.DependentProperty);
    
                if (field != null)
                {
                    // get the value of the dependent property
                    var dependentvalue = field.GetValue(validationContext.ObjectInstance, null);
    
                    // compare the value against the target value
                    if ((dependentvalue == null && this.TargetValue == null) ||
                        (dependentvalue != null && dependentvalue.Equals(this.TargetValue)))
                    {
                        // match => means we should try validating this field
                        if (!_innerAttribute.IsValid(value))
                            // validation failed - return an error
                            return new ValidationResult(this.ErrorMessage, new[] { validationContext.MemberName });
                    }
                }
    
                return ValidationResult.Success;
            }
    
            public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
            {
                var rule = new ModelClientValidationRule()
                {
                    ErrorMessage = FormatErrorMessage(metadata.GetDisplayName()),
                    ValidationType = "requiredif",
                };
    
                string depProp = BuildDependentPropertyId(metadata, context as ViewContext);
    
                // find the value on the control we depend on;
                // if it's a bool, format it javascript style 
                // (the default is True or False!)
                string targetValue = (this.TargetValue ?? "").ToString();
                if (this.TargetValue.GetType() == typeof(bool))
                    targetValue = targetValue.ToLower();
    
                rule.ValidationParameters.Add("dependentproperty", depProp);
                rule.ValidationParameters.Add("targetvalue", targetValue);
    
                yield return rule;
            }
    
            private string BuildDependentPropertyId(ModelMetadata metadata, ViewContext viewContext)
            {
                // build the ID of the property
                string depProp = viewContext.ViewData.TemplateInfo.GetFullHtmlFieldId(this.DependentProperty);
                // unfortunately this will have the name of the current field appended to the beginning,
                // because the TemplateInfo's context has had this fieldname appended to it. Instead, we
                // want to get the context as though it was one level higher (i.e. outside the current property,
                // which is the containing object (our Person), and hence the same level as the dependent property.
                var thisField = metadata.PropertyName + "_";
                if (depProp.StartsWith(thisField))
                    // strip it off again
                    depProp = depProp.Substring(thisField.Length);
                return depProp;
            }
        }
    }
    



    RequiredIfValidator

    namespace Infrastructure.Extensions
    {
        public class RequiredIfValidator : DataAnnotationsModelValidator<RequiredIfAttribute>
        {
            public RequiredIfValidator(ModelMetadata metadata, ControllerContext context, RequiredIfAttribute attribute)
                : base(metadata, context, attribute)
            {
            }
    
            public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
            {
                return base.GetClientValidationRules();
            }
    
            public override IEnumerable<ModelValidationResult> Validate(object container)
            {
                // get a reference to the property this validation depends upon
                var field = Metadata.ContainerType.GetProperty(Attribute.DependentProperty);
    
                if (field != null)
                {
                    // get the value of the dependent property
                    var value = field.GetValue(container, null);
    
                    // compare the value against the target value
                    if ((value == null && Attribute.TargetValue == null) ||
                        (value.Equals(Attribute.TargetValue)))
                    {
                        // match => means we should try validating this field
                        if (!Attribute.IsValid(Metadata.Model))
                            // validation failed - return an error
                            yield return new ModelValidationResult { Message = ErrorMessage };
                    }
                }
            }
        }
    }
    



    Client Validation

    /// <reference path="jquery-1.4.4-vsdoc.js" />
        /// <reference path="jquery.validate.unobtrusive.js" />
    
        $.validator.addMethod('requiredif',
            function (value, element, parameters) {
                var id = '#' + parameters['dependentproperty'];
    
                // get the target value (as a string, 
                // as that's what actual value will be)
                var targetvalue = parameters['targetvalue'];
                targetvalue =
                  (targetvalue == null ? '' : targetvalue).toString();
    
                // get the actual value of the target control
                // note - this probably needs to cater for more 
                // control types, e.g. radios
                var control = $(id);
                var controltype = control.attr('type');
                var actualvalue =
                    controltype === 'checkbox' ?
                    control.is(":checked").toString() :
                //control.attr('checked').toString() :
                    control.val();
    
                actualvalue = actualvalue.toLocaleLowerCase();
    
                // if the condition is true, reuse the existing 
                // required field validator functionality
                if (targetvalue === actualvalue)
                    return $.validator.methods.required.call(
                      this, value, element, parameters);
    
                return true;
            }
        );
    
        $.validator.unobtrusive.adapters.add(
            'requiredif',
            ['dependentproperty', 'targetvalue'], 
            function (options) {
                options.rules['requiredif'] = {
                    dependentproperty: options.params['dependentproperty'],
                    targetvalue: options.params['targetvalue']
                };
                options.messages['requiredif'] = options.message;
            });