ASP.NET MVC Framework (Part 3): Passing ViewData from Controllers to Views
The last few weeks I have been working on a series of blog posts that cover the new ASP.NET MVC Framework we are working on. The ASP.NET MVC Framework is an optional approach you can use to structure your ASP.NET web applications to have a clear separation of concerns, and make it easier to unit test your code and support a TDD workflow.
The first post in this series built a simple e-commerce product listing/browsing site. It covered the high-level concepts behind MVC, and demonstrated how to create a new ASP.NET MVC project from scratch to implement and test this e-commerce product listing functionality. The second post in this series drilled deep into the URL routing architecture of the ASP.NET MVC framework, and discussed both how it worked as well as how you can handle more advanced URL routing scenarios with it.
In today's blog post I'm going to discuss how Controllers interact with Views, and specifically cover ways you can pass data from a Controller to a View in order to render a response back to a client.
Quick Recap from Part 1
In Part 1 of this series, we created an e-commerce site that implemented basic product listing/browsing support. We implemented this site using the ASP.NET MVC Framework, which led us to naturally structure the code into distinct controller, model and view components.
When a browser sends a HTTP request to our web site, the ASP.NET MVC Framework will use its URL routing engine to map the incoming request to an action method on a controller class to process it. Controllers in a MVC based application are responsible for processing incoming requests, handling user input and interactions, and executing application logic based on them (retrieving and updating model data stored in a database, etc).
When it comes time to render an HTML response back to the client, controllers typically work with "view" components - which are implemented as separate classes/templates from the controllers, and are intended to be focused entirely on encapsulating presentation logic.
Views should not contain any application logic or database retrieval code, instead all application/data logic should only be handled by the controller class. The motivation behind this partitioning is to help enforce a clear separation of your application/data logic from your UI generation code. This makes it easier to unit test your application/data logic in isolation from your UI rendering logic.
Views should only render their output using the view-specific data passed to it by the Controller class. In the ASP.NET MVC Framework we call this view-specific data "ViewData". The rest of this blog post is going to cover some of the different approaches you can use to pass this "ViewData" from the Controller to the View to render.
A Simple Product Listing Scenario
To help illustrate some of the techniques we can use to pass ViewData from a Controller to a View, let's build a simple product listing page:
We will use a CategoryID integer to filter the products that we want to display for the page. Notice above how we are embedding the CategoryID as part of the URL (for example: /Products/Category/2 or /Products/Category/4).
Our product listing page is then rendering two separate dynamic content elements. The first is the textual name of the category we are displaying (for example: "Condiments"). The second is an HTML <ul><li/></ul> list of product names. I've circled both of these in red in the above screen-shot.
Below we'll look at two different approaches we can use to implement a "ProductsController" class that processes the incoming request, retrieves the data necessary to handle it, and then passes this data to a "List" view to render it. The first approach we'll examine passes the data using a late-bound dictionary object. The second approach we'll examine passes it using a strongly-typed class.
Approach 1: Passing ViewData using the Controller.ViewData Dictionary
The Controller base class has a "ViewData" dictionary property that can be used to populate data that you want to pass to a View. You add objects into the ViewData dictionary using a key/value pattern.
Below is a ProductsController class with a "Category" action method that implements our product listing scenario above. Notice how it is using the category's ID parameter to lookup the textual name of the category, as well as retrieve a list of the Products within that category. It is storing both of these in the Controller.ViewData collection using a "CategoryName" and "Products" key:
Our Category action above is then calling RenderView("List") to indicate which view template it wants to render. When you call RenderView like this it will pass the ViewData dictionary to the View in order for it to render.
Implementing Our View
We will implement our List view using a List.aspx file that lives under the \Views\Products directory of our project. This List.aspx page will inherit the layout of the Site.Master MasterPage under the \Views\Shared folder (right click within VS 2008 and select Add New Item->MVC View Content Page to wire up a master page when you create a new view page):
When we create our List.aspx page using the MVC View Content Page template it derives not from the usual System.Web.UI.Page class, but rather from the System.Web.Mvc.ViewPage base class (which is a subclass of the existing Page class):
The ViewPage base class provides us with a ViewData dictionary property that we can use within the view page to access the data objects that were added by the Controller. We can then take these data objects and use them to render HTML output using either server controls, or by using <%= %> rendering code.
Implementing Our View Using Server Controls
Below is an example of how we could use the existing <asp:literal> and <asp:repeater> server controls to implement our HTML UI:
We can bind the ViewData to these controls using the below code-behind class (note how we are using the ViewPage's ViewData dictionary to-do this):
Note: Because we have no <form runat="server"> on the page, no view-state is ever emitted. The above controls also don't automatically render any ID value - which means that you have total control over the HTML emitted.
Implementing our View using <%= %> Code
If you prefer to use inline rendering code to generate the output, you can accomplish the same result as above using the List.aspx below:
Note: Because ViewData is typed as a dictionary containing "objects", we need to cast ViewData["Products"] to a List<Product> or an IEnumerable<Product> in order to use the foreach statement on it. I am importing both the System.Collections.Generic and MyStore.Models namespaces on the page to avoid having to fully qualify the List<T> and Product types.
Note: The use of the "var" keyword above is an example of using the new C# and VB "type inference" feature in VS 2008 (read here for my previous post on this). Because we have cast ViewData["Products"] as a List<Product> we get full intellisense on the product variable within the List.aspx file:
Approach 2: Passing ViewData using Strongly Typed Classes
In addition to supporting a late-bound dictionary approach, the ASP.NET MVC Framework also enables you to pass strongly typed ViewData objects from your Controller to your View. There are a couple of benefits of using this strongly typed approach:
- You avoid using strings to lookup objects, and get compile-time checking of both your Controller and View code
- You avoid the need to explicitly cast values from the ViewData object dictionary when using strongly-typed languages like C#
- You get automatic code intellisense against your ViewData object within both the markup and code-behind of your view page
- You can use code refactoring tools to help automate changes across your app and unit-test code base
Below is a strongly typed "ProductsListViewData" class that encapsulates the data needed for the List.aspx view to render our product listing. It has CategoryName and Products properties (implemented using the new C# automatic properties support):
We can then update our ProductsController implementation to use this object to pass a strongly typed ViewData object to our view:
Notice above how we are passing our strongly typed ProductsListViewData object to the View by adding it as an extra parameter to the RenderView() method.
Using the View's ViewData Dictionary with a Strongly Typed ViewData Object
The previous List.aspx view implementations we wrote will continue to work with our updated ProductsController - no code changes required. This is because when a strongly typed ViewData object is passed to a View that derives from ViewPage, the ViewData dictionary will automatically use reflection against the properties of the strongly typed object to lookup values. So code in our view like below:
will automatically use reflection to retrieve the value from the CategoryName property of the strongly typed ProductsListViewData object we passed when calling the RenderView method.
Using the ViewPage<T> Base Class to Strongly Type ViewData
In addition to supporting a dictionary based ViewPage base class, the ASP.NET MVC Framework also ships with a generics based ViewPage<T> implementation. If your View derives from ViewPage<T> - where T indicates the type of the ViewData class the Controller passes to the view - then the ViewData property will be strongly typed using this class type.
For example, we could update our List.aspx.cs code-behind class to derive not from ViewPage, but from ViewPage<ProductsListViewData>:
When we do this, the ViewData property on the page will change from being a dictionary to being of type ProductsListViewData. This means that instead of using string-based dictionary lookups to retrieve data, we can now use strongly typed properties:
We can then use either a sever-control approach, or a <%= %> rendering approach to render HTML based on this ViewData.
Implementing Our ViewPage<T> Implementation Using Server Controls
Below is an example of how we could use the <asp:literal> and <asp:repeater> server controls to implement our HTML UI. This is the exact same markup that we used when our List.aspx page derived from ViewPage:
Below is what the code-behind now looks like. Note how because we are deriving from ViewPage<ProductsListViewData> we can access the properties directly - and we don't need to cast anything (we'll also get refactoring tool support anytime we decide to rename one of the properties):
Implementing our ViewPage<T> implementation using <%= %> Code
If you prefer to use inline rendering code to generate the output, you can accomplish the same result as above using the List.aspx below:
Using the ViewPage<T> approach we now no longer need to use string lookups of the ViewData. Even more importantly, notice above how we no longer need to cast any of the properties - since they are strongly typed. This means we can write foreach (var product in ViewData.Products) and not have to cast Products. We also get full intellisense on the product variable within the loop:
Summary
Hopefully the above post helps provide some more details about how Controllers pass data to a View in order to render a response to a client. You can use either a late-bound dictionary, or a strongly-typed approach to accomplish this.
The first time you try and build an MVC application you will likely find the concept of splitting up and separating your application controller logic from your UI generation code a little strange. It will probably take some dedicated app-building time before you become comfortable and orient your mind-set around the idea of processing a request, executing all of the application logic for it, packaging up the viewdata required to build a UI response, and then handing it off to a separate view page to render it. Important: If this model doesn't feel comfortable to you then don't use it - the MVC approach is purely optional, and we don't think it is something everyone will want to use.
The benefit and goal behind this application partitioning, though, is that it enables you to run and test your application and data logic in isolation from your UI rendering code. This makes it much easier to develop comprehensive unit tests for your application, as well as to use a TDD (test driven development) workflow as you build your applications. In later blog posts I'll drill in deeper into this more, as well as discuss best practices you can use to easily test your code.
Hope this helps,
Scott