Practical .NET
Leveraging Custom ASP.NET MVC Templates to Support Real-World Views
In the real world, you'll often need to display constant and repeating data, a.k.a. Master/Detail pages. Custom templates are the cleanest, simplest way for you to manage them.
When I look at the sample code people show for ASP.NET MVC applications I see relatively simple-minded scenarios: Typically, just a Web page that displays one Entity Framework entity object (for example, a Customer object). In my experience, at least, the real world is a lot more complicated. Typical pages in a real-world application are driven by workflow and, as a result, they pull together data from multiple business entities. In addition, many pages are variations on master-detail pages with a combination of single and multiple records. In the real world, those examples won't do.
When I do see examples that handle repeating data, they often do so by adding for…each logic to the View. That's sometimes necessary, but it violates the point of the Model-View-Controller (MVC) design pattern. One of the goals of the MVC pattern is to have all your logic in your Controller class, which supports automated testing. Any logic (such as for…each loops) in your View requires human testing. The goal is to have a View so brain-dead simple that all of your errors will be caught by the compiler (though you can still have what I call "blunders": misspellings, for example). Custom templates let you eliminate that for…each logic in your Views.
Getting Data to the View
In the real world the customer class you might want returned from a customer page would combine multiple classes, as this example mingles information from a single customer with information about multiple customer addresses:
Public Class CustomerInfo
Public Property FirstName() As String
Public Property LastName() As String
Public Property CustomerAddresses() As List(of Address)
...many more properties
End Class
The Address class referenced in that Customer class might look something like this:
Public Class Address
Public Property AddressType() As String
Public Property City() As String
Public Property Street() As String
...many more properties
End Class
And, in fact, a typical page might go much further and include many other entities. A page of sales order information would include the sales order header and the list of items purchased on the sales order (which would include product information like the item name), along with the customer and customer address information.
The first step in delivering a real-world View is to define a data transfer object (DTO) with the data that your View needs. A DTO for my sales order example might look like this:
Public Class SalesOrderDTO
Public Property SalesOrder() As SalesOrder
Public Property Customer() As CustomerInfo
Public Property SalesOrderDetails() As List(of SalesOrderDetail)
End Class
You'll notice I'm using a complete SalesOrder class in this DTO. The SalesOrder entity class returned from Entity Framework probably includes more information than any particular View requires. However, rather than tailor a DTO to a specific View, I recommend having your DTO hold all the data from each entity the View needs (provided that doesn't turn into hundreds of properties or BLOB columns). This will simplify the code required to populate your DTO with data, and it will make it more likely that your DTO will be useful in other Views. This practice will also, probably, save you some maintenance time down the road because one of the most common maintenance requests is to add a related column to the View. If you're already delivering that data to the View, you may just need to add the HTML with the appropriate binding syntax to your View to put the data in front of the user.
With the DTO for your View defined, somewhere in your Model classes (or, perhaps, even in your View's Controller), you'll instantiate your DTO, retrieve the individual entities that make it up, and move selected data from the entities' properties into your DTO's properties (AutoMapper will make this last step easier).
However, the real work begins when you want to get that SalesOrderDTO information onto the screen. Because my SalesOrder example is really just more of the same problem that my earlier Customer-with-Addresses example demonstrated, I'll use that earlier example for the rest of this column. For this column, I'll use a View that supports updating data, but all of these techniques apply equally well in a View that just displays data.
Exploiting Templates
To generate my View, I need an action method like this one:
<HttpGet>
Public Function DisplayCustomer(custId As String) As ActionResult
Dim cust As CustomerInfo
...code to generate and populate the CustomerInfo object...
Return View(cust)
End Function
But the real issue is how I get the data back from the browser for processing. What you want is to have all of the data that comes back from the browser handed to your Action method as a single object. For example, to process a page that allows updates to my CustomerInfo class, I'd want to write an Action method like this one:
<HttpPost>
Public Function Update(cust As CustomerInfo) As ActionResult
...code to use the CustomerInfo object...
End Function
To have the data displayed in the form transferred into the CustomerInfo parameter in my Action method, I need to have the name attribute of each HTML element that displays CustomerInfo information set to the name of a property on my CustomerInfo object. The simplest way to generate that HTML is to use the HtmlHelp class's EditorFor method (or DisplayFor for display-only data). This code will almost do the job:
@Html.EditorFor(Function(c) c.FirstName) @Html.EditorFor(Function(c) c.LastName)
@Html.EditorFor(Function(c) c.CustomerAddresses)
Handling Master-Detail Data
This code will display the "master" data (the Customer information) without a problem. Where this code will fall down is in displaying the "detail" data: The CustomerAddresses collection that consists of a List of Address objects. While ASP.NET MVC knows how to display a string (like FirstName and LastName), it has no idea how to display a List of Address objects. Fortunately, you can fix that by providing a custom template for displaying Address objects.
Before creating your own template for Address objects, you need to create the folder where ASP.NET MVC will look for your custom template. In your Views/Shared folder add a new folder called EditorTemplates (for a display-only template that works with DisplayFor, add a folder called DisplayTemplates). If you're working with Areas and you expect your new template to be used only in one Area, put your folder in the Views/Shared folder for that Area; if you'd like to use your new template in multiple Areas, add your folder to Views/Shared folder in your project's root folder.
To create your template, right-click on the EidtorTemplates folder and add a Partial View with the same name as the class (Address, for my example). In that Partial View, bind the View's Model to the class name (Address, again) and write whatever HTML and code you want to display the properties in your class. For my CustomerAddresses collection of Address objects, my template looks like this:
@ModelType CustomerInformation.Address
<h2>Main Address</h2>
<p>
@Html.LabelFor(Function(ca) ca.AddressType)
<br/>
@Html.LabelFor(Function(ca) ca.Street): @Html.EditorFor(Function(ca) ca.Street)
<br/>
@Html.LabelFor(Function(ca) ca.City): @Html.EditorFor(Function(ca) ca.City)
<br />
</p>
My HtmlEditorFor for my CustomerAddresses collection will now find my Address template and use it to display each of the Address objects in the collection.
I like custom templates because they simplify my Views by removing logic and giving me a better match with the MVC pattern (custom templates also support the single responsibility principle: One View = One DTO component). There is, of course, a downside: You end up with a lot more Views, primarily in your Shared folder (I do wish that Visual Studio would let me right-click on an EditorFor and take me to the custom template that will be used when the View is displayed). But even with all those Views building up in my Shared folder, I prefer the benefits I get from custom templates.
However, I also recognize that, in the real world, you'll need more flexibility than I offer here, so I'll discuss that in my next column. But, even then, I'll still be exploiting custom templates.
About the Author
Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.