Those who have worked with ASP.NET MVC for more than a day have no doubt found themselves repeating common patterns when handling POSTs. Jimmy Bogard recently blogged one way to simplify your actions. I handled the same problem in Fail Tracker by implementing a very simple convention (one-model-in, one-model-out) and pushing some responsibility into the application framework. With this in place, cross-cutting POST handling logic can be pushed out of the action methods, and a common “doh” error (forgetting to perform server-side validation) can be eliminated. Read on to find out how you can adopt this simple convention in your application framework.
[more]
Handling Posts
As Jimmy pointed out in his post, the usual pattern for handling a POST in an MVC application looks like:
[HttpPost] public ActionResult Edit(SomeEditModel form) { if (IsNotValid) { return ShowAView(form); } DoActualWork(); return RedirectToSuccessPage(); }
When handling a post, we check to see if the form is valid, and if not, we re-display the view. Otherwise, we do something, then redirect to a success page. Here’s a POST handler in Fail Tracker that adds a new issue to a project:
[HttpPost] public ActionResult Index(AddIssueForm form) { if (!ModelState.IsValid) { return View(form); } var project = _projects.Query().Single(p => p.ID == form.TargetProjectID); var issue = Issue.CreateNewIssue(project, form.Title, form.CurrentUser, form.Body); issue.ChangeTypeTo(form.Type); issue.ChangeSizeTo(form.Size); if (form.AssignedTo.HasValue) { issue.ReassignTo(_users.Query().Where(u => u.ID == form.AssignedTo).Single()); } _issues.Save(issue); return this.RedirectToAction<IssuesController>(c => c.View(issue.ID)); }
Most of that method deals with doing the actual work, but the method must also check that the POSTed data is valid, and that bit of logic (which is quite easy to forget) shows up in every single POST handler in Fail Tracker. Or at least, it would show up if it wasn’t handled automatically by the framework.
Globally Handling Validation
All controllers in Fail Tracker inherit from a layer supertype controller called FailTrackerController. This base controller hooks in to the ASP.NET MVC request processing pipeline and handles validation automagically:
public abstract class FailTrackerController : Controller { protected override void OnActionExecuting(ActionExecutingContext filterContext) { if (ActionNeedsServerSideValidation(filterContext)) { if (!filterContext.Controller.ViewData.ModelState.IsValid) { HandleModelValidationFailure(filterContext); } } base.OnActionExecuting(filterContext); } private static bool ActionNeedsServerSideValidation(ActionExecutingContext filterContext) { return filterContext.ActionDescriptor.GetParameters().Any(p => p.ParameterType.Name.Contains("Form")); } private static void HandleModelValidationFailure(ActionExecutingContext filterContext) { var result = new ViewResult { ViewData = new ViewDataDictionary(filterContext.Controller.ViewData) { Model = filterContext.ActionParameters.Values.Single() }}; filterContext.Result = result; } }
By convention in Fail Tracker, input model names are suffixed with “Form” when the model is the input to a POST handling action method. Fail Tracker also enforces the “one model in” convention, so action methods are allowed to have *at most* one input model. The base controller leverages these conventions to automatically provide server-side validation. Prior to executing an action, the base controller first checks to see if validation is necessary (does the action have a parameter whose type ends in “Form?”). If so, it performs the usual check on the ModelState, and returns a ViewResult with the model if validation fails.
Because validation is now automatically handled by the application framework, my actions aren’t littered with repetitive code, and I don’t have to worry about accidentally forgetting to perform server-side validation. Should I want to opt out of this convention, I need only drop the “Form” suffix from my model type.