Yan-Feng

记录经历、收藏经典、分享经验

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

Overview

A user's first impression of your application starts with the View. While your Models may well be elegantly designed, and your Controllers may be well factored and streamlined, none of that is visible to the end user. Their entire interaction with your application starts with the View. The View is effectively your application's ambassador to the user — representing your application to the user and providing the basis on which the application is first judged.

Obviously, if the rest of your application is buggy, then no amount of spit and polish on the View will make up for the application's deficiencies. However, build an ugly and hard-to-use View, and many users will not give your application the time of day to prove just how feature rich and bug-free it may well be.

In this chapter, the authors won't show you how to make a pretty View, as our own aesthetic skills may be called into question. We will demonstrate how Views work in ASP.NET MVC and what their responsibilities are, and provide you with the tools to build Views your application will be proud to wear.

 

What a View Does

The View is responsible for providing the user interface (UI) to the user. It is given a reference to the Model, and it transforms that Model into a format ready to be presented to the user. In ASP.NET MVC, this consists of examining the ViewDataDictionary handed off to it by the Controller (accessed via the ViewData property) and transforming that to HTML.

In the strongly typed View case, which is covered in more depth later, the ViewDataDictionary has a strongly typed Model object that the View renders. This Model might represent the actual domain object, such as a Product instance, or it might be a presentation Model object specific to the View, such as a ProductEditViewData instance.

Let's take a quick look at an example of a View. The following code sample shows the Index View within the default ASP.NET MVC project template reformatted to fit the format of this book:

<%@ Page Language=" C#" MasterPageFile="~/Views/Shared/Site.Master"  Inherits=" System.Web.Mvc.ViewPage" %><asp:Content ID=" indexTitle" ContentPlaceHolderID=" TitleContent" runat=" server">    Home Page</asp:Content><asp:Content ID=" indexContent" ContentPlaceHolderID=" MainContent" runat=" server">    <h2><%= Html.Encode(ViewData["Message"]) %></h2>    <p>        To learn more about ASP.NET MVC visit <a href=" http://asp.net/mvc"  title=" ASP.NET MVC Website">http://asp.net/mvc</a>.    </p></asp:Content>

This is an extremely simple example of a View, but it's useful for pointing out some of the key details of Views in ASP.NET MVC. One of the first things you'll notice is that on the surface, it looks just like a Web Form. ASP.NET MVC allows you to swap out different View engines, but the default View engine is a WebFormViewEngine.

Technically, this is not a Web Form because it doesn't include the <form runat="server"> tag; it's really just an ASP.NET Page. Views in ASP.NET MVC derive from a common base class, System.Web.Mvc.ViewPage, which itself derives from System.Web.UI.Page. Strongly typed Views derive from the generic ViewPage<T>.

If you really wanted to, you could add the <form runat=" server"> tag to the View and start dropping server controls into the View. In general, the authors recommended against this, as many server controls violate the MVC pattern because they encapsulate the Controller behavior and View in a single control — not to mention that MVC doesn't support View State and PostBacks. However, there are some "View-only" server controls that require the server form tag. Using these would be entirely appropriate.

Important 

Product Team Aside

While it's true that the ASP.NET MVC Feature Team warns against using traditional ASP.NET server controls in the View, the CodePlex team, one of the first internal teams to use ASP.NET MVC, has found a couple of cases where they were successfully able to merge the two worlds. For example, there are certain Web Form controls they still use that require PostBack. It is possible to add such a control to a View and then include a conditional check within a Controller action to check for a web control PostBack. In the case that it is a PostBack, the action method exits immediately, because there is nothing for the Controller to do in response to the request; instead it lets the server control ultimately handle the request. However, when interacting with another portion of a View that doesn't use an ASP.NET server control, the Controller action would handle the request normally.

This was a situation in which the CodePlex team needed to reuse an existing Web Form control and didn't have time to immediately rewrite it in an MVC fashion. Again, this is not a supported scenario and not necessarily recommended by the Feature Team, but you know your application's needs best and can consider such techniques at your own risk.

Another thing to notice is that this View is able to take advantage of Master Pages, one of the benefits of building on Views on top of the existing ASP.NET Page infrastructure.

 
 

What a View Shouldn't Do

The astute reader may have noticed the following line of code in the above View:

<%= Html.Encode(ViewData["Message"]) %>

The use of code nuggets within the View, represented by <% %> blocks, is a departure from the declarative approach that many Web Form developers are used to. With MVC, all the real work is done within the Model and Controllers, so by the time the View becomes involved, the only thing it really needs to do is output the result in the form of HTML markup.

To many, seeing the "angle percent" markup on a page is a visual nightmare, taking them back to last century and ASP classic and ultimately drawing comparisons to "spaghetti code," wherein the business logic and View logic is all intertwined in one saucy noodle mess.

This comparison ignores the fact that it's possible to write spaghetti code with Web Forms. In fact, no language is completely immune from spaghetti code. The difference with ASP.NET MVC, compared to classic ASP, is that the structure of the project encourages proper separation of concerns. When you put business logic in the Model and application logic in the Controllers, the View is much less prone to spaghetti code. Also, ASP.NET MVC provides a library of helper methods used to output properly encoded HTML, which provides a lot of help in creating Views.

Important 

Product Team Aside

The product team has publicly stated that they will add declarative "View-only" controls, which are essentially wrappers of the existing helpers. The primary reason for going with the Html helper methods first, and doing declarative helpers later, is that the team tries to follow a code-first, configuration/markup-second approach. It's much easier to a write unit test for code than it is to write one for XML configuration and HTML markup.

By following this approach, the team ensures that all the functionality it supplies is easily testable and reusable.

In following with the principle of Separation of Concerns, Views should not contain application and business logic. In fact, they should contain as little code as possible. While it's perfectly acceptable for a View to contain View logic, Views are generally the most difficult part of the application to test in an automated fashion, and they therefore benefit from having very little code.

 
 

Specifying a View

So far, you've seen what a View does and what it doesn't do, this chapter hasn't covered how to specify the View that should render the output for a specific action. It turns out that this is very easy when you follow the conventions implicit in the framework.

When you create a new project template, you'll notice that the project contains a "Views" directory structured in a very specific manner (see Figure 6-1).

Image from book
Figure 6-1

By convention, the Views directory contains a folder per Controller, with the same name as the Controller, sans the "Controller" suffix. Within each Controller folder, there's a View file for each action method, named the same as the action method. This provides the basis for how Views are associated to an action method.

For example, as discussed in Chapter 5, an action method can return a ViewResult via the View method like so:

public class HomeController : Controller{  public ActionResult Index()  {    ViewData["Title"] = "Home Page";    ViewData["Message"] = "Welcome to ASP.NET MVC!";    return View();  }}

This method ought to look familiar; it's the Index action method of HomeController in the default project template. Because the View name was not specified, the ViewResult returned by this method looks for a View named the same as the action name in the /Views/ControllerName directory. The View selected in this case would be /Views/Home/Index.aspx.

As with most things in ASP.NET MVC, this convention can be overridden. Suppose that you want the Index action to render a different View. You could supply a different View name like so:

public ActionResult Index(){  ViewData["Title"] = "Home Page";  ViewData["Message"] = "Welcome to ASP.NET MVC!";  return View("NotIndex");}

In this case, it will still look in the /Views/Home directory, but choose NotIndex.aspx as the View. In some situations, you might even want to specify a View in a completely different directory structure. You can use the tilde syntax to provide the full path to the View like so:

public ActionResult Index(){  ViewData["Title"] = "Home Page";  ViewData["Message"] = "Welcome to ASP.NET MVC!";  return View("~/Some/Other/View.aspx");}

When using the tilde syntax, you must supply the file extension of the View because this bypasses the View engine's internal lookup mechanism for finding Views.

 
 

Strongly Typed Views

Suppose that you have a list of Product instances you wish to display in a View. One means of doing this is to simply add the products to the View Data Dictionary and iterate them over the View.

For example, the code in your Controller action might look like this:

public ActionResult List(){  var products = new List<Product>();  for(int i = 0; i < 10; i++)  {    products.Add(new Product {ProductName = "Product "+ i});  }  ViewData["Products"] = products;  return View();}

In your View, you can then iterate and display the products like so:

<ul><% foreach(Product p in (ViewData["Products"] as IEnumerable<Product>)) {%>  <li><%= Html.Encode(p.ProductName) %></li><% } %></ul>

Because the ViewData indexer returns an object, it was necessary to cast ViewData["Products"] to an IEnumerable<Product> before enumerating it. It would be cleaner if you could provide the View with the type for the Model being sent in. This is where strongly typed Views come in.

In the Controller method, you can specify the Model via an overload of the View method whereby you pass in the Model.

public ActionResult List(){  var products = new List<Product>();  for(int i = 0; i < 10; i++)  {    products.Add(new Product {ProductName = "Product "+ i});  }  return View(products);}

Behind the scenes, this sets the value of the ViewData.Model property to the value passed into the View method. The next step is to change the type of the View inherit from ViewPage<T>. In early preview versions of ASP.NET MVC, Views had a code-behind file and you would change the View's derived type like this:

public partial class Index : ViewPage{}

becomes

public partial class Index : ViewPage<IEnumerable<Product>>{}

However the View really has no business having a code-behind file in the MVC Model. In fact, by default, there are no code-behind files for Views in ASP.NET MVC. If you want strongly typed Views, just add the type to derive from in the @Page directive like this:

<%@ Page Language=" C#" MasterPageFile="~/Views/Shared/Site.Master"Inherits=" System.Web.Mvc.ViewPage<IEnumerable<Product>>" %>

This is the preferred way to have strongly typed Views. Now within the markup for the View, you can access the strongly typed ViewData.Model property, with full Intellisense support.

<ul><% foreach(Product p in Model) {%>  <li><%= Html.Encode(p.ProductName) %></li><% } %></ul>
 

HTML Helper Methods

One of the traits of the ASP.NET MVC framework often touted is that it puts you in full control of your application, including the HTML markup. Many announce this as a benefit of the framework. After all, full control is good, right? But it's really a characteristic of the framework that's only good or bad depending on the circumstance.

There are times when you don't want to be in control over the markup. You'd rather drop a control and have it figure out the markup because you don't care how it looks. Other times, you want to have absolute control over the markup. Being in control is great, but it also means more responsibility. You are now responsible for outputting markup that would have otherwise been handled by a server control in the Web Forms world.

HTML helpers provide a middle ground. These are methods included with the framework that help with rendering markup for very common cases. In most cases, they handle common mistakes such as forgetting to encode attribute values, etc

HtmlHelper Class and Extension Methods

The ViewPage class has an HtmlHelper property named Html. When you look at the methods of HtmlHelper, you'll notice it's pretty sparse. This property is really an anchor point for attaching extension methods. When you import the System.Web.Mvc.Html namespace (imported by default in the default template), the Html property suddenly lights up with a bunch of helper methods.

In the screenshot that follows, the extension methods are denoted by the blue down arrow (gray in the dialog in Figure 6-2).

Image from book
Figure 6-2

One benefit of this approach is that if you don't like the helper methods included with the framework, you can remove this namespace and attach your own HTML helper extension methods. Likewise, it provides a convenience conventional place to add your own helper methods by simply writing extension methods of the HtmlHelper class.

Using the HTML Helpers

This section covers the HTML helper methods that ship with ASP.NET MVC. There are a few common patterns that all helpers share, which are worth calling out now:

  • All helpers attribute encode attribute values.

  • All helpers HTML encode values they display, such as link text.

  • Helpers that accept a RouteValueDictionary have a corresponding overload that allows you to specify an anonymous object as the dictionary, as covered in Chapter 4, which discussed anonymous object initializers.

  • Likewise, helpers that accept an IDictionary<string, object> used to specify HTML attributes, have a corresponding overload that allows you to specify an anonymous object as the dictionary.

  • Helpers used to render form fields will automatically look up their current value in the ModelState dictionary. The name argument to the helper is used as the key to the dictionary.

  • If the ModelState contains an error, the form helper associated with that error will render a CSS class of "input-validation-error" in addition to any explicitly specified CSS classes. The default stylesheet, style.css, included in the project template contains styling for this class.

Note 

If you're writing a third-party library (as opposed to a web application) that uses the HTML helpers, never call the overloads that accept anonymous objects. Always call the overloads that accept dictionaries. Otherwise, web application authors that consume your library are likely to experience SecurityExceptions.

Html.ActionLink and Html.RouteLink

The ActionLink method renders a hyperlink (anchor tag) to another Controller action. This method uses the Routing API under the hood to generate the URL. For example, when linking to another action within the same Controller used to render the current View, you can simply specify the action name like so:

<%= Html.ActionLink("Link Text", "AnotherAction") %>

This produces the following markup, assuming the default routes:

<a href="/Home/About">LinkText</a>

You can specify a Controller name as the second argument to link to an action of another Controller. For example, to link to the AccountController.Withdraw action, use:

<%= Html.ActionLink("Link Text", "Withdraw", "Account") %>

Notice that you specify the Controller name without the "Controller" suffix. You never specify the Controller's type name.

In many cases, you have more route parameters than you need to supply in order to choose the correct URL to render for the action link. In that case, you can specify a RouteValueDictionary for the routeValues argument. The following example uses the overload that accepts the anonymous object overload for route values to specify the ID:

<%= Html.ActionLink("Link Text", "Withdraw", "Account", new {id=34231}, null) %>

To specify HTML attributes, provide a value for the htmlAttributes argument. This accepts an IDictionary<string, object>, but for ease of use, you can call the overload that accepts an anonymous object, like so:

<%= Html.ActionLink("LinkText", "About", "Account", null,new {title=" withdraw from account"}) %>

The ActionLink methods have specific knowledge about ASP.NET MVC Controllers and actions. Hence these methods provide convenient overloads that allow the specifying of a controllerName and actionName directly.

The RouteLink methods follow the same pattern as the ActionLink methods but also accept a route name and do not have arguments for Controller name and action name. For example, the first example ActionLink shown previously is equivalent to the following:

<%= Html.RouteLink("Link Text", new {action=" AnotherAction"}) %>

Html.BeginForm

The BeginForm method follows a different pattern from the other helper methods. An HTML form generally starts with a <form> tag, contains a bunch of markup in the middle, and then ends with a closing tag.

It wouldn't be very useful to have to specify the markup that goes in between the form start and end tags to the form helper method. Instead, the method uses the disposing pattern to denote the scope of the form. While this may seem like an inappropriate use of the disposing pattern, it turns out that the IDisposable interface was designed for defining scopes and not just for disposing of managed resources.

An example should clarify what the authors mean:

<% using(Html.BeginForm()) { %> <! -- <form ...> tag here // -- >  <label for=" firstName">  <input type=" text" name=" FirstName" id=" firstName" />  <input type=" submit" value=" Save" /><% } %> <! -- End </form> tag here // -- >

In this code sample, you enclose the BeginForm method in a using block. This method returns an instance of MvcForm that implements the IDisposable interface. At the end of the using block, the Dispose method is called, which renders the closing form tag. This provides a convenient approach for implementing HTML blocks in which the closing tag needs to be rendered at the end of a block of arbitrary HTML.

It's an unusual use of a using block, for sure, but being completely biased, the authors find it quite elegant. For those who find it completely distasteful, you can also use the following approach, which provides a bit of symmetry:

<% Html.BeginForm(); %>  <label for=" firstName">  <input type=" text" name=" FirstName" id=" firstName" />  <input type=" submit" value=" Save" /><% Html.EndForm(); %>

Future helper methods that follow this pattern should be named according to the Begin* pattern.

Html.Encode

Encode is not an extension method but a proper method of HtmlHelper. It's really a shorthand way of calling the HttpUtility.HtmlEncode method, but with the benefit that it contains a convenience overload that accepts an object, unlike HtmlEncode. When an object is supplied, it is converted to a string by using CultureInfo.CurrentCulture, before being encoded.

Html.Hidden

This method is used to render a hidden input. For example, the following code:

<%= Html.Hidden("wizardStep", "1") %>

results in:

<input id=" wizardStep" name=" wizardStep" type=" hidden" value="1" />

Html.DropDownList and Html.ListBox

Both the DropDownList and ListBox helpers render a <select /> HTML element. The difference is that ListBox is used for multiple item selection. It sets the multiple attribute to true in the rendered markup, allowing for multiple selections.

Typically, a select element serves two purposes:

  • To show a list of possible options

  • To show the current value for a field

For example, if you have a Product class with a CategoryID property, you may use a select element to display the value of the CategoryID property of Product, as well as to show all possible categories.

Thus, there's a bit of setup work to do in the Controller when using these helpers in order to provide these two bits of information. Here's an example of the code in the Controller that prepares an enumeration of SelectListItem instances. SelectListItem serves as the presentation Model for the DropDownList and ListBox methods:

[AcceptVerbs(HttpVerbs.Get)]public ActionResult Edit(int id) {  NorthwindDataContext context = new NorthwindDataContext();  var product = context.Products.Single(p => p.ProductID == id);  ViewData["CategoryId"] = from c in p.Categories    select new SelectListItem {      Text = c.CategoryName,      Value = c.CategoryID,      Selected = (c.CategoryID == p.CategoryID)    };  return View(product);}

Now in the View, you can render the dropdown list simply by referring to the IEnumerable<ListItem> that you put in the View data:

<%= Html.DropDownList("CategoryID") %>

There are other overloads that allow you to supply the enumeration directly. The SelectList class and the MultiSelectList class are helper classes used to transform an enumeration of any type into an IEnumerable<SelectListItem>. In the preceding example, a LINQ projection was used instead of these helper classes to transform a enumeration of Category instances into an enumeration of SelectListItem instances.

While a bit more advanced than using the helper classes, using a LINQ projection lets you avoid using reflection to generate the enumeration.

Html.Password

This is a simple helper used to render a password field. It's much like the TextBox helper, except that it does not retain the posted value and it uses a password mask:

<%= Html.Password("my-password") %>

and results in:

<input id=" my-password" name=" my-password" type=" password" value=""/>

Html.RadioButton

Radio buttons are generally grouped together to provide a range of possible options for a single value. For example, if you wanted the user to select a color from a specific list of colors, you might use multiple radio buttons to present the choices. To group the radio buttons, you give each button the same name. Only the selected radio button is posted back to the server when the form is submitted.

This helper renders a simple radio button:

<%= Html.RadioButton("color", "red") %><%= Html.RadioButton("color", "blue", true) %><%= Html.RadioButton("color", "green") %>

and results in:

<input id=" color" name=" color" type=" radio" value=" red" /><input checked=" checked" id=" color" name=" color" type=" radio" value=" blue" /><input id=" color" name=" color" type=" radio" value=" green" />

Html.RenderPartial

RenderPartial is used to render a partial View, as opposed to a full View. Typically, this is used to render out a snippet of markup that is meant to be a reusable piece of the View. In the default View engine, a partial View is implemented as a UserControl (aka an .ascx file, although it is possible to use a full .aspx page as a partial View), but other View engines may use other means for implementing a partial View.

There are four overloads for RenderPartial:

public void RenderPartial(string partialViewName);public void RenderPartial(string partialViewName, object model);public void RenderPartial(string partialViewName, ViewDataDictionary viewData);public void RenderPartial(string partialViewName, object model,  ViewDataDictionary viewData);

A partial View may be rendered using a different View engine from your main View. This is useful in cases where you might be reusing some View component code, which assumes a different View engine than the one you're using.

Here's an example of calling a partial View:

<% Html.RenderPartial("MyUserControl"); %>
Note 

Note that RenderPartial writes directly to the Response output stream; it does not return a string. Hence, you use <% rather than <%=, and you need the semicolon at the end for C#.

The lookup for the partial View follows the same logic as normal View lookup.

Html.TextArea

The aptly named TextArea helper renders a <textarea> tag, which is commonly used for entering multi-line data in a form. It properly encodes the value of the text area and attributes. For example:

<%= Html.TextArea("text", "hello <br/> world") %>

results in:

<textarea cols="20" id=" text" name=" text" rows="2">hello &lt;br /&gt; world</textarea>

There are overloads that allow you to explicitly specify the columns and rows; for example:

<%= Html.TextArea("text", "hello <br /> world", 10, 80, null) %>

results in:

<textarea cols="80" id=" text" name=" text" rows="10">hello &lt;br /&gt; world</textarea>

Html.TextBox

The TextBox helper renders an input tag with the type attribute set to text. This is one of the form helpers most commonly used to accept free-form input from a user; for example:

<%= Html.TextBox("name") %>

results in:

<input id=" name" name=" name" type=" text" value=""/>

Like most form helpers, it looks in the ModelState and ViewData to obtain its current value. For example, here's one way to set the value of a Product's name in a form:

public ActionResult Edit(int id) {  var product = new Product {Name = "ASP.NET MVC"}  ViewData["Name"] = product.Name;  return View();}

and in the View you can use a textbox to display that value by giving the TextBox helper the same name as the value in the ViewData:

<%= Html.TextBox("Name") %>

which results in:

<input id=" Name" name=" Name" type=" text" value=" ASP.NET MVC" />

A ViewData lookup can also look up properties of objects in the View Data. For example, going back to the previous example, let's change it to add the whole Product to ViewData:

public ActionResult Edit(int id) {  var product = new Product {Name = "ASP.NET MVC"};  ViewData["Product"] = product;  return View();}

You can use the following code to display a textbox with the product's name as the current value:

<%= Html.TextBox("Product.Name") %>

which results in:

<input id=" Product_Name" name=" Product.Name" type=" text" value=" ASP.NET MVC" />

If there are now values matching "Product.Name" in the View data, then it attempts to look up a value for the portion of the name before the first dot, "Product," in which case it finds a Product instance. It then evaluates the remaining portion of the name "Name" against the product it found, yielding the value.

One other thing to note is that the id of the attribute replaced the dot in the name with an underscore. This behavior is controllable via the static HtmlHelper.IdAttributeDotReplacement property. The reason that this replacement occurs is to make the default markup for these helpers more easily usable with JavaScript libraries such as jQuery.

The TextBox helper also works well against strongly typed View data. For example, given the following Controller action:

public ActionResult Edit(int id) {  var product = new Product {Name = "ASP.NET MVC"}  return View(product);}

you can supply the TextBox helper with the name of a property in order to display that property:

<%= Html.TextBox("Name") %>

which results in:

<input id=" Name" name=" Name" type=" text" value=" ASP.NET MVC" />

The TextBox helper allows you to supply an explicit value and avoid View data lookup if you wish. For example:

<%= Html.TextBox("Name", "ASP.NET MVC") %>

produces the same markup as the previous example.

If you wish to specify HTML attributes, you'll need to call an overload that also supplies an explicit value. If you want to use View data lookup in these cases, just pass in null to the TextBox helper. For example:

<%= Html.TextBox("Name", null, new {@class=" lotsofit"}) %>

will result in:

<input class="lotsofit " id=" Name" name=" Name" type=" text" value=" ASP.NET MVC" />

Html.ValidationMessage

When there is an error for a particular field in the ModelState dictionary, you can use the ValidationMessage helper to display that message.

For example, in the following Controller action, we purposefully add an error to the Model state:

public ActionResult Index(){    var modelState = new ModelState();    modelState.Errors.Add("Ouch");    ModelState["Name"] = modelState;    return View();}

Now in the View, you can display the error message for the "Name" field like so:

<%= Html.ValidationMessage("Name") %>

which results in:

<span class="field-validation-error ">Ouch</span>

This message is only shown if there is an error in the Model state for the key "Name." You can also call an override that allows you to override the error message from within the View:

<%= Html.ValidationMessage("Name", "Something is wrong with your name") %>

which results in:

<span class="field-validation-error ">Something wrong with your name </span>
Note 

Note that by convention, this helper renders the CSS class "field-validation-error" along with any specific CSS classes you provide. The default template includes some styling to display these items in red, which you can change in style.css.

Html.ValidationSummary

This displays an unordered list of all validation errors in the ModelState dictionary. This summary can be styled using CSS. As an example, update the Controller action you used in the previous section to include another Model state error:

public ActionResult Index(){    var modelState = new ModelState();    modelState.Errors.Add("Ouch");    ModelState["Name"] = modelState;    var modelState2 = new ModelState();    modelState2.Errors.Add("Ooh!");    ModelState["Age"] = modelState;    return View();}

A call to:

<%= Html.ValidationSummary() %>

results in:

<ul class="validation-summary-errors ">  <li>Ouch</li>  <li>Ouch</li></ul>

This method also allows the specifying of a generic header message, which is displayed only if there is an error. For example:

<%= Html.ValidationSummary("An error occurred") %>

results in:

<div class="validation-summary-errors ">  <span>An error occurred</span>  <ul>    <li>Ouch</li>    <li>Ouch</li>  </ul></div>
Note 

Note that, by convention, this helper renders the CSS class "validation-summary-errors" along with any specific CSS classes you provide. The default template includes some styling to display these items in red, which you can change in style.css. See Chapter 9 for more information.

 

The View Engine

ScottHa likes to call the View engine "just an angle bracket generator." At the most general level, a View engine will take a representation of a View in any format you like and turn it into whatever other format you like. Usually, this means that you will create an ASPX file containing markup and script, and ASP.NET MVC's default View engine implementation, the WebFormViewEngine, will use some existing ASP.NET APIs to render your page as HTML.

View Engines aren't limited to using ASPX pages, nor are they limited to rendering HTML. You'll see later how you can create alternate View engines that render output that isn't HTML, as well as unusual View engines that take a custom DSL (Domain Specific Language) as input.

To better understand what a View engine is, let's review the ASP.NET MVC lifecycle (very simplified in Figure 6-3).

Image from book
Figure 6-3

There are a lot more subsystems involved than Figure 6-3 shows; this figure just highlights where the View engine comes into play — which is right after the Controller action is executed and returns a ViewResult in response to a request.

It is very important to note here that the Controller itself does not render the View; it simply prepares the data (aka the Model) and decides which View to display by returning a ViewResult instance. As you saw earlier in this chapter, the Controller base class contains a simple convenience method, named View, used to return a ViewResult. Under the hood, the ViewResult calls into the current View engine to actually render the View.

Configuring a View Engine

As just mentioned, it's possible to have alternative View engines configured for an application. View engines are configured in Global.asax.cs. By default, there is no need to configure other View engines if you stick with just using WebFormViewEngine. However, if you want to replace this View engine with another, you could use the following code in you Application_Start method:

protected void Application_Start(){  ViewEngines.Engines.Clear();  ViewEngines.Engines.Add(new MyViewEngine());  RegisterRoutes(RouteTable.Routes);}

Engines is a static ViewEngineCollection used to contain all registered View engines. This is the entry point for registering View engines. You needed to call the Clear method first because WebFormViewEngine is included in that collection by default. Calling the Clear method is not necessary if you wish to add your custom View engine as another option in addition to the default one, rather than replace the default one. It's fine to have multiple View engines registered.

Selecting a View Engine

If you can have multiple View engines registered at a time, how does ASP.NET MVC know which one to use when rendering a View? Let's dig into this a bit.

It was mentioned earlier that WebFormViewEngine was the default View engine. While it's convenient to think of it in this way, this isn't exactly correct. In truth, the static ViewEngines class has a DefaultEngine property, which is always set to AutoViewEngine. This class is given a reference to the ViewEngines .Engines collection when instantiated.

Thus, when a ViewResult is ready to render a View, it looks at its own ViewEngine property, which by default is the AutoViewEngine. The AutoViewEngine, in turn, iterates through each of the registered View engines, asking each one if it can render the specified View. The first one that can render the specified View gets to render it, much as in route matching.

It's possible to change the ViewEngine property of ViewResult to some View engine other than AutoViewEngine, but doing so means that you will forgo the View engine selection process. This allows you to override View engine selection in rare cases where you may need to do so.

Finding a View

The IViewEngine interface is the key interface to implement when building a custom View engine:

public interface IViewEngine{  ViewEngineResult FindPartialView(ControllerContext controllerContext,    string partialViewName);  ViewEngineResult FindView(ControllerContext controllerContext, string viewName,    string masterName);  void ReleaseView(ControllerContext controllerContext, IView view);}

With the AutoViewEngine, the implementation of FindView simply iterates through the registered View engines and calls FindView on each one, passing in the specified View name. This is the means by which the AutoViewEngine can "ask" each View engine if it can render a particular View.

The FindView method returns an instance of ViewEngineResult, which encapsulates the "answer" to the question of whether the View engine can render the View (see table below).

Open table as spreadsheet

Property

Description

View

Returns the found IView instance for the specified View name. If the View could not be located, then it returns null.

ViewEngine

Returns an IViewEngine instance if a View was found; otherwise null.

SearchedLocations

Returns an IEnumerable<string> that contains all the locations that the View engine searched.

If the IView returned is null, then the View engine was not able to locate a View corresponding to the View name. Even when a View engine cannot locate a View, it will return the list of locations it checked. These are typically file paths for View engines that use a template file, but they could be something else entirely, such as database locations for View engines that store Views in the database.

Note that the FindPartialView method works in exactly the same way as FindView, except that it focuses on finding a partial View. It is quite common for View engines to intrinsically treat Views and partial Views differently. For example, some View engines automatically attach a master View (or layout) to the current View by convention. So it's important for that View engine to know whether it's being asked for a full View or a partial View. Otherwise, every partial View might have the master layout surrounding it.

The View Itself

The IView interface is the second interface one needs to implement when implementing a custom View engine. Fortunately, it is quite simple, containing a single method.

public interface IView{    // Methods    void Render(ViewContext viewContext, TextWriter writer);}

Custom Views are supplied with a ViewContext instance, which provides the information that might be needed by a custom View engine, along with a TextWriter instance. The View will then call methods of the TextWriter instance to render the output.

The ViewContext contains the following properties, accessible by the View.

Open table as spreadsheet

Property

Description

HttpContext

An instance of HttpContextBase, which provides access to the ASP.NET intrinsic objects such as Server, Session, Request, Response, and the like

Controller

An instance of ControllerBase, which provides access to the Controller making the call to the View engine

RouteData

An instance of RouteData, which provides access to the route values for the current request

ViewData

An instance of ViewDataDictionary containing the data passed from the Controller to the View

TempData

An instance of TempDataDictionary containing data passed to the View by the Controller in a special one-request-only cache

View

An instance of IView, which is the View being rendered

Not every View needs access to all these properties to render a View, but it's good to know they are there when needed.

Alternative View Engines

When working with ASP.NET MVC for the first time, you're likely to use the View engine that comes preloaded with ASP.NET MVC: the WebFormViewEngine. There are many advantages to this:

  • Familiarity with Web Forms

  • Using Master Pages

  • Support for scripting with C#/VB

  • Use of System.Web.UI.Page

  • IntelliSense support in Visual Studio

Don't be fooled by the "Web Form" part of WebFormViewEngine. It's not really a Web Form in that, by default, there is no <form runat=" server"> tag in ViewPage. There are also no PostBacks or View State, as mentioned many times before.

Important 

Product Team Aside

As we tend to reiterate in this chapter, WebFormViewEngine is a bit of a misnomer. We had intense debates on what to name this default View engine. We considered DefaultViewEngine, but as you saw earlier, it's not really the default; AutoViewEngine is. Also the name is not very descriptive.

We considered PageViewEngine, or even AspxViewEngine, but then what about partial Views, which are implemented as user controls? Phil tried to put forth TemplateControlViewEngine because the one thing that Page, UserControl, and MasterPage all share in common is that they all derive from TemplateControl. That was rejected because that name was obscure, as the connection to TemplateControl is not readily apparent or well known.

In the end, we stuck with WebFormViewEngine because we never really came up with a better name and we deemed it good enough. Rob's sure that, upon reading this aside, someone will come up with the perfect name and we will end up regretting the name we chose.

The ViewPage class, which serves as the default View, does invoke the Page lifecycle when rendering, but the various points of the lifecycle end up being no-ops. It's possible to handle the various events in the page lifecycle, but this approach is generally frowned upon. Ideally, developers should not treat the ViewPage as if it were a Web Form page.

Stated succinctly, ASP.NET MVC includes the WebFormViewEngine for designer support and familiarity, but ASP.NET MVC uses it for angle bracket generation, nothing more.

There are times, however, when you might want to use a different View engine, for example, when you:

  • Desire to use a different language (like Ruby or Python)

  • Need more concise HTML coding with better standards support

  • Render non-HTML output such as graphics, PDFs, RSS, and the like

There are a number of different View engines available at the time of this writing — the next sections take a look at some of the really interesting ones. Currently, there are others, and there will be even more — we predict — in the future, but each of these has something unique to offer.

NHaml

One of the more popular View engines is NHaml, created by Andrew Peters and released on his blog in December 2007. The project is a port of the popular Rails Haml View engine and is described on their blog as follows:

Note 

Haml is a markup language that's used to cleanly and simply describe the XHTML of any web document, without the use of inline code. Haml functions as a replacement for inline page templating systems such as PHP, ERB, and ASP. However, Haml avoids the need for explicitly coding XHTML into the template, because it is actually an abstract description of the XHTML, with some code to generate dynamic content.

The goal of NHaml is to reduce the verbosity of XHTML, doing away with the excess of angle brackets required for most HTML pages. Some have called it "markup haiku."

The core syntax of NHaml is not unfamiliar and uses many of the same concepts that you'd see on a Web Forms ViewPage. The one main difference, however, is that whitespace (lines, spaces, tabs) counts in how things are marked up.

In this example, taken from the Andrew's blog, you can contrast a View written with Web Forms-style markup with one ported to the NHaml View Engine:

Northwind Products List Using WebForms View

<%@ Page Language=" C#" MasterPageFile="~/Views/Shared/Site.Master"      AutoEventWireup=" true"      CodeBehind=" List.aspx"      Inherits=" MvcApplication5.Views.Products.List" Title=" Products" %><asp:Content ContentPlaceHolderID=" MainContentPlaceHolder" runat=" server">  <h2><%= ViewData.CategoryName %></h2>  <ul>    <% foreach (var product in ViewData.Products) { %>      <li>        <%= product.ProductName %>        <div class="editlink ">          (<%= Html.ActionLink("Edit",             new { Action=" Edit", ID=product.ProductID })%>)        </div>      </li>    <% } %>  </ul>  <%= Html.ActionLink("Add New Product", new { Action=" New" }) %></asp:Content>

Northwind Products List Using NHaml View

%h2= ViewData.CategoryName%ul  - foreach (var product in ViewData.Products)    %li      = product.ProductName      .editlink        = Html.ActionLink("Edit", new { Action=" Edit", ID=product.ProductID })= Html.ActionLink("Add New Product", new { Action=" New" })

You can see from this example that NHaml does away with angle brackets quite nicely, and also uses whitespace to help in the formatting of the output.

NHaml also allows you to implement some core features of Ruby on Rails, namely the use of Partials (akin to UserControls) and Layouts (akin to MasterPages).

The use of Partials in NHaml highlights the project's commitment to simplicity and less typing. To use a Partial in NHaml, you first need to create a bit of NHaml markup and save it in an .haml file. Their convention is to use an underscore as a prefix: "_Product.haml" to denote that the file is a Partial. To use this Partial, you just reference the name of the Partial without the extension, and it will be rendered inline (the Partial must be in the same physical directory as the file referencing it):

- foreach (var product in ViewData.Products)  %li    _ Product

Layouts work in much the same way as Master Pages do with ASP.NET Web Forms, but with a few less features. With NHaml, the use of Layouts relies heavily on convention, in that a particular layout must be located in the Views/Shared folder and must use the same name as the Controller (you can override this behavior by passing in the MasterName argument to the View method from the Controller).

To create a Layout in NHaml, you create some NHaml markup and save it in the Views/Shared/[ControllerName] folder. The ProductController is used for the following example:

!!!%html{xmlns=" http://www.w3.org/1999/xhtml"}  %head    %title Rob's NHamlized MVC Application    %link{href="http://www.cnblogs.com/Content/Sites", rel=" stylesheet", type=" text/css"}  %body    #inner      #header        %h1 Welcome To My Store    #maincontent      _    #footer

This is a pretty simple page, and the part that matters, once again, is the underscore. All of the Views for the ProductController (unless the Action tells it to use a different layout) will be injected into this Layout where the underscore is.

You can also use application-wide Layouts with NHaml by creating an Application.haml file in the Views/Shared directory.

NVelocity

NVelocity is an Open Source templating engine and a port of the Apache/Jakarta Velocity project, built for Java-based applications. The NVelocity project did quite well for a few years, until 2004, when check-ins stopped and the project slowed down.

The Castle Project (the guys responsible for MonoRail, an alternative to ASP.NET Web Forms) forked the NVelocity project at that time and added some much needed bug fixes and changes to an alternative templating engine. NVelocity remains an interpreted language, but it performs quite well regardless.

MonoRail's fork of NVelocity includes a number of really clever improvements to the original. The most striking is their "Fancy foreach loop" inspired by Joel Spolsky's CityDesk language. This loop is almost reason enough to take a good, hard look at using NVelocity. Here's a sample from the NVelocity documentation:

#foreach($person in $people)#beforeall       <table>              <tr><th>Name</th><th>Age</th></tr>#before       <tr#odd       Style='color:gray'>#even       Style='color:white'>#each       <td>$person.Name</td><td>$person.Age</td>#after       </tr>#between       <tr><td colspan='2'>$person.bio</td></tr>#afterall       </table>#nodata       Sorry No Person Found#end

The addition of all the directives such as #before, #between, and many others allow you to make quick work of even the most complex table markup.

Brail

Brail is an interesting View engine because it uses the Boo Language for its syntax. Boo is an object-oriented statically type language for the CLR, but it has a Python language style to it, has significant whitespace, and includes some interesting ideas. It's not IronPython, to be clear; it's a whole new language. It promotes a syntax that tries to avoid explicit type-casting, and includes syntactic sugar for patterns like string formatting and regular expressions intended to be "wrist-friendly" to the developer.

Brail was originally created as a View engine for the MonoRail project that has been ported over to ASP.NET MVC's interface. Brail support for ASP.NET MVC currently lives in the MVCContrib project at www.mvccontrib.org. Brail's Views are compiled, which makes them potentially faster than interpreted Views like those in NVelocity.

Where ASP.NET Web Forms has Master Pages, Brail has Layouts. A primary layout in Brail might look like this:

<html> <head>        <title>My site</title> </head> <body>        <h1>My sample site</h1>        <div>               ${childOutput}        </div> </body></html>

Note that the ${childOutput} indicates where the rendered View's output should appear within the larger layout. A Hello World might look like this:

<% output "Hello, powerful person! The time is ${DateTime.Now}"if user.IsAdministrator %>

Of course, you can do for loops, call methods, and all the things you'd expect. However, note the Boo-ism where there's an if control statement at the end of the line. Boo's fluent easy-to-read syntax was one of the aspects that got the MonoRail team excited about Brail, and now it's an option for ASP.NET MVC.

Spark

Spark is the brainchild of Louis DeJardin (http://whereslou.com/tag/spark) and is being actively developed with support for both MonoRail and ASP.NET MVC. It is of note because it blurs the line between markup and code. Hamilton, the founder of the Castle Project, toyed with similar ideas when trying to create an IronPython View engine (http://hammett.castleproject.org/?p=94), and Spark seems to have come up with a similar idea but takes it to the next level with a number of choices for making your Views aesthetically pleasing and functional.

First, you can use standard <% %> syntax:

<%int total = 0;foreach(var item in items){  total += item.Quantity;}%>

or a hash syntax for code blocks:

#int total = 0;#foreach(var item in items)#{#   total += item.Quantity;#}

It's nice to have a choice, but neither of these two examples is revolutionary. The real spark of Spark is code like this:

<viewdata model=" IEnumerable[[Person]]"/><ul class="people "><li each=" var person in ViewData.Model">${person.LastName}, ${person.FirstName}</li></ul>

ScottHa's eyes blurred when Rob saw this, but then he realized what was happening. The each attribute is specific to Spark and provides a means of data binding to the supplied code expression. Thus, the <li> element will be repeated once per person. Within the <li> tag, you can use the Spark templating syntax to output properties of the current person instance.

This makes for a very natural HTML-centric syntax such that the code almost disappears in the markup. Remember that a View is only supposed to contain control flow that is specific to its one concern — generating the View. The View isn't a place for business logic or complex code, and Spark enables an idiomatic coding style that cleans up many of the kinds of inline code that makes some Views look "messy." Additionally, at the time of this writing, Spark is well-formed XML, which opens the door for the use of editors and other accelerators in the future.

Spark is definitely an innovative View engine to keep an eye on, and it is representative of the kind of exciting outside-the-box thinking that's happening around the "V" in MVC. Louis recently released support for IronPython and IronRuby using Spark, for those who enjoy using a dynamic language in their Views.

 

New View Engine or New ActionResult?

One question the authors are often asked is when should someone create a custom View engine as opposed to simply a new ActionResult type? For example, suppose that you want to return objects via a custom XML format; should you write a custom View engine or a new MyCustomXmlFormatActionResult?

The general rule of thumb for choosing between one and the other is whether or not it makes sense to have some sort of template file that guides how the markup is rendered. If there's only one way to convert an object to the output format, then writing a custom ActionResult type makes more sense.

For example, the ASP.NET MVC framework includes a JsonResult, by default, which serializes an object to JSON syntax. In general, there's only one way to serialize an object to JSON. You wouldn't change the serialization of the same object to JSON according to which action method or View is being returned. Serialization is generally not controlled via templating.

But suppose that you wanted to use XSLT to transform XML into HTML. In this case, you may have multiple ways to transform the same XML into HTML depending on which action you're invoking. In this case, you would create an XsltViewEngine, which uses XSLT files as the View templates.

 

Summary

View engines have a very specific, constrained purpose. They exist to take data passed to them from the Controller, and they generate formatted output, usually HTML. Other than those simple responsibilities, or "concerns," as the developer, you are empowered to achieve the goals of your View in any way that makes you happy.

posted on 2010-06-26 13:24  Yan-Feng  阅读(536)  评论(0编辑  收藏  举报