0

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?

Jim Shaffer
  • 101
  • 1
  • 2
  • 14
  • Something like this: http://kevww.wordpress.com/2013/02/28/conditional-required-validation-or-field-mandatory-depends-on-another-field-mvc-4/ ? – b2zw2a Nov 12 '14 at 22:53
  • possible duplicate of [RequiredIf Conditional Validation Attribute](http://stackoverflow.com/questions/7390902/requiredif-conditional-validation-attribute) – artm Nov 12 '14 at 22:53
  • I don't think it's overkill. If you implement a custom attribute, you can write server side and client side code so that it functions like all other code attributes, otherwise, you would have to write a custom solution in code with no client side implementation. – Jim Shaffer Nov 12 '14 at 23:01
  • Thanks for the link on the possible duplicate. This looks like exactly what I am looking for. – Jim Shaffer Nov 12 '14 at 23:02
  • 3
    When you have a hammer everything looks like a property you want to attribute. – crthompson Nov 12 '14 at 23:04
  • I am not sure what you mean. Adding custom attributes allows you to implement additional custom requirements in the exact same fashion that the rest of the MVC code works. It provides a seamless implementation on both the server and client side, and once the attribute is created, you never have to write a single line of additional validation logic. Maybe I am missing something. You apparently have a better idea? – Jim Shaffer Nov 12 '14 at 23:13

1 Answers1

1

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">
Jim Shaffer
  • 101
  • 1
  • 2
  • 14