If we want to create an attribute for validation, we usually create a class and inherit from the ValidationAttribute. And then we override the IsValid method according to the need we have for that attribute. For example, in the following we have created an attribute that compares two properties. And if the property on which the LowerThan attribute is located is not less than another property to be compared to, we add an error to the ModelState.

public class LowerThanAttribute : ValidationAttribute
{
    public LowerThanAttribute(string dependentPropertyName)
    {
        DependentPropertyName = dependentPropertyName;
    }

    public string DependentPropertyName { get; set; }
    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        int? currentPropertyValue = value as int?;
        currentPropertyValue ??= 0;
        var typeInfo = validationContext.ObjectInstance.GetType();
        var dependentPropertyValue = Convert.ToInt32(typeInfo.GetProperty(DependentPropertyName)
                                        .GetValue(validationContext.ObjectInstance, null));

        var displayDependentProperyName = typeInfo.GetProperty(DependentPropertyName)
                                        .GetCustomAttributes(typeof(DisplayAttribute), false)
                                        .Cast<DisplayAttribute>()
                                        .FirstOrDefault()?.Name;

        if (!(currentPropertyValue.Value < dependentPropertyValue))
        {
            return new ValidationResult("Value of {0} must be lower than " + displayDependentProperyName);
        }
        return ValidationResult.Success;
    }
}

First we take the value of the property we want to compare with using reflection and store it in the dependentPropertyValue variable. Next, we read the Name value using a reflection from the DisplayAttribute, and then perform the comparison operation. We add that otherwise the validation operation will be successful.

But a problem. This operation is checked only on the server side and when we call ModelState.IsValid in the action, the validation operation is performed. This means that all data is sent to the server, and if there is an error in the ModelState, the user must resend the data.

But you can use the IClientModelValidator interface to validate this attribute on the client side. To do this, we must first inherit from the IClientModelValidator interface and implement its AddValidation method.

public class LowerThanAttribute : ValidationAttribute, IClientModelValidator
{
    public LowerThanAttribute(string dependentPropertyName)
    {
        DependentPropertyName = dependentPropertyName;
    }

    public string DependentPropertyName { get; set; }

    public void AddValidation(ClientModelValidationContext context)
    {
        var displayCurrentProperyName = context.ModelMetadata.ContainerMetadata
                                            .ModelType.GetProperty(context.ModelMetadata.PropertyName)
                                            .GetCustomAttributes(typeof(DisplayAttribute), false)
                                            .Cast<DisplayAttribute>()
                                            .FirstOrDefault()?.Name;

        var displayDependentProperyName = context.ModelMetadata.ContainerMetadata
                                            .ModelType.GetProperty(DependentPropertyName)
                                            .GetCustomAttributes(typeof(DisplayAttribute), false)
                                            .Cast<DisplayAttribute>()
                                            .FirstOrDefault()?.Name;


        MergeAttribute(context.Attributes, "data-val", "true");
        MergeAttribute(context.Attributes, "data-val-lowerthan", $"{displayCurrentProperyName} must be lower than {displayDependentProperyName}");
        MergeAttribute(context.Attributes, "data-val-dependentpropertyname", "#" + DependentPropertyName);
    }
    private  bool MergeAttribute(IDictionary<string, string> attributes, string key, string value)
    {
        if (attributes.ContainsKey(key))
        {
            return false;
        }
        attributes.Add(key, value);
        return true;
    }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        int? currentPropertyValue = value as int?;
        currentPropertyValue ??= 0;
        var typeInfo = validationContext.ObjectInstance.GetType();
        var dependentPropertyValue = Convert.ToInt32(typeInfo.GetProperty(DependentPropertyName)
                                        .GetValue(validationContext.ObjectInstance, null));

        var displayCurrentProperyName = typeInfo.GetProperty(DependentPropertyName)
                                        .GetCustomAttributes(typeof(DisplayAttribute), false)
                                        .Cast<DisplayAttribute>()
                                        .FirstOrDefault()?.Name;

        if (!(currentPropertyValue.Value < dependentPropertyValue))
        {
            return new ValidationResult("Value of {0} must be lower than " + displayCurrentProperyName);
        }
        return ValidationResult.Success;
    }
}

The IClientModelValidator interface has a method called AddValidation that allows us to perform validation on the client side. Next, we need to use JQuery to implement validation for this attribute on the client side. In the AddValidation method, we specify only the function name and parameters required on the client side. For example, in the example above, we have introduced a function called lowerthan, which we will later have to implement on the client side, and we have introduced the name of the property to be compared with the data-val-dependentpropertyname. In the following code, we have implemented client-side validation. The value is the value of the property on which the LowerThan attribute is located, and otherPropId is the name of the property to be compared to what we read from the element.

jQuery.validator.addMethod("lowerthan", function (value, element, param) {
    var otherPropId = $(element).data('val-dependentpropertyname');
    if (otherPropId) {
        var otherProp = $(otherPropId);
        if (otherProp) {
            var otherVal = otherProp.val();
            if (parseInt(otherVal) > parseInt(value)) {
                return true;
            }
            return false;
        }
    }
    return true;
});
jQuery.validator.unobtrusive.adapters.addBool("lowerthan");

We have saved the above JavaScript code in a file called LowerThan.js that we need to add to our page.

To use this interface, you must add client-side validation files and the LowerThan.js file to your page.

<script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>
<script src="~/js/LowerThan.js"></script>

Then to use, we need to put the LowerThan attribute on the desired property as below.

public class User
{
    [Required]
    public string Username { get; set; }
    [Required]
    public int Age { get; set; }
    [LowerThan(nameof(Age))]
    [Required]
    public int Experience { get; set; }
}

Finally, if the value of the Experience property on which the LowerThan attribute is located is less than the value of the Age property to be compared, but if it is greater than or equal to, the error text we added in the AddValidation method will be displayed.

Powered by Froala Editor

Comments