Group validation messages for multiple properties together into one message asp.net mvc
Asked Answered
V

4

14

I have a view model that has year/month/day properties for someone's date of birth. All of these fields are required. Right now, if someone doesn't enter anything for the date of birth they get 3 separate error messages.

Date of birth fields

What I want to do is somehow group those error messages together into 1 message that just says 'Date of birth is required'. So if 1 or more of those fields are blank, they will always just get the 1 validation message.

I NEED this to work on client-side validation via jquery validate and unobtrusive validate. I know this is possible with the jquery validate plugin by looking at this question. But I don't know how to achieve this with asp.net mvc using validation attributes on my model and unobtrusive validation. Hopefully there's some built in way to group properties for validation purposes, but if not can this be done with a custom validation attribute?

Here's what my existing model and view looks like:

The Model:

public class MyModel {
    [Required(ErrorMessage = "Year is required")]
    public int Year { get; set; }
    [Required(ErrorMessage = "Month is required")]
    public int Month { get; set; }
    [Required(ErrorMessage = "Day is required")]
    public int Day { get; set; }
}

The View:

<div>
    <label>Date of birth: <span style="color:red;">*</span></label>
    <div>@Html.DropDownListFor(m => m.Year, ApplicationModel.GetSelectListForDateRange(DateTime.Today.Year - 16, DateTime.Today.Year - 10), "", new{data_description="birthDate"})@Html.LabelFor(m => m.StudentBirthYear)</div>
    <div>@Html.DropDownListFor(m => m.Month, ApplicationModel.GetSelectListForDateRange(1, 12, true), "", new{data_description="birthDate"})@Html.LabelFor(m => m.StudentBirthMonth)</div>
    <div>@Html.DropDownListFor(m => m.Day, ApplicationModel.GetSelectListForDateRange(1, 31), "", new{data_description="birthDate"})@Html.LabelFor(m => m.StudentBirthDay)</div>
</div>
<div class="error-container">@Html.ValidationMessageFor(m => m.Year)</div>
<div class="error-container">@Html.ValidationMessageFor(m => m.Month)</div>
<div class="error-container">@Html.ValidationMessageFor(m => m.Day)</div>
Variolite answered 10/9, 2013 at 17:35 Comment(0)
P
10

I am somewhat late to the party (only couple of years) still...

Most appropriate solution is indeed creating a CustomAttribute but instead of giving you good advice an leaving to die I will show you how.

Custom Attribute:

public class GroupRequiredAttribute : ValidationAttribute, IClientValidatable
{
    private readonly string[] _serverSideProperties;

    public GroupRequiredAttribute(params string[] serverSideProperties)
    {
        _serverSideProperties = serverSideProperties;
    }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        if (_serverSideProperties == null || _serverSideProperties.Length < 1)
        {
            return null;
        }

        foreach (var input in _serverSideProperties)
        {
            var propertyInfo = validationContext.ObjectType.GetProperty(input);
            if (propertyInfo == null)
            {
                return new ValidationResult(string.Format("unknown property {0}", input));
            }

            var propertyValue = propertyInfo.GetValue(validationContext.ObjectInstance, null);
            if (propertyValue is string && !string.IsNullOrEmpty(propertyValue as string))
            {
                return null;
            }

            if (propertyValue != null)
            {
                return null;
            }
        }

        return new ValidationResult(FormatErrorMessage(validationContext.DisplayName));
    }

    public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
    {
        var rule = new ModelClientValidationRule
        {
            ErrorMessage = ErrorMessage,
            ValidationType = "grouprequired"
        };

        rule.ValidationParameters["grouprequiredinputs"] = string.Join(",", this._serverSideProperties);

        yield return rule;
    }
}

ViewModel: Decorate only one field on your viewModel like following:

    [GroupRequired("Year", "Month", "Day", ErrorMessage = "Please enter your date of birth")]
    public int? Year { get; set; }

    public int? Month { get; set; }

    public int? Day { get; set; }

Jquery: You will need to add adapters in my case it's jquery.validate.unobtrusive.customadapters.js or wherever you register your adapters (you might put this on the page just do it after unobtrusive validation runs).

(function ($) {
    jQuery.validator.unobtrusive.adapters.add('grouprequired', ['grouprequiredinputs'], function (options) {
        options.rules['grouprequired'] = options.params;
        options.messages['grouprequired'] = options.message;
    });
}(jQuery));

jQuery.validator.addMethod('grouprequired', function (value, element, params) {
    var inputs = params.grouprequiredinputs.split(',');
    var values = $.map(inputs, function (input, index) {
        var val = $('#' + input).val();
        return val != '' ? input : null;
    });
    return values.length == inputs.length;
});

and that should do it.

For those who are interested what this does: In C# land it grabs the ids of fields glues them with , and puts into custom attribute on Year field.

HTML should look something like this (If it doesn't debug C# attribute):

<input class="tooltip form-control input dob--input-long" data-val="true" data-val-grouprequired="Please enter your date of birth" data-val-grouprequired-grouprequiredinputs="Year,Month,Day" name="Year" placeholder="YYYY" tabindex="" type="text" value="">

Then Jquery validation splits them back into id's and checks if all of them are not empty and that's pretty much it.

You will want to mark fields as invalid somehow (now it would only mark the field attribute is sitting on) most appropriate solution IMHO is to wrap all fields in container with class field-error-wrapper and then add following to your page after Jquery validation is loaded:

$.validator.setDefaults({
    highlight: function (element) {
        $(element).closest(".field-error-wrapper").addClass("input-validation-error");
    },
    unhighlight: function (element) {
        $(element).closest(".field-error-wrapper").removeClass("input-validation-error");
    }
});

instead of marking field it will mark container and then you can write your css in a way that if container is marked with .input-validation-error then all fields inside turn red. I think my job here is done.

EDIT: Ok so there appears to be one more issue where fields get unmarked because validator thinks that day and month are valid and it needs to remove invalid class from parent, validator first marks invalid fields then unmarks valid which causes validation not to get highlighted, so I changed the sequence in which validation happens, I wouldn't recommend overriding this globally (cause I am not sure on what catastrophic side affects it might have) just paste it on the page where you have birthdate fields.

$(function () {
    $.data($('form')[0], 'validator').settings.showErrors = function () {
        if (this.settings.unhighlight) {
            for (var i = 0, elements = this.validElements() ; elements[i]; i++) {
                this.settings.unhighlight.call(this, elements[i], this.settings.errorClass, this.settings.validClass);
            }
        }
        this.hideErrors();
        for (var i = 0; this.errorList[i]; i++) {
            var error = this.errorList[i];
            this.settings.highlight && this.settings.highlight.call(this, error.element, this.settings.errorClass, this.settings.validClass);
            this.showLabel(error.element, error.message);
        }
        if (this.errorList.length) {
            this.toShow = this.toShow.add(this.containers);
        }
        if (this.settings.success) {
            for (var i = 0; this.successList[i]; i++) {
                this.showLabel(this.successList[i]);
            }
        }
        this.toHide = this.toHide.not(this.toShow);

        this.addWrapper(this.toShow).show();
    };
});

Hope this saves you some time.

Pegram answered 2/12, 2015 at 10:52 Comment(2)
This appears to be the more "mvc unobtrusive validation" way to handle the issue. I've handled other group validation scenarios in a similar way with the string[] approach in the custom attribute. I don't have time, or a great page to test this on anymore, but it appears as though it would do the trick.Variolite
This isn't triggering for me but I have an slightly different setup. I have multiple groups of the same fields. I'm using the BeginCollection NuGet pkg so that they fields all have unique names therefore each field has a GUID in the name. Will this work for that or does someone have a suggestion on how to make it work?Schreibman
U
1

Hi I hope this fulfill your requirement

//--------------------------HTML Code-----------------------------
    <form id="myform">  
        <select name="day">
            <option value="">select</option>
            <option value="1">1</option>
        <select>
             <select name="mnth">
            <option value="">select</option>
            <option value="Jan">Jan</option>
        <select>
             <select name="yr">
            <option value="">select</option>
            <option value="2015">2015</option>
        <select>
            <br/>
        <input  type="submit"  />
                    <br/>
            <div id="msg"></div>
    </form>



    //-------------------------------JS Code to validate-------------



      $(document).ready(function() {


    $('#myform').validate({
        rules: {
            day: {
                required: true
            },
            mnth: {
                required: true
            },
             yr: {
                required: true
            }
        },
          errorPlacement: function (error, element) {
            var name = $(element).attr("name");
            error.appendTo($("#msg"));
        },
        messages: {
            day: {
                required: "Date of birth is required"
            },
            mnth: {
                required: "Date of birth is required"
            },
             yr: {
                required: "Date of birth is required"
            }
        },
        groups: {
            p: "day mnth yr"
        },
        submitHandler: function(form) { // for demo
            alert('valid form');
            return false;
        }
    });

});

Here is Running Example

Universalize answered 30/1, 2015 at 20:24 Comment(0)
S
0

You should implement IValidatableObject and take of the Require. Then the validation on the server side will do the job, something like:

public class MyModel : IValidatableObject
{
  public int Year { get; set; }
  public int Month { get; set; }
  public int Day { get; set; }

  public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
  {
      if (/*Validate properties here*/) yield return new ValidationResult("Invalid Date!", new[] { "valideDate" });
  }
}

For client side validation you need to implement your own function, and prompt the error to the user somehow.

EDIT: Given that you still need client side validation, you should do something like this:

$("form").validate({
    rules: {
        Day: { required: true },
        Month : { required: true },
        Year : { required: true }
    },
    groups: {
        Date: "Day Month Year"
    },
   errorPlacement: function(error, element) {
       if (element.attr("id") == "Day" || element.attr("id") == "Month" || element.attr("id") == "Year") 
        error.insertAfter("#Day");
       else 
        error.insertAfter(element);
   }
});
Schauer answered 10/9, 2013 at 19:33 Comment(3)
I understand the server-side validation and implementing IValidatableObject, but my issue is getting it to basically have only 1 error message with the client side validation. It seems like there would be something built in to MVC to handle this type of grouping. If there isn't though I don't know if I need to make my own ValidationAttribute or something similarVariolite
You didnt specified that in the original post! So I gave my best tip! Lets see if i can help you!Schauer
Using this JavaScript works, but none of my other fields validate on the client side.. It seems that it's overwriting the validation rules MVC has put in place from the view model attributes.Wrand
W
0

You could do that simply using CustomAttribute.

Just put this attribute on your model

[CustomValidation(typeof(MyModel), "ValidateRelatedObject")]

and then simply define the rules to validate the values in the following method:

public static ValidationResult ValidateRelatedObject(object value, ValidationContext context)
{
    var context = new ValidationContext(value, validationContext.ServiceContainer, validationContext.Items);
    var results = new List<ValidationResult>();
    Validator.TryValidateObject(value, context, results);

    // TODO: Wrap or parse multiple ValidationResult's into one ValidationResult

    return result; 
}

For more information, you could visit this link.

Wall answered 10/9, 2013 at 19:47 Comment(1)
It looks like this will only do server-side validation. I really need this to work with the unobtrusive client-side validation and jquery.validateVariolite

© 2022 - 2024 — McMap. All rights reserved.