Wednesday, February 24, 2010

Maintainable MVC Series: View hierarchy

This article is part of the Maintainable MVC Series.


The view system of ASP.NET MVC knows the followings types of views:



  • Master views

  • Page views

  • Partial views


The regular page view contains the specific html and presentation of data for each rendered page. For html shared by multiple pages - like navigation, header and footer - every page has a master page.


Both of these types can include partial views for small parts, that can be reused from multiple views. For example page navigation for display of results covering multiple pages, or widgets. Even if they're not reused it is useful to separate parts of your page view or master view to partial views, to keep the (master) view itself comprehensible and maintainable.


For using partial views in the master view we have a special way to provide them with data (which is sometimes specific to the page requested, like which menu item is active). This is explained in the future part Poor Man's RenderAction.



Master view



In the following diagram you can see the master page and it's partial views. A page view is shown in more detail later on.





The code for the Site.Master looks something like this:


[html]
<%@ Master Language="C#" Inherits="System.Web.Mvc.ViewMasterPage" %>
<%@ Import Namespace="ClientX.Website.Models"%>

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Site title - <asp:ContentPlaceHolder ID="TitleContent" runat="server" /></title>
<% #if DEBUG %>
<% Html.RenderPartial("CssDebug", ViewData.Eval("DataForCss")); %>
<% #else %>
<% Html.RenderPartial("CssRelease", ViewData.Eval("DataForCss")); %>
<% #endif %>
</head>
<body>

<% Html.RenderPartial("Header", ViewData.Eval("DataForHeader")); %>

<% Html.RenderPartial("PostBackForm", ViewData.Eval("DataForPostBackForm")); %>

<% Html.RenderPartial("Messages", ViewData.Eval("DataForMessages")); %>

<div class="content">

<asp:ContentPlaceHolder ID="MainContent" runat="server" />

</div><!-- /content -->

<% Html.RenderPartial("Footer", ViewData.Eval("DataForFooter")); %>

<% #if DEBUG %>
<% Html.RenderPartial("JavascriptDebug", ViewData.Eval("DataForJavascript")); %>
<% #else %>
<% Html.RenderPartial("JavascriptRelease", ViewData.Eval("DataForJavascript")); %>
<% #endif %>

</body>
</html>
[/html]

In the code above you can identify the inclusion of multiple partial views and the placeholders where the page view descending from the master view can show their html. This quite straightforward.


More specific to our master is the way we select the javascript and css partial views. The DEBUG switches make sure the right css and javascript is included on the production server. For development you'll want to use your original javascript files, while the production servers will serve minified and merged javascript files.


The data supplied to the to the partial views makes it look like these partial views aren't strongly type, however they certainly are. Perhaps it's possible to provide the master view itself with a strongly typed model, however the effort is probably not worth it, because production issues rarely arise within the master view.


If your site uses multiple different layouts (1, 2 or 3 columns for example) it is advisable to have nested master pages. The top one still keeps the css, javascript and navigation, where the nested master pages will only contain the html that differs between the different layouts.


Page view






[html]
<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<OverviewViewModel>" %>
<%@ Import Namespace="ClientX.Website.Models"%>

<asp:Content ID="TitleContentPlaceHolder" ContentPlaceHolderID="TitleContent" runat="server">
Page title
</asp:Content>

<asp:Content ID="MainContentPlaceHolder" ContentPlaceHolderID="MainContent" runat="server">

<% Html.RenderPartial("Heading", Model.Heading); %>

<div>
<% Html.RenderPartial("OverviewSearchForm", Model.OverviewSearchForm); %>

<% Html.RenderPartial("Pager", Model.Pager); %>

<table>
<% Html.RenderPartial("TableHeader", Model.TableHeader); %>
<%foreach (TableRowViewModel item in Model.ItemList){
Html.RenderPartial("TableRow", item);
} %>
</table>

<% Html.RenderPartial("Pager", Model.Pager); %>
</div>
</asp:Content>
[/html]

The page view is a little bit different from the master view. For one, it actually contains the content for the placeholders present in the master view. The other difference with the master view is that the included partial views are called strongly-typed.


Also notice, in the first line, that the view is strongly typed. All of our page views and partial views are strongly typed, no exceptions made.


Furthermore, our rule is never (and I mean never) to share a view model between multiple views. Each view has it's own tailor made view model.


The view model only has properties which are actually used in the view. Also, it is totally flat (only simple properties) and quite dumb. You wouldn't want to put too much logic in the view model, because it discourages reuse of the logic in question.


To fill the view models from the domain model we use AutoMapper by Jimmy Bogard where possible and for the rest set it by hand in our presentation mapper classes. More on that in future part ViewModel and FormModel.


The only view models that aren't completely flattened are the ones containing form data. The view model itself contains data that is just for view purposes, like select lists and descriptions for radio button options, etc. The form data itself is contained in a form model, which is accessible through the view model. Something like the code below:


[csharp highlight="3"]
public class SearchFormViewModel {

public SearchFormModel FormData { get; set; }

public SelectList Countries { get; set; }

public SelectList OtherDropDownData { get; set; }

}
[/csharp]

Okay, enough for now. More on view models and form models in the next part.

No comments:

Post a Comment