Jim Shaffer Jim Shaffer - 3 months ago 29
C# Question

C# Custom Attribute Required If

I am just getting into custom attributes, and I absolutely love them. I am wondering if it is possible to create an attribute that gets applied to a property and denotes the name of another property in the same object. If would check to see if the referenced property has a value, and if so, the decorated attribute would be required.
Something like this:

[RequiredIfNotNull("ApprovedDate")]
[DisplayName("Approved By")]
[StringLength(50, ErrorMessage = "{0} must not exceed {1} characters")]
public string ApprovedBy { get; set; }

[DisplayName("Approved Date")]
[DisplayFormat(DataFormatString = "{0:d}")]
[PropertyMetadata(ColumnName = "ApprovedDate")]
public DateTime? ApprovedDate { get; set; }


So the approved by property is decorated with the RequiredIfNotNull attribute which references the property to check for null. In this case, the Approved Date.
I would want the ApprovedBy Property to be required if the ApprovedDate had a value.
Is it possible to do something like this?
If so can you implement it server side and client side?

Answer

Here is what I came up with: Server side:

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Reflection;
using System.Web.Mvc;   
namespace Atlas.Core.Attributes
{
    /// <summary>
    /// Add the following decoration: [ConditionalRequired("Model", "Field")]
    /// Model = client model being used to bind object
    /// Field = the field that if not null makes this field required.
    /// </summary>
    public class ConditionalRequiredAttribute : ValidationAttribute, IClientValidatable
    {
        private const string DefaultErrorMessageFormatString = "The {0} field is required.";
        private readonly string _dependentPropertyPrefix;
        private readonly string _dependentPropertyName;

        public ConditionalRequiredAttribute(string dependentPropertyPrefix, string dependentPropertyName)
        {
            _dependentPropertyPrefix = dependentPropertyPrefix;
            _dependentPropertyName = dependentPropertyName;
            ErrorMessage = DefaultErrorMessageFormatString;
        }

        protected override ValidationResult IsValid(object item, ValidationContext validationContext)
        {
            PropertyInfo property = validationContext.ObjectInstance.GetType().GetProperty(_dependentPropertyName);
            object dependentPropertyValue = property.GetValue(validationContext.ObjectInstance, null);

            if (dependentPropertyValue != null && item == null)
                return new ValidationResult(string.Format(ErrorMessageString, validationContext.DisplayName));

            return ValidationResult.Success;
        }

        public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
        {
            var rule = new ModelClientValidationRule
            {
                ErrorMessage = string.Format("{0} is required", metadata.GetDisplayName()),
                ValidationType = "conditionalrequired",
            };

            rule.ValidationParameters.Add("requiredpropertyprefix", _dependentPropertyPrefix);
            rule.ValidationParameters.Add("requiredproperty", _dependentPropertyName);
            yield return rule;
        }
    }
}

Client side:

$.validator.unobtrusive.adapters.add('conditionalrequired', ['requiredpropertyprefix', 'requiredproperty'], function (options) {
        options.rules['conditionalrequired'] = {
            requiredpropertyprefix: options.params['requiredpropertyprefix'],
            requiredproperty: options.params['requiredproperty']
        };
        options.messages['conditionalrequired'] = options.message;
});

$.validator.addMethod('conditionalrequired', function (value, element, params) {
        var requiredpropertyprefix = params['requiredpropertyprefix'];
        var requiredproperty = params['requiredproperty'];
        var field = $('#' + requiredproperty).length == 0 ? '#' + requiredpropertyprefix + '_' + requiredproperty : '#' + requiredproperty;
        return !($(field).val().length > 0 && value.length == 0);
    }
);

I set this up to accept a model or prefix value and then the name of the actual field. The reason for this is that in many cases, I will add an object as part of a model, and that will cause the form id for that element to be rendered as ModelName_FieldName. But it also occurred to me that you may or may not use a model with an embedded object. In that case the id would just be FieldName, so the clientside code checks to see if the element exists by FieldName, and if not it returns ModelName_FieldName otherwise it just returns the FieldName. I didn't do it yet, but I should probably check to make sure the resulting fieldname has is not null.

and then to decorate your property you would do something like this:

[DataMember]
[DisplayName("Approved By")]
[ConditionalRequired("HOA", "ApprovedDate")]
[StringLength(50, ErrorMessage = "{0} must not exceed {1} characters")]
public string ApprovedBy { get; set; }

my model looks like this:

    public class HOAModel
    {
        public HOA HOA { get; set; }
   }

my view implementation looks like this:

Html.Kendo().DatePickerFor(m => m.HOA.ApprovedDate)

So my client side element has the following ID:

<input name="HOA.ApprovedDate" class="k-input" id="HOA_ApprovedDate" role="textbox">
Comments