Validation - Part 3 - Server-Side
<Update>
You can download the sample application from here. Sorry, it turned into more than just a demo of the validation stuff.
</Update>
So far we've built-up a small foundation for a custom validation framework in part 1, and then tied that to a jQuery plugin for our client-side validation in part 2. In this part we'll look at doing server-side validation. Of the three parts, this is the simplest.
Validator
Most of the heavy lifting takes place in our Validator class. Before looking at it, we'll make use of a simple ValidationError object to hold any errors:
public class ValidationError
{
public string Field { get; private set; }
public string Message { get; private set; }
public ValidationError(string field)
{
Field = field;
}
public ValidationError(string field, string message)
{
Field = field;
Message = message;
}
}
And here's the powerful Validator:
public interface IValidator
{
bool Validate(object o, out ValidationError[] errors);
}
public class Validator : IValidator
{
private readonly IRepository _repository;
public Validator(IRepository repository)
{
_repository = repository;
}
public bool Validate(object o, out ValidationError[] e)
{
var errors = new List<ValidationError>(10);
var propertyErrors = ValidateProperties(o);
if (propertyErrors != null)
{
errors.AddRange(propertyErrors);
}
var customErrors = ValidateCustom(o);
if (customErrors != null)
{
errors.AddRange(customErrors);
}
e = errors.ToArray();
return e.Length == 0;
}
private static ICollection<ValidationError> ValidateProperties(object o)
{
//todo
}
private ICollection<ValidationError> ValidateCustom(object o)
{
//todo
}
}
All the Validate method does is take an object and merge any attribute-based errors with any custom-based errors. Here's the implementation for ValidateProperties:
private static ICollection<ValidationError> ValidateProperties(object o)
{
if (!ValidatorConfiguration.Rules.ContainsKey(o.GetType()))
{
return null;
}
var errors = new List<ValidationError>(10);
foreach (var property in ValidatorConfiguration.Rules[o.GetType()].Properties)
{
var value = property.Property.GetValue(o, null);
foreach (var validator in property.Validators)
{
if (validator.IsValid(o, value)) { continue; }
errors.Add(new ValidationError(property.Property.Name));
break;
}
}
return errors;
}
We loop through each validation attribute of each property of our object (assuming any validation rules are configured for it). Notice that we don't add a message to our ValidationError - that's because those are already rendered on the client in the form of the "tip" - we'll get back to that in a bit.
The ValidateCustom is even simpler:
private ICollection<ValidationError> ValidateCustom(object o)
{
if (!(o is IValidate))
{
return null;
}
return ((IValidate) o).Validate(_repository);
}
Here's an example of what a custom Validate method might look like:
//our user clas
public virtual ValidationError[] Validate(IRepository repository)
{
if (repository.Exists<User>(u => u.Email == Email))
{
return new[] {new ValidationError("Email", "This email is already registered")};
}
return null;
}
Notice that I pass around an IRepository. That's just to show how you might pass infrastructure elements that your validators will need.
Back to the Client
You can now use the Validator from your WebForms or Controllers. The downloadable example shows a more proper (opinionated) MVC example, but without going into the necessary infrastructure, you might do something as simple as:
public class UserController : Controller
{
private readonly IValidator _validator;
public UserController(IValidator validator)
{
_validator = validator;
}
//Everything about this action is ugly, check out the example for a better approach
public ViewResult Register([Bind(Exclude="id")]User user)
{
ValidationError[] errors;
if (!_validator.Validate(user, out errors))
{
ViewData["ValidationErrors"] = ToJson(errors);
return View(user);
}
//todo Save the user;
return View("RegistrationSuccessful");
}
private string ToJSon(ValidationError[] errors)
{
var sb = new StringBuilder("init:{");
Array.ForEach(errors, e => sb.AppendFormat("{0}:'{1}',", e.Field, e.Message.Replace("'", "\\'"));
_validationErrors = sb.RemoveLast().Append("}").ToString();
return _validationErrors;
}
}
We've serialized our array of errors to JSON and stored them in our ViewData. We'll modify our jQuery plugin to handle the errros. First though, we change our jQuery call:
$('#register').validate({<%=Html.RulesFor<User>() %>, <%=ViewData["ValidationErrors"]%>});
The last change is to the initialize method, while we are hooking up each field's blur event, we'll also see if an init error exists within the options:
$fields.each(function(i, field)
{
var $field = $(field);
$field.blur(function()
{
v.validateField(this);
});
if (options.init[field.name] != null)
{
v.markAsInvalid($field, options.init[field.name] != '' ? options.init[field.name] : rules[field.name].tip);
}
});
Conclusion
Folks, that's how easy it is to do custom validation. Granted its quite a bit of code, but there's nothing overly complex (except for maybe the reflection or jQuery plugin if you aren't familiar with either). Do stay tuned as tomorrow I'll make a sample application available for download which should help make all of this more useful.
Posted
Tue, Apr 28 2009 8:08 PM
by
karl