1

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>
1

1 Answer 1

1

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;)

Sign up to request clarification or add additional context in comments.

3 Comments

Hmm it doesn't seem to be working. I didn't mention this before - but I am using DevExpress to build my forms. Could you help me understand exactly what is going on in the jQuery script that you wrote? Why do you use 'jQuery' instead of '$' in the first function? What does "data-val-requiredif-dependentname' refer to?
I tried implementing your updates but I still cant't get it to work. I posted the updated code above and I uploaded the solution here (1drv.ms/1kpvbm7). I really appreciate all of your help!
I have edit code. Now it should works with DevExpress forms. But it wont work with standard mvc. I am not familiar with DevExpress so there is a litte bit of hacky code that checks if RequiresAddress is checked. Is it ok now? jQuery and $ are equivalent. You can use both of them in your example. In standard mvc inpust has attributes such as data-val-requiredif-dependentname, but now options.params are passed on adding adapter to $.validator.unobtrusive so it is not needed.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.