Input Validation in ASP.NET MVC
Explore Model State and Data Validation
October 30, 2009
InData Entry and ASP.NET MVC I discussed how to arrange adata entry page using the ASP.NET MVC Framework. There s a common sentimentaround the community that the ASP.NET MVC Framework is not particularly suitedto data entry. I was one of those sustaining this point of view. Well, it lasteduntil I tried it out seriously. Now I ll probably remain a fan of Web Forms formost of the foreseeable future, but this is not a good reason to yield toprejudice when it comes to ASP.NET MVC and data entry. Data entry (and inputvalidation as we ll see in this article) can be equally as effective in ASP.NETMVC as it is in Web Forms. This said, feel free to pick up the framework withwhich you feel most comfortable. But let s make it clear it is a matter ofpreference.
Figure 1 shows where I left off last month. The figureshows a Web page through which a user can select a customer from a list andclick to edit its record. When this happens, a new view is displayed thatincludes a user control. The user control incorporates the user interface to editthe content of the bound record. The user control also contains a Save buttonand posts a new request to the site to update the record. Keeping the editingstuff isolated in a user control makes it easy for you to replace it anddecouples to an acceptable level the host page from the edit-record engine.
Figure 1: A sample data entry page.
In this article, I ll further enhance the user control toadd the ability to display user messages that regard validation and othererrors. In doing so, I ll also be using the jQuery library to add someanimation.
A Common Pattern for a Data Entry Page
Since the advent of ASP.NET 1.0, I ve been working onquite a few Web pages to do data entry. When it comes to this, you haveessentially two points to address: isolating the editing UI and update logicfrom the page, and letting the page know about what happens during the editingphase.
By employing a user control for editing the record, you areable to isolate the editing UI from the page. Furthermore, in an ASP.NET MVCscenario, the logic to handle the update is already out of the page, isolatedin the controller. You must figure out how to pass data to the user control;possibly in a way that abstracts the actual view away. In addition, you mustfigure out how the user control communicates with the host page. This regardshow the input data is being passed to the user control, but also how messagesfor errors and notifications are shown.
Figure 2 shows structure and overall behavior of a samplepage using this pattern. When the user clicks to edit, a POST is made to theWeb server to turn the selected record into edit mode.
Figure 2: Internal architecture ofan ASP.NET MVC data entry page.
The controller s Edit action is invoked for a POST verb:
[ActionName("Edit"), AcceptVerbs(HttpVerbs.Post)]public ActionResult EditViaPost(string listCustomers){string customerID = listCustomers;return RedirectToAction("Edit",new RouteValueDictionary(new { id = customerID }));}
The parameter indicates the currently selected element inthe list. The action method simply redirects to the Edit action for a GET verbthe same action you expect to take place when an input/edit/id URL isrequested. This ensures a dual programming interface to the page the user getsto the same point via both the user interface and the address bar.
The Edit action for GET verbs performs two steps. First,it refills the list and selects the element identified by the URL argument.Second, it stores in the view s data container any data needed to update theuser interface. It is recommended that you opt for a custom data structure thatabstracts away the data required by the next view. However, a similar fragmentof code still works:
object data = GetContentForTheUserControl(...)ViewData["CustomerData"] = data;
The view will check the presence of data for the usercontrol and display it on demand, as shown here:
<%object data = ViewData["CustomerData"];if (data == null)return;Html.RenderPartial(userControlViewName, data);%>
Everything else takes place within the user control. Theuser control renders its own view, drawing data from passed objects, and waitsfor the user to enter data. Let s skip for just a moment over validation andfocus on the Save button. When the user clicks the Save button, a POST requestis generated to a URL input/update/id. The controller s Update action does thefollowing:
public ActionResult Update(string id){// Update the data model (if current data is OK)UpdateRecord(id);// Save a message to UISetMessageBar();// Redirect to Edit mode on the current recordRedirectToAction("Edit", new RouteValueDictionary(new{ id = id }));}
The first step accomplished by the controller is updating therecord provided that valid data has been provided through the user interface.The controller s method receives this data from the ASP.NET MVC machinery. Inturn, the ASP.NET MVC machinery gets raw data from the Request.Form collectionpacked with the POST request (see last month s article for details).
Finally, the controller sets any information for the userinterface and redirects to Edit mode. This is the pattern I found verycomfortable to use in a data entry page, and especially easy and effective inASP.NET MVC.
Is there any reason to use a redirect in Update (and otheractions) instead of simply rendering the view? Using a redirect separates theaction of updating the model from the action of updating the view. They are twodistinct actions performed by the browser. The pattern PRG explains thisapproach. PRG stands for Post/Redirect/Get. In this way, any page refresh isdone via GET, which makes for a cleaner MVC solution. However, it also savesyou from a nasty problem if the user refreshes (F5) the page displayed after,say, an update, no POST is repeated and no message about resending data is displayed.If you use the PRG pattern and redirect to a GET, any F5 refresh repeats a GET,not a POST.
Let s see more about updating the user interface toreflect the state of the record during the update phase.
A Bit of jQuery in the User Control
It s fine to have the helper user control trigger theupdate on the controller and redirect to a selected view. This is what onewould call a clean MVC solution. What about synchronizing the rest of the userinterface (mostly the page) with the effect of updates? Let me explain with anexample. Suppose you have a master/detail view. You edit the detail recordthrough the user control, but still you may need to update the master record topossibly reflect changes. The user control can hardly take care of that. Inthis case, the best you can do is instruct the user control to fire an event tothe host page. This the most general approach.
However, in simpler but not necessarily infrequent situations,you don t need to sync up the rest of the page with the actions performedthrough the user control. Any feedback regarding the update in this case can bedisplayed within the user control itself. In Figure 2, in fact, you see amessage bar area in the user control that displays the content set by thecontroller. Here s how the controller stores data to display in the view:
// TempData is a dictionary on the controller base classTempData["OutputMessage"] = "Successfully updated!";
Why use TempData instead of ViewData? Any data you stuffin ViewData is stored for the duration of the request. But if you redirect torender the view, any such content is lost. TempData ensures that any data youplace in it also survives a second request, if this is a redirect.
How would you display a message (be it an error or anotification) after an update? If the update didn t work because of errors, youwant to keep the message on during editing for the user to keep in mind whatwent wrong. If the message, instead, is a plain notification that the updateworked successfully, you might want to show it, but hide it at some pointbecause during an editing phase it doesn t make much sense to recall that the lastupdate worked just fine.
In other words, what you want are flashy messages thatappear for just a few seconds if a text is found in TempData and disappearshortly afterwards. You need client-side capabilities for this. Figure 3 showshow to do it with jQuery. Figure 4 shows the real effect on the user interface.The message Successfully updated! disappears in a couple of seconds, leavingthe editing interface clean for a new set of changes.
<%string msg = TempData["OutputMessage"] as string;%>
Figure 3: A flashymessage using jQuery.
Figure 4: The effect on the userinterface when using jQuery.
You put the code in Figure 3 in the ASCX file of the usercontrol. The message is displayed when the page renders out. At the same time,when the document is ready, a timer is set up that clears the message at theend of the interval.
Features for Validation
You can use the TempData container to pass up to the userinterface any error message you want that relates to the update. The usercontrol receives an entity object from the model (in the sample code it s aCustomer object) and attempts to fit into that object any data the user enteredthrough input fields. But what if the input data is invalid?
Clearly, any invalid data should originate an errormessage that must be reported to the user. The best way to validate the stateof an object in an MVC context is a non-trivial problem with many aspects, anddeserves an article of its own. For the purpose of this article, I ll simplyassume you know how to validate an object and capture the list of invalid inputvalues and related error messages. How would you display these messages? In WebForms you have server-side validation controls that make it easy. A validationcontrol simply applies a built-in validation rule to a value, and has no visionon the object that may be behind that scalar value.
ASP.NET MVC provides some facilities to display errormessages associated with input data. You have a ValidationMessage helper togenerate a label and a ModelState collection to capture all error messages todisplay. Let s start with the markup in the user control:
Country<%= Html.TextBox("Country", ViewData.Model.Country,new Dictionary() { { "class","textBox" } })%><%= Html.ValidationMessage("Country") %>
ValidationMessage produces no output if the ModelStatecollection on the ViewData object is empty. You add an entry to the collectionfor each detected error. The controller s UpdateModel method uses the contentin the Form collection to update an entity object:
UpdateModel(customer);
Next, you validate the object applying some rules. Foreach error detected, you add an entry to the ModelState:
if (!cust.Country.Equals("Germany")){ViewData.ModelState.AddModelError("Country","Invalid country.");TempData["OutputMessage"] = "Check your input data.";}else{context.SubmitChanges();TempData["OutputMessage"] = "Successfully updated!";}
The preceding code is excerpted from a controller method.However, this is a debatable point that deserves more attention.
In ASP.NET MVC, the whole point is to fill the ModelStatecollection. Once this is done, matching ValidationMessage elements will displaymessages. The match is found on the key passed to ModelState and the argumentpassed to ValidationMessage.
If the page contains a CSS style named.input_validation_error, that is automatically applied to the control withwrong data, provided that the control is not styled already (see Figure 5).
Figure 5: An example MVC application.
Fix for User Controls
The approach described works great for HTML elementsincorporated in the Web page. If the input elements live within a user controland the user control has a strongly typed view data model no informationabout model state ever reaches the user control. As a result, no error messageis displayed. At least in Beta 1, you need to pass the content of ModelState toTempData so the user control can retrieve it:
if (!cust.Country.Equals("Germany")){ViewData.ModelState.AddModelError("Country","Invalid country.");TempData["OutputMessage"] = "Check your input data.";TempData["ModelState"] = ViewData.ModelState;}else { ... }
The user control then copies the content of the passedmodel state into its own model state:
<%if (TempData["ModelStateForUserControl"] != null){foreach (var x in (ModelStateDictionary)TempData["ModelStateForUserControl"]){if (x.Value.Errors.Count <= 0)continue;string errorMsg = x.Value.Errors[0].ErrorMessage;ViewData.ModelState.AddModelError(x.Key, errorMsg);}}%>
With this fix, it works also when the user interface isisolated in a user control.
Source code accompanying this article is available for download.
About the Author
You May Also Like