Asp.Net MVC: Custom Validation by Dataannotation

ASP.NET MVC: Custom Validation by DataAnnotation

You could write a custom validation attribute:

public class CombinedMinLengthAttribute: ValidationAttribute
{
public CombinedMinLengthAttribute(int minLength, params string[] propertyNames)
{
this.PropertyNames = propertyNames;
this.MinLength = minLength;
}

public string[] PropertyNames { get; private set; }
public int MinLength { get; private set; }

protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var properties = this.PropertyNames.Select(validationContext.ObjectType.GetProperty);
var values = properties.Select(p => p.GetValue(validationContext.ObjectInstance, null)).OfType<string>();
var totalLength = values.Sum(x => x.Length) + Convert.ToString(value).Length;
if (totalLength < this.MinLength)
{
return new ValidationResult(this.FormatErrorMessage(validationContext.DisplayName));
}
return null;
}
}

and then you might have a view model and decorate one of its properties with it:

public class MyViewModel
{
[CombinedMinLength(20, "Bar", "Baz", ErrorMessage = "The combined minimum length of the Foo, Bar and Baz properties should be longer than 20")]
public string Foo { get; set; }
public string Bar { get; set; }
public string Baz { get; set; }
}

ASP.NET MVC custom multiple fields validation

In order to get client side validation, you need to pass the values of the 'other properties' in the ModelClientValidationRule by using the .Add() method of the rules ValidationParameters property, and then write the client side scripts to add the rules to the $.validator.

But first there are a few other issues to address with your attribute. First you should execute your foreach loop only if the value of the property you applied the attribute is null. Second, returning a ValidationResult if one of the 'other properties' does not exist is confusing and meaningless to a user and you should just ignore it.

The attribute code should be (note I changed the name of the attribute)

public class RequiredIfAnyAttribute : ValidationAttribute, IClientValidatable
{
private readonly string[] _otherProperties;
private const string _DefaultErrorMessage = "The {0} field is required";

public RequiredIfAnyAttribute(params string[] otherProperties)
{
if (otherProperties.Length == 0) // would not make sense
{
throw new ArgumentException("At least one other property name must be provided");
}
_otherProperties = otherProperties;
ErrorMessage = _DefaultErrorMessage;
}

protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (value == null) // no point checking if it has a value
{
foreach (string property in _otherProperties)
{
var propertyName = validationContext.ObjectType.GetProperty(property);
if (propertyName == null)
{
continue;
}
var propertyValue = propertyName.GetValue(validationContext.ObjectInstance, null);
if (propertyValue != null)
{
return new ValidationResult(FormatErrorMessage(validationContext.DisplayName));
}
}
}
return ValidationResult.Success;
}
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
var rule = new ModelClientValidationRule
{
ValidationType = "requiredifany",
ErrorMessage = FormatErrorMessage(metadata.GetDisplayName()),
};
/ pass a comma separated list of the other propeties
rule.ValidationParameters.Add("otherproperties", string.Join(",", _otherProperties));
yield return rule;
}
}

The scripts will then be

sandtrapValidation = {
getDependentElement: function (validationElement, dependentProperty) {
var dependentElement = $('#' + dependentProperty);
if (dependentElement.length === 1) {
return dependentElement;
}
var name = validationElement.name;
var index = name.lastIndexOf(".") + 1;
var id = (name.substr(0, index) + dependentProperty).replace(/[\.\[\]]/g, "_");
dependentElement = $('#' + id);
if (dependentElement.length === 1) {
return dependentElement;
}
// Try using the name attribute
name = (name.substr(0, index) + dependentProperty);
dependentElement = $('[name="' + name + '"]');
if (dependentElement.length > 0) {
return dependentElement.first();
}
return null;
}
}

$.validator.unobtrusive.adapters.add("requiredifany", ["otherproperties"], function (options) {
var element = options.element;
var otherNames = options.params.otherproperties.split(',');
var otherProperties = [];
$.each(otherNames, function (index, item) {
otherProperties.push(sandtrapValidation.getDependentElement(element, item))
});
options.rules['requiredifany'] = {
otherproperties: otherProperties
};
options.messages['requiredifany'] = options.message;
});

$.validator.addMethod("requiredifany", function (value, element, params) {
if ($(element).val() != '') {
// The element has a value so its OK
return true;
}
var isValid = true;
$.each(params.otherproperties, function (index, item) {
if ($(this).val() != '') {
isValid = false;
}
});
return isValid;
});

ASP.NET MVC: Custom Validation by DataAnnotation base on two fields length

You can write a custom ValidationAttribute that reads other properties from your model. You can do so using the ValidationContext available to you in your overload of the IsValid method.

You can access a different property this way:

    var otherPropertyInfo = validationContext.ObjectType.GetProperty("OtherPropertyName");
if (otherPropertyInfo == null) {
return new ValidationResult("The other property doesn't exist");
}

object otherPropertyValue = otherPropertyInfo.GetValue(validationContext.ObjectInstance, null);

In this example, the value will be in otherPropertyValue and you can implement the validations you need using this value along with the value of your annotated property (materialsconfirm in your case).

You can make this attribute work on the client side by signaling from your ValidationAttribute that it has client side validation and creating a jQuery validator for it, as illustrated here, but that will require you to translate your validation logic into JS.

If you want these validations to happen on the client side using server-side logic, you can implement the [Remote] attribute which lets you specify an action that will be called to validate the property. You can also specify additional properties that will be used to perform the validation. You can find some examples on how to implement this option including additional properties here. The downside is that this validation is not enforced on the server.

How Can I Use Custom Validation Attributes on Child Models of a DB Entity?


Am I trying to do something that is basically impossible (or at least
very difficult)?

No, there is a very simple solution that integrates perfectly with the framework and technologies using DataAnnotations.

You can create a custom ValidationAttribute that is called by EF Validation and call Validator.TryValidateObject inside. This way, when CustomValidation.IsValid is called by EF you launch child complex object validation by hand and so on for the whole object graph. As a bonus, you can gather all errors thanks to CompositeValidationResult.

i.e.

using System;
using System.ComponentModel.DataAnnotations;
using System.Collections.Generic;

public class Program
{
public static void Main() {
var person = new Person {
Address = new Address {
City = "SmallVille",
State = "TX",
Zip = new ZipCode()
},
Name = "Kent"
};

var context = new ValidationContext(person, null, null);
var results = new List<ValidationResult>();

Validator.TryValidateObject(person, context, results, true);

PrintResults(results, 0);

Console.ReadKey();
}

private static void PrintResults(IEnumerable<ValidationResult> results, Int32 indentationLevel) {
foreach (var validationResult in results) {
Console.WriteLine(validationResult.ErrorMessage);
Console.WriteLine();

if (validationResult is CompositeValidationResult) {
PrintResults(((CompositeValidationResult)validationResult).Results, indentationLevel + 1);
}
}
}

}

public class ValidateObjectAttribute: ValidationAttribute {
protected override ValidationResult IsValid(object value, ValidationContext validationContext) {
var results = new List<ValidationResult>();
var context = new ValidationContext(value, null, null);

Validator.TryValidateObject(value, context, results, true);

if (results.Count != 0) {
var compositeResults = new CompositeValidationResult(String.Format("Validation for {0} failed!", validationContext.DisplayName));
results.ForEach(compositeResults.AddResult);

return compositeResults;
}

return ValidationResult.Success;
}
}

public class CompositeValidationResult: ValidationResult {
private readonly List<ValidationResult> _results = new List<ValidationResult>();

public IEnumerable<ValidationResult> Results {
get {
return _results;
}
}

public CompositeValidationResult(string errorMessage) : base(errorMessage) {}
public CompositeValidationResult(string errorMessage, IEnumerable<string> memberNames) : base(errorMessage, memberNames) {}
protected CompositeValidationResult(ValidationResult validationResult) : base(validationResult) {}

public void AddResult(ValidationResult validationResult) {
_results.Add(validationResult);
}
}

public class Person {
[Required]
public String Name { get; set; }

[Required, ValidateObject]
public Address Address { get; set; }
}

public class Address {
[Required]
public String Street1 { get; set; }

public String Street2 { get; set; }

[Required]
public String City { get; set; }

[Required]
public String State { get; set; }

[Required, ValidateObject]
public ZipCode Zip { get; set; }
}

public class ZipCode {
[Required]
public String PrimaryCode { get; set; }

public String SubCode { get; set; }
}

ASP.NET MVC enable custom validation attributes

This question has been answered here:

How to create custom validation attribute for MVC

In order to get your custom validator attribute to work, you need to register it. This can be done in Global.asax with the following code:

public void Application_Start()
{
System.Web.Mvc.DataAnnotationsModelValidatorProvider.RegisterAdapter(
typeof (MyNamespace.RequiredAttribute),
typeof (System.Web.Mvc.RequiredAttributeAdapter));
}

(If you're using WebActivator you can put the above code into a startup class in your App_Start folder.)

My custom attribute class looks like this:

public class RequiredAttribute : System.ComponentModel.DataAnnotations.RequiredAttribute
{
private string _propertyName;

public RequiredAttribute([CallerMemberName] string propertyName = null)
{
_propertyName = propertyName;
}

public string PropertyName
{
get { return _propertyName; }
}

private string GetErrorMessage()
{
// Get appropriate error message from Sitecore here.
// This could be of the form "Please specify the {0} field"
// where '{0}' gets replaced with the display name for the model field.
}

public override string FormatErrorMessage(string name)
{
//note that the display name for the field is passed to the 'name' argument
return string.Format(GetErrorMessage(), name);
}
}


Related Topics



Leave a reply



Submit