Neither, you’re doing fine. Like everything else one does, it kind of evolves as you go. You’ve stumbled on the toes of the DefaultModelBinder. This is good. It means you’ve tried something, and should now realise that there are times when custom validation, or custom binders, are needed.
Here’s what I do, albeit for MVC 2.0…
The first thing I did was create custom error classes:
public interface IValidationError
{
string Field { get; }
string Error { get; }
}
public interface IValidationErrors : ICollection
{
int Add(string field, string error);
int Add(IValidationError error);
bool IsValid { get; }
}
public class ValidationError : IValidationError
{
public string Field { get; private set; }
public string Error { get; private set; }
public ValidationError(string field, string error)
{
this.Field = field;
this.Error = error;
}
}
public class ValidationErrors : CollectionBase, IValidationErrors
{
public int Add(string field, string error)
{
return this.Add(new ValidationError(field, error));
}
public int Add(IValidationError error)
{
return this.List.Add(error);
}
public bool IsValid
{
get { return this.Count == 0; }
}
}
public abstract class BaseController : Controller
{
[NonAction]
protected void ConsumeError(string field, string error)
{
ModelState.AddModelError(field, error.Localize());
}
[NonAction]
protected void ConsumeError(IValidationError error)
{
ConsumeError(error.Field, error.Error);
}
[NonAction]
protected void ConsumeErrors(IValidationErrors errors)
{
ModelState.Clear();
foreach (IValidationError error in errors)
{
ConsumeError(error);
}
}
[NonAction]
protected void ConsumeException(Exception exception)
{
ConsumeError("_FORM", exception.Message);
}
}
We now have a custom validation error, a runner style collection, and a base controller class designed to clear the model state errors (this will clear your pesky conversion error), and allows us to insert our own messages into the model state. Typically, I do my validation in a service. Such a service interface might look like this:
public interface ICategoryService
{
IEnumerable<Category> GetAll();
Category GetById(int id);
IValidationErrors Add(Category entity);
IValidationErrors Edit(Category entity);
IValidationErrors Delete(Category entity);
IValidationErrors Validate(Category entity);
}
Since the Add, Edit and Delete methods call Validate, your actions can now look like this:
[Authorize(Roles = "Administrator")]
[AcceptVerbs(HttpVerbs.Get)]
public ActionResult Edit(int id)
{
return View(new CategoryEditViewModel()
{
Category = categoryService.GetById(id)
});
}
[Authorize(Roles = "Administrator")]
[AcceptVerbs(HttpVerbs.Post)]
[ValidateAntiForgeryToken()]
public ActionResult Edit(Category entity)
{
ConsumeErrors(categoryService.Add(entity));
if (ModelState.IsValid) return RedirectToAction("Manager");
return View(new CategoryEditViewModel()
{
Category = entity
});
}
Again, since you’re using MVC 1.0 you’ll need to adjust the ConsumeError method of the base controller to this:
protected void ConsumeError(string field, string error)
{
ModelState.AddModelError(field, error.Localize());
ModelState.SetModelValue(field, this.ValueProvider.GetValue(field));
}
With something like this in place, you can now validate anywhere you choose, exactly what you want to validate, and exactly how you want to validate it. You can even consume exceptions and place them in ModelState.
A bit long winded, but I hope it helps anyway.