I have a custom class I've written which allows me to validate individual properties of items, without firing validation errors on other properties. This allows me to individually validate what my users are editing without flinging up a lot of red mess on fields they haven't gotten to yet.
The validator uses reflection to get the properties to validate, this is so that I can trigger property validation in my view model in response to control updates on the view, and this lets me use databinding strings to identify the properties without needing any access to the model on the view-side. The validation rules are specified using data annotation attributes on the model.
The validation methods attempt to attach validation errors to the object being validated if possible (i.e. if it's a ComplexObject
or Entity
type) but also returns a collection of them to be processed manually.
For completeness, the validator also allows me to validate whole items as a helper, for use when submitting final copies of items.
using ComponentModelValidator = System.ComponentModel.DataAnnotations.Validator;
/// <summary>
/// Uses data annotation attributes to validate whole items and individual properties
/// </summary>
public class Validator
{
/// <summary>
/// Validates properties on the provided item using validation attributes.
/// </summary>
/// <remarks>
/// The validation errors are attached to the object if it is a ComplexObject or Entity.
/// </remarks>
/// <param name="item">The item to validate.</param>
/// <returns>A collection of validation errors.</returns>
/// <exception cref="System.ArgumentNullException">Thrown when item is null.</exception>
public ICollection<ValidationResult> ValidateItem(object item)
{
if (item == null)
{
throw new ArgumentNullException("item");
}
if (item is Entity)
{
((Entity)item).ValidationErrors.Clear();
}
if (item is ComplexObject)
{
((ComplexObject)item).ValidationErrors.Clear();
}
var validationResults = new Collection<ValidationResult>();
ComponentModelValidator.TryValidateObject(item, new ValidationContext(item), validationResults);
if (item is Entity)
{
foreach (var validationResult in validationResults)
{
((Entity)item).ValidationErrors.Add(validationResult);
}
}
if (item is ComplexObject)
{
foreach (var validationResult in validationResults)
{
((ComplexObject)item).ValidationErrors.Add(validationResult);
}
}
return validationResults;
}
/// <summary>
/// Validates the value of the property on the object provided using validation attributes.
/// </summary>
/// <remarks>
/// The validation errors are attached to the object if it is a ComplexObject or Entity.
/// </remarks>
/// <typeparam name="T"></typeparam>
/// <param name="item">The item that owns the property to be validated.</param>
/// <param name="propertyName">Name of the property to be validated.</param>
/// <returns>A collection of validation errors.</returns>
/// <exception cref="System.ArgumentNullException"> Thrown when item or propertyName are null.</exception>
/// <exception cref="System.ArgumentException">Thrown when propertyName is the empty String.</exception>
public ICollection<ValidationResult> ValidateProperty<T>(T item, string propertyName)
{
if (item == null)
{
throw new ArgumentNullException("item");
}
if (propertyName == null)
{
throw new ArgumentNullException("propertyName");
}
if (propertyName.Equals(String.Empty))
{
throw new ArgumentException("Argument must not be the empty String.", "propertyName");
}
// need to keep this as object so we can access ValidationErrors regardless of whether target is an Entity or a ComplexObject, etc.
object target = item;
target = GetRelativeProperty(propertyName, target);
var validationResults = new Collection<ValidationResult>();
var validationContext = new ValidationContext(target, null, null)
{
MemberName = propertyName.Split('.').Last()
};
object value = GetPropertyValue(target, validationContext.MemberName);
ComponentModelValidator.TryValidateProperty(value, validationContext, validationResults);
if (target is Entity)
{
// remove existing validation errors for this property.
var targetAsEntity = ((Entity)target);
AddValidationErrorsToEntity(validationResults, validationContext, targetAsEntity);
}
else if (target is ComplexObject)
{
// remove existing validation errors for this property.
var targetAsComplexObject = ((ComplexObject)target);
AddValidationErrorsToComplexObject(validationResults, validationContext, targetAsComplexObject);
}
return validationResults;
}
private static object GetRelativeProperty(string propertyName, object target)
{
var prependPropertyName = String.Empty;
if (propertyName.Contains('.'))
{
prependPropertyName = propertyName.Substring(0, propertyName.LastIndexOf('.') + 1);
var dotIndices = new List<int>();
// add -1 here, because we want to add the first block of text, but that doesn't begin with a '.', which we correct for later
dotIndices.Add(-1);
for (var i = 0; i < propertyName.Length; i++)
{
if (propertyName[i] == '.')
{
dotIndices.Add(i);
}
}
// we don't go through the last property because we want to grab the leaf object's parent, not the leaf object itself.
for (var i = 0; i < dotIndices.Count - 1; i++)
{
// add 1 to the index to skip the full stop character.
var prop = i == dotIndices.Count - 1 ?
propertyName.Substring(dotIndices[i] + 1) :
propertyName.Substring(dotIndices[i] + 1, dotIndices[i + 1] - dotIndices[i] - 1);
target = GetPropertyValue(target, prop);
}
}
return target;
}
private static void AddValidationErrorsToComplexObject(Collection<ValidationResult> validationResults, ValidationContext validationContext, ComplexObject targetAsComplexObject)
{
var errors = targetAsComplexObject.ValidationErrors.ToList();
//Remove all errors that are in the new validation results list so we don't duplicate.
errors.RemoveAll(
error => error.MemberNames.Any(
memberName => memberName.EndsWith(validationContext.MemberName, StringComparison.Ordinal)));
targetAsComplexObject.ValidationErrors.Clear();
foreach (var validationResult in validationResults.Concat(errors))
{
targetAsComplexObject.ValidationErrors.Add(validationResult);
}
}
private static void AddValidationErrorsToEntity(Collection<ValidationResult> validationResults, ValidationContext validationContext, Entity targetAsEntity)
{
var errors = targetAsEntity.ValidationErrors.ToList();
//Remove all errors that are in the new validation results list so we don't duplicate.
errors.RemoveAll(
error => error.MemberNames.Any(
memberName => memberName.EndsWith(validationContext.MemberName, StringComparison.Ordinal)));
targetAsEntity.ValidationErrors.Clear();
foreach (var validationResult in validationResults.Concat(errors))
{
targetAsEntity.ValidationErrors.Add(validationResult);
}
}
private static object GetPropertyValue(object target, string propertyName)
{
return target.GetType().GetProperty(propertyName).GetValue(target, null);
}
}
GetRelativeProperty
takes a base object and a property string like "Item.Value.Range.Maximum" and traverses the object's properties to get theMaximum
property. \$\endgroup\$