William
William

Reputation: 3395

Conditional Validation with Attributes in ASP.NET MVC

I am trying to create the ability to have conditionally validate models in ASP.NET. I got it to work for server-side validation, but I cannot figure out how to get it to work with client-side validation.

For instance,

public class Student
{
    [Required]
    public string Name { get; set; }
    public bool RequiresAddress { get; set; }
    [RequiredIf("RequiresAddress", true)]
    public string Address { get; set; }
}

Here is my RequiredIf attribute (creating following this post):

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
public sealed class RequiredIfAttribute : ValidationAttribute
{
    public string DependentName;
    public object DependentValue;

    public RequiredIfAttribute(string PropertyName, object PropertyValue, string ErrorMessage)
    {
        this.DependentName = PropertyName;
        this.DependentValue = PropertyValue;
        this.ErrorMessage = ErrorMessage;
    }

    public override bool IsValid(object value)
    {
        if (value == null) { return false; }

        string ValueString = value as string;
        if (ValueString != null) { return (ValueString.Trim().Length != 0); }
        return true;
    }
}

And then I have this validator (which I registered in Global.asax.cs using the DataAnnotationsModelValidatorProvider):

public class RequiredIfValidator : DataAnnotationsModelValidator<RequiredIfAttribute>
{
    public RequiredIfValidator(ModelMetadata metadata, ControllerContext context, RequiredIfAttribute attribute)
        : base(metadata, context, attribute)
    {
    }

    public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
    {
        var Rule = new ModelClientValidationRule
        {
            ErrorMessage = ErrorMessage,
            ValidationType = "requiredif",
        };

        var viewContext = (ControllerContext as ViewContext);
        string depProp = viewContext.ViewData.TemplateInfo.GetFullHtmlFieldId(Attribute.DependentName);
        Rule.ValidationParameters.Add("dependentname", depProp);
        Rule.ValidationParameters.Add("dependentvalue", Attribute.DependentValue);

        yield return Rule;
    }

    public override IEnumerable<ModelValidationResult> Validate(object container)
    {
        var Field = Metadata.ContainerType.GetProperty(Attribute.DependentName);
        if (Field != null)
        {
            var Value = Field.GetValue(container, null);
            if ((Value == null && Attribute.DependentValue == null) || (Value != null && Value.Equals(Attribute.DependentValue)))
            {
                if (!Attribute.IsValid(Metadata.Model))
                {
                    yield return new ModelValidationResult { Message = ErrorMessage };
                }
            }
        }
    }
}

Finally, in my view, I attempt to register the client-side validator with jQuery:

if (typeof (jQuery) !== "undefined" && typeof (jQuery.validator) !== "undefined") {
    (function ($) {
        $.validator.addMethod('requiredif', function (value, element, parameters) {
            var id = '#' + parameters['dependentname'];
            var dependentvalue = parameters['dependentvalue'];
            dependentvalue = (dependentvalue == null ? '' : dependentvalue).toString();
            var actualvalue = $(id).val();
            if (dependentvalue === actualvalue) {
                return $.validator.methods.required.call(this, value, element, parameters);
            }
            else {
                return true;
            }
        });

    })(jQuery);
}

Like I said, the server-side validation works flawlessly, but Client-Side does not seem to be working at all. I am a bit of a n00b with JavaScript so I am not sure how to figure this out - what am I missing here?

Update

I got rid of the Validator class and I implemented IClientValidatable on the custom Attribute. Here is my new RequiredIfAttribute. Again, everything on the Server-Side seems to be working as expected, but the Client-Side is not working.

As you can see from my view, I am using DevExpress to build the form.

p.s. The zipped solution can be found here.

public class RequiredIfAttribute : ValidationAttribute, IClientValidatable
{
    public string DependentName { get; set; }
    public object DependentValue { get; set; }

    public RequiredIfAttribute(string DependentName, object DependentValue)
    {
        this.DependentName  = DependentName;
        this.DependentValue = DependentValue;
    }

    public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata Metadata, ControllerContext Context)
    {   
        var rule = new ModelClientValidationRule();
        rule.ErrorMessage = FormatErrorMessage(Metadata.GetDisplayName());
        rule.ValidationParameters.Add("dependentname", DependentName);
        rule.ValidationParameters.Add("dependentvalue", DependentValue);
        rule.ValidationType = "requiredif";
        yield return rule;
    }

    public override bool IsValid(object value)
    {
        if (value == null) { return false; }

        string ValueString = value as string;
        if (ValueString != null) { return (ValueString.Trim().Length != 0); }
        return true;
    }

    protected override ValidationResult IsValid(object Value, ValidationContext Context)
    {
        var ContainerType = Context.ObjectInstance.GetType();
        var Field = ContainerType.GetProperty(this.DependentName);

        if (Field != null)
        {
            var DependentValue = Field.GetValue(Context.ObjectInstance, null);
            if ((DependentValue == null && DependentValue == null) ||
                (DependentValue != null && DependentValue.Equals(this.DependentValue)))
            {
                if (!IsValid(Value))
                {
                    //return new ValidationResult(ErrorMessage, new[] { Context.MemberName });
                    return new ValidationResult(FormatErrorMessage(Context.DisplayName));
                }
            }
        }
        return ValidationResult.Success;
    }

    public string GetPropertyID(ModelMetadata Metadata, ViewContext Context)
    {
        string DependentID = Context.ViewData.TemplateInfo.GetFullHtmlFieldId(this.DependentName);
        var Fie1ldID = Metadata.PropertyName + "_";
        
        return (DependentID.StartsWith(FieldID))
            ? DependentID.Substring(FieldID.Length)
            : DependentID;
    }
}

And then this is my View class with the associated JavaScript:

<script type="text/javascript">
function OnValueChanged(s, e) {
    var Result = ASPxClientCheckBox.Cast(s).GetValue();
    StudentForm.GetItemByName("Address").SetVisible(!Result);
}
</script>

@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()
    @Html.ValidationSummary(false)
    @Html.DevExpress().FormLayout(Form =>
    {
        Form.Name = "StudentForm";
        Form.Items.Add(x => x.Name, Item =>
        {
            (Item.NestedExtensionSettings as TextBoxSettings).ShowModelErrors = true;
            (Item.NestedExtensionSettings as TextBoxSettings).Properties.ValidationSettings.ErrorDisplayMode = ErrorDisplayMode.ImageWithTooltip;
            (Item.NestedExtensionSettings as TextBoxSettings).Properties.ValidationSettings.Display = Display.Dynamic;
        });

        Form.Items.Add(x => x.Address, Item =>
        {
            Item.Name = "Address";
            (Item.NestedExtensionSettings as TextBoxSettings).ShowModelErrors = true;
            (Item.NestedExtensionSettings as TextBoxSettings).Properties.ValidationSettings.ErrorDisplayMode = ErrorDisplayMode.ImageWithTooltip;
            (Item.NestedExtensionSettings as TextBoxSettings).Properties.ValidationSettings.Display = Display.Dynamic;
        });

        Form.Items.Add(x => x.AddressHidden, Item =>
        {
            Item.Name = "AddressHidden";
            (Item.NestedExtensionSettings as CheckBoxSettings).Properties.ClientSideEvents.Init = "OnValueChanged";
            (Item.NestedExtensionSettings as CheckBoxSettings).Properties.ClientSideEvents.ValueChanged = "OnValueChanged";
        });

        Form.Items.Add(x => x.AddressRequired);

        Form.Items.Add(x =>
        {
            x.NestedExtensionType = FormLayoutNestedExtensionItemType.Button;
            ButtonSettings Settings = (ButtonSettings)x.NestedExtensionSettings;
            Settings.UseSubmitBehavior = true;
            Settings.Text = "Submit";
            Settings.Name = "SubmitButton";
        });
    }).Bind(Model).GetHtml()
}

<script type="text/javascript">
    jQuery.validator.unobtrusive.adapters.add('requiredif', ['dependentname', 'dependentvalue'], function (options) {
    options.rules["requiredif"] = true;
    options.messages["requiredif"] = options.message;
    });

    $.validator.addMethod('requiredif', function (value, element, params) {
        var id = '#' + $(element).attr("data-val-requiredif-dependentname");
        var dependentvalue = $(element).attr("data-val-requiredif-dependentvalue");
        var actualvalue = $(id).is(":checked");
        if (dependentvalue == "True" && actualvalue && value.length <= 0) {
            return false;
        }
        return true;
    });
</script>

Upvotes: 1

Views: 6235

Answers (1)

py3r3str
py3r3str

Reputation: 1879

If you want to pass addtitional attibutes to view your RequiredIfAttribute have to implement IClientValidatable interface. It can be done by add this method:

public IEnumerable<ModelClientValidationRule> GetClientValidationRules( ModelMetadata metadata, ControllerContext context)
{
    var rule = new ModelClientValidationRule();
    rule.ErrorMessage = FormatErrorMessage(metadata.GetDisplayName());
    rule.ValidationParameters.Add("dependentname", DependentName);
    rule.ValidationParameters.Add("dependentvalue", DependentValue);
    rule.ValidationType = "requiredif";
    yield return rule;
}

And your js code need some changes to. I am not unobtrusive guru but it works for me:

if ($.validator && $.validator.unobtrusive) {
    $.validator.unobtrusive.adapters.add('requiredif', ['dependentname', 'dependentvalue'], function(options) {
        options.rules["requiredif"] = options.params;
        options.messages["requiredif"] = options.message;
    });

    $.validator.addMethod('requiredif', function(value, element, params) {
        var dependentElement = $("input[name=" + params["dependentname"] + "]");
        var dependentvalue = params["dependentvalue"];
        var actualvalue = dependentElement.parents(".dxWeb_edtCheckBoxChecked_DevEx:first").length > 0; //is checkbox is checked
        if (dependentvalue == "True" && actualvalue && (value == null || value.length <= 0)) {
            return false;
        }
        return true;
    });
}

This code works only for this example. It assumes that RequiresAddress propery is a bool and it is render as chckbox. It needs more coding to work on every type. I thinh it would be better to write another validator for another type.

Is that good solution for you?

btw. RequiredIfAttribute constructor takes three arguments, in Your example in Student class there are two passed;)

Upvotes: 1

Related Questions