Input Validation in ASP.NET MVC

Explore Model State and Data Validation

InData Entry and ASP.NET MVC I discussed how to arrange a data entry page using the ASP.NET MVC Framework . There s a common sentiment around the community that the ASP.NET MVC Framework is not particularly suited to data entry. I was one of those sustaining this point of view. Well, it lasted until I tried it out seriously. Now I ll probably remain a fan of Web Forms for most of the foreseeable future, but this is not a good reason to yield to prejudice when it comes to ASP.NET MVC and data entry. Data entry (and input validation as we ll see in this article) can be equally as effective in ASP.NET MVC as it is in Web Forms. This said, feel free to pick up the framework with which you feel most comfortable. But let s make it clear it is a matter of preference.

Figure 1 shows where I left off last month. The figure shows a Web page through which a user can select a customer from a list and click to edit its record. When this happens, a new view is displayed that includes a user control. The user control incorporates the user interface to edit the content of the bound record. The user control also contains a Save button and posts a new request to the site to update the record. Keeping the editing stuff isolated in a user control makes it easy for you to replace it and decouples to an acceptable level the host page from the edit-record engine.

Figure 1: A sample data entry page.
Figure 1: A sample data entry page.

In this article, I ll further enhance the user control to add the ability to display user messages that regard validation and other errors. In doing so, I ll also be using the jQuery library to add some animation.

A Common Pattern for a Data Entry Page

Since the advent of ASP.NET 1.0, I ve been working on quite a few Web pages to do data entry. When it comes to this, you have essentially two points to address: isolating the editing UI and update logic from the page, and letting the page know about what happens during the editing phase.

By employing a user control for editing the record, you are able to isolate the editing UI from the page. Furthermore, in an ASP.NET MVC scenario, the logic to handle the update is already out of the page, isolated in 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 must figure out how the user control communicates with the host page. This regards how the input data is being passed to the user control, but also how messages for errors and notifications are shown.

Figure 2 shows structure and overall behavior of a sample page using this pattern. When the user clicks to edit, a POST is made to the Web server to turn the selected record into edit mode.

Figure 2: Internal architecture of an ASP.NET MVC data entry page.
Figure 2: Internal architecture of an 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 in the list. The action method simply redirects to the Edit action for a GET verb the same action you expect to take place when an input/edit/id URL is requested. This ensures a dual programming interface to the page the user gets to 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 the user interface. It is recommended that you opt for a custom data structure that abstracts away the data required by the next view. However, a similar fragment of code still works:

object data = GetContentForTheUserControl(...)

ViewData["CustomerData"] = data;

The view will check the presence of data for the user control 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. The user control renders its own view, drawing data from passed objects, and waits for the user to enter data. Let s skip for just a moment over validation and focus on the Save button. When the user clicks the Save button, a POST request is generated to a URL input/update/id. The controller s Update action does the following:

public ActionResult Update(string id)

{

// Update the data model (if current data is OK)

UpdateRecord(id);

// Save a message to UI

SetMessageBar();

// Redirect to Edit mode on the current record

RedirectToAction("Edit", new RouteValueDictionary(new

{ id = id }));

}

The first step accomplished by the controller is updating the record provided that valid data has been provided through the user interface. The controller s method receives this data from the ASP.NET MVC machinery. In turn, the ASP.NET MVC machinery gets raw data from the Request.Form collection packed with the POST request (see last month s article for details).

Finally, the controller sets any information for the user interface and redirects to Edit mode. This is the pattern I found very comfortable to use in a data entry page, and especially easy and effective in ASP.NET MVC.

Is there any reason to use a redirect in Update (and other actions) instead of simply rendering the view? Using a redirect separates the action of updating the model from the action of updating the view. They are two distinct actions performed by the browser. The pattern PRG explains this approach. PRG stands for Post/Redirect/Get. In this way, any page refresh is done via GET, which makes for a cleaner MVC solution. However, it also saves you 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 to reflect 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 the update on the controller and redirect to a selected view. This is what one would call a clean MVC solution. What about synchronizing the rest of the user interface (mostly the page) with the effect of updates? Let me explain with an example. Suppose you have a master/detail view. You edit the detail record through the user control, but still you may need to update the master record to possibly reflect changes. The user control can hardly take care of that. In this case, the best you can do is instruct the user control to fire an event to the 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 performed through the user control. Any feedback regarding the update in this case can be displayed within the user control itself. In Figure 2, in fact, you see a message bar area in the user control that displays the content set by the controller. Here s how the controller stores data to display in the view:

// TempData is a dictionary on the controller base class

TempData["OutputMessage"] = "Successfully updated!";

Why use TempData instead of ViewData? Any data you stuff in ViewData is stored for the duration of the request. But if you redirect to render the view, any such content is lost. TempData ensures that any data you place in it also survives a second request, if this is a redirect.

How would you display a message (be it an error or a notification) after an update? If the update didn t work because of errors, you want to keep the message on during editing for the user to keep in mind what went wrong. If the message, instead, is a plain notification that the update worked successfully, you might want to show it, but hide it at some point because during an editing phase it doesn t make much sense to recall that the last update worked just fine.

In other words, what you want are flashy messages that appear for just a few seconds if a text is found in TempData and disappear shortly afterwards. You need client-side capabilities for this. Figure 3 shows how 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, leaving the editing interface clean for a new set of changes.

<%

string msg = TempData["OutputMessage"] as string;

%>



Figure 3: A flashy message using jQuery.

Figure 4: The effect on the user interface when using jQuery.
Figure 4: The effect on the user interface when using jQuery.

You put the code in Figure 3 in the ASCX file of the user control. 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 the end of the interval.

Features for Validation

You can use the TempData container to pass up to the user interface any error message you want that relates to the update. The user control receives an entity object from the model (in the sample code it s a Customer object) and attempts to fit into that object any data the user entered through input fields. But what if the input data is invalid?

Clearly, any invalid data should originate an error message that must be reported to the user. The best way to validate the state of an object in an MVC context is a non-trivial problem with many aspects, and deserves an article of its own. For the purpose of this article, I ll simply assume you know how to validate an object and capture the list of invalid input values and related error messages. How would you display these messages? In Web Forms you have server-side validation controls that make it easy. A validation control simply applies a built-in validation rule to a value, and has no vision on the object that may be behind that scalar value.

ASP.NET MVC provides some facilities to display error messages associated with input data. You have a ValidationMessage helper to generate a label and a ModelState collection to capture all error messages to display. 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 ModelState collection on the ViewData object is empty. You add an entry to the collection for each detected error. The controller s UpdateModel method uses the content in the Form collection to update an entity object:

UpdateModel(customer);

Next, you validate the object applying some rules. For each 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 ModelState collection. Once this is done, matching ValidationMessage elements will display messages. The match is found on the key passed to ModelState and the argument passed to ValidationMessage.

If the page contains a CSS style named .input_validation_error, that is automatically applied to the control with wrong data, provided that the control is not styled already (see Figure 5).

Figure 5: An example MVC application.
Figure 5: An example MVC application.

Fix for User Controls

The approach described works great for HTML elements incorporated in the Web page. If the input elements live within a user control and the user control has a strongly typed view data model no information about model state ever reaches the user control. As a result, no error message is displayed. At least in Beta 1, you need to pass the content of ModelState to TempData 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 passed model 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 is isolated in a user control.

Source code accompanying this article is available for download.

Hide comments

Comments

  • Allowed HTML tags: <em> <strong> <blockquote> <br> <p>

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.
Publish