Sunday, May 02, 2010

Consumption of Data in MVC2 Views

The following data sources are implicitly available as properties of a View:

  • ViewData
  • ViewData.ModelState
  • ViewData.Model (or more simply just Model via a shortcut property)
  • TempData

TempData is a special case that has been previously covered. The other three all relate to ViewData, but are substantially different from each other. Understanding these differences is important; otherwise it can lead to unexpected behavior when reading and writing ViewData and Model values.

First, let’s address what each type of ViewData refers to:

ViewData Property

The ViewData property is a dictionary object. It is usually used for passing values from the Action to the View that are not rendered as input elements to be posted back to the server. Its usage is straightforward:

Within the Action:

ViewData["SectionTitle"] = "Product Details";

Within the View:

<h2><%: ViewData["SectionTitle"] %></h2>

ViewData.ModelState

The ModelState property is a dictionary object that tracks HTTP values submitted to the server. In addition to storing the name and value of each field, it also tracks associated validation errors. Although its name may suggest otherwise, ModelState isn’t Model-aware. It doesn’t understand what a "Product" is. It simply contains a collection of items with names such as "ProductName" and "UnitPrice". It is the responsibility of other objects—ModelBinders, ViewResult, and the strongly-typed View—to map and interpret ModelState values as Model properties.

ModelState isn't intended to be used directly by the developer, but once in a while you may need to override a model state value within an Action method. The syntax to do so is:

ModelState.SetModelValue("ProductName", new ValueProviderResult("Your new value", "", CultureInfo.InvariantCulture));

Or, to clear the ModelState value:

ModelState.SetModelValue("ProductName", null);

If your application calls SetModelValue more than once or twice, then consider creating an extension method for SetModelValue:

public static class ModelStateDictionaryExtensions {
    public static void SetModelValue(this ModelStateDictionary modelState, string key, object rawValue) {
        modelState.SetModelValue(key, new ValueProviderResult(rawValue, String.Empty, CultureInfo.InvariantCulture));
    }
}

Then you can simply write:

ModelState.SetModelValue("ProductName", "Your new value");

Model (or ViewData.Model)

The Model property is assigned when an object is passed to the View by specifying an object in the parameter for the returned ViewResult at the end of the Action:

return View(product);

Assignment of the Model property is most commonly used in GET Action methods to initially populate form data. Within a POST Action method, it more common to see the simpler form:

return View();

When called without a Model object, Model property is null, so it can’t be referenced by the View. Let's consider for a moment why this syntax makes sense in POST Actions. To illustrate the point, take a look at the following implementation of a basic Action POST method that implements the Post/Redirect/Get (PRG) design pattern.

AcceptVerbs("POST")]
public ActionResult Edit(int id, FormCollection collection) {
    var product = GetProduct(id);
    if (TryUpdateModel(product)) {
        SaveProduct(product);
        return RedirectToAction("Index");
    }
    return View();
}

There are two possible outcomes to this method. Either there are no errors, in which case the RedirectToAction method returns an ActionResult that causes a REDIRECT to occur, which then GETs the Index Action. Or, there are errors, in which case the same View is reloaded. If it is reloaded, we usually want to repopulate the View form with the same values that were posted by the user. This is taken care automatically by the HTML Helper methods using ModelState. So, in either case, there is no benefit gained by passing a new Model object to the View.

Furthermore, if you do pass a Model object to the View, then you may be tempted to write code such as the following, only to be surprised that it doesn't work:

AcceptVerbs("POST")]
public ActionResult Edit(int id, FormCollection collection) {
    var product = GetProduct(id);
    // ...
    product.ProductName = "This has no effect";
    return View(product);
}

The reason it doesn't work is because ModelState information is given precedence over Model information by HTML Helpers. More on this next.

Html Helpers

Html Helpers are accessed from the Html property of the View object. Each helper comes in a weakly-typed flavor that accepts the key as a string value, and a strongly-typed flavor that specifies the property name via a lambda expression:

<% using (Html.BeginForm()) { %>
    <p>
        <%= Html.Label("ProductName") %>
        <%= Html.TextBox("ProductName") %>
    </p>
    <p>
        <%= Html.LabelFor(x => x.ProductName) %>
        <%= Html.TextBoxFor(x => x.ProductName) %>
    </p>
< } >

To use the strongly-typed Helpers, declare the View to inherit from the applicable generic—ViewPage or ViewUserControl:

<%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage<Product>" % >

You will notice that Html Helpers do not specify whether the value to be displayed should come from ViewData, ModelState, or Model. Helpers do include overrides to explicitly pass the value, but in most cases what you want is for the Helpers to automatically figure out the right value to use, which is exactly what they do. In order of precedence, Html Helpers attempt lookup of a key in this order:

  1. ViewData.ModelState dictionary
  2. Model property (if a strongly-typed View)
  3. ViewData dictionary

Armed with this knowledge, it is probably clear to you know why assignment to ViewData or the Model has no effect in Views generated from Post Actions.

Without Html Helpers

Html Helpers provide a convenient way to apply the Don't Repeat Yourself (DRY) programming principle. In addition, Html Helpers support data annotation and validations, which further increases their value. But let’s look at how to consume data within a View without the Helpers.

One approach is implemented without making any changes to our existing Action implementation. Within the View, each input element needs a condition to determine what value to render:

<input id="ProductName" name="ProductName" type="text" value="<%= ViewData.ModelState["ProductName"] != null ? ViewData.ModelState["ProductName"].Value.AttemptedValue : Model.ProductName %>" />

Perhaps there's a simpler way to represent this that I'm not aware of. If not, a custom helper methods, such as HtmlHelper.Value/ValueOf could be written to abstract the conditional logic while maintaining full control over the markup. By the way. if wanting full control over the markup is putting you using Html Helpers, then consider the Display/DisplayFor and Editor/EditorFor helpers, which support rendering by custom templates.

Other approaches require changes to the Controller Action method. In one case, you would choose to always supply the Model in the return View statement of the Action. In this case, the View would look like this:

<input id="ProductName" name="ProductName" type="text" value="<%= Model.ProductName %>" />

The other method is to use the ViewData dictionary directly. This is the only non-Helper alternative that doesn't require a strongly-typed view. In this case, the Action will end with:

ViewData["Product"] = product;
return View();

And the View contains:

<input id="ProductName" name="ProductName" type="text" value="<%= ViewData["ProductName"] %>" />

Summary

Html Helpers simplify the markup required to implement form elements, plus they incorporate support for data annotations and validations. To use properly though, it is helpful to understand their internal logic for selecting the which of the ViewData data sources are used in POST and GET requests.

3 comments:

Anonymous said...

I’ve recently started a blog, the information you provide on this site has helped me tremendously. Thank you for all of your time & work.

Anonymous said...

Thanks you for providing the details in concrete yet effective way.
Rajesh

Anonymous said...

simply stopping by to say hi