Thanks to Scott Guthrie's series of posts on the ASP.NET MVC Framework, I was able to pretty quickly get started once the first CTP dropped. However, I was still having trouble getting my head around how scenarios like validation may work in MVC, so I decided to try coding a simple validated form. It goes without saying that this is subject to change and provided as-is, but here's my first stab at it.
Overview
In many cases, we're used to validation being part of the User Interface. We drag a control that tells ASP.NET "this text box is required. Set a few properties, and it's done. This is convenient and may work in many cases. However this may not be the best place for this sort of logic. If you have multiple UIs, then you have to code this logic in each one. If rules change, then you have to find the pages that have the rules and change them. If your code is of any size, then you may want to consider putting these rules in a business layer. In MVC, they belong on the model - those classes that represent data in your application. Then it becomes the View's role to display any errors to the user and except input to correct the broken rules. The Controller's job is to mediate between the View and the Model and decide what to do with invalid or valid Model instances.
Setting Up The View
The MVC Web Application project gets you started with a simple company website. For this example, we'll create a "Contact Us" form that includes validation to prevent incorrect email addresses or empty messages. So, the first thing to do is to create the view: rt click Views\Home -> New Item -> MVC View Content Page -> (ContactUs.aspx) -> Add
In MVC, the View is a User Interface that interacts with a Model to display data and get input. For this form, we want the View to work against a ContactForm class, which will be our model. So, we specify in the page's code behind that this is a "ViewPage of type ContactForm":
public partial class ContactUs : ViewPage<ContactForm>
This will make the page's ViewData property be of type ContactForm. We'll create the ContactForm class in a minute. To really know what needs to go on the ContactForm class, we need to finish up the view. Reference MvcToolkit and add this to bring in the HtmlHelper extension methods:
using System.Web.Mvc.BindingHelpers;
Next, add the following to the page's markup:
<%using(Html.Form("SendContactForm","Home", FormExtensions.FormMethod.post)){ %> <%if (!ViewData.IsValid){ %> <ul><%=ViewData.ValidationErrors.ToFormattedList("<li>{0}</li>")%></ul> <%} %> <label for="FromEmailAddress">Your Email Address</label><br /> <%=Html.TextBox("FromEmailAddress", ViewData.FromEmailAddress) %><br /> <label for="Subject">Subject</label><br /> <%=Html.TextBox("Subject", ViewData.Subject) %><br /> <label for="MessageBody">Message</label><br /> <%=Html.TextArea("MessageBody", ViewData.MessageBody) %><br /> <%=Html.SubmitButton() %> <%} %>
This will render a form based on the ContactForm instance passed to the view. If the form is not valid, then any errors will be displayed on the view. Based on this, we now know we need a model with IsValid, ValidationErrors, FromEmailAddress, Subject, and MessageBody properties.
Adding a Self-Validating Model
The View will render a ContactForm instance, which contains information about whether or not it is currently valid. There are a number of ways to do this. Microsoft ships IDataErrorInfo in the framework, and Enterprise Library comes with a validation framework. I have my suspicions that MVC will ship with something to address this in the future. While it seems simple, things like globalization and dependency injection come into play and it can get complex quickly. So for now, we're not going to confuse things with an elaborate validation framework. We just want a model class that knows when it's valid and allows the view to get a list of error messages.
Here's a simple ContactForm class that does just that:
public class ContactForm { private IList<string> _ValidationErrors = new List<string>(); public IEnumerable<string> ValidationErrors { get { return _ValidationErrors; } } public bool IsValid { get; set; } public string FromEmailAddress { get; set; } public string Subject { get; set; } public string MessageBody { get; set; } public void Validate() { _ValidationErrors.Clear(); if (String.IsNullOrEmpty(MessageBody)) _ValidationErrors.Add("Message body is required."); if (String.IsNullOrEmpty(Subject)) _ValidationErrors.Add("Subject is required."); if (String.IsNullOrEmpty(FromEmailAddress)) { _ValidationErrors.Add("Email address is required."); } else if (!System.Text.RegularExpressions.Regex.IsMatch(FromEmailAddress, @"\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}\b",System.Text.RegularExpressions.RegexOptions.IgnoreCase)) { _ValidationErrors.Add("Email addresses must be in the format '[email protected]'."); } this.IsValid = _ValidationErrors.Count == 0; } }
The only job this class has is to contain ContactForm data, and to know when the data is or isn't valid. One problem with the above is that it requires the developer to call "Validate()" explicitly, instead of validating when a property value changes. We'll refactor that out later, but for now, this gives us what we need in the model.
Gluing it Together with a Controller
We now have a view to display our model, but the controller isn't yet aware of the views. To make the HomeController handle requests to /ContactUs and /SendContactForm, add the following methods:
[ControllerAction] public void ContactUs() { RenderView("ContactUs", new ContactForm()); } [ControllerAction] public void SendContactForm() { var contactForm = new ContactForm(); contactForm.UpdateFrom(Request.Form);
contactForm.Validate(); if (!contactForm.IsValid) { RenderView("ContactUs", contactForm); } else { //TODO: Send email RenderView("ContactFormThanks"); } }
The first method simply creates a new instance of ContactForm and passes it to the view. The second creates a ContactForm instance and updates it from the form input. If it is not valid, then it returns to the view, passing in the invalid instance. If it is valid, then it sends the email and renders a "thank you" view.
Now, if we run and browse to /Home/ContactUs, our email form is displayed. Click 'Submit' with invalid data, and the errors are displayed. Click 'Submit' with valid data, and we are redirected to the confirmation page. Add a little CSS, and viola, an email form with validation!
Where to go from Here
This is a simple example of validation using the new MVC framework. There's plenty of work left, though. Here are a few things we'll try to hit in a future posts:
- Refactoring the validation logic to use interfaces and associate error messages with properties.
- Examine other validation frameworks for use with MVC.
- Adding Extension Methods to simplify creating forms that display validation errors.
- Allowing for localization.
3 comments:
I tried something similiar to this a few days ago. The problem that I came across was that the url would change to "home/submitcontactform" after submitting. This is expected behavior, but it would be nice to have it return to "home/contactus" even if there was validation errors. How could you accomplish this? I don't think you can simply redirect because you need to pass the new view data with validation messages. There's something called RedirectToAction, but I couldn't get it to pass data.
Jim
Jim:
You can solve this by using RedirectToAction instead of RenderView, and by putting the necessary data in ViewContext.TempData instead of ViewData.
Thanks for the help! This got me started. I must say, it's going to be hard to resist the urge to implement my own validation framework like I did for my WinForms MVP pattern.
http://blog.jeffhandley.com/archive/2008/01/15/extended-mvp-pattern---domain-validation.aspx
Post a Comment