Order this Book from Amazon
The set of views in an ASP.NET MVC application is the public face of the application. ASP.NET MVC views are responsible for rendering the HTML pages that people see when they visit your website.
In this chapter, you learn how to create and work with views. You learn how to pass information from a controller to a view. You also learn how to create both typed and untyped views. Finally, you learn strategies for testing your views.
Creating a View
The easiest way to create a new view is to create the view from an existing controller action. You can right-click any controller action within the Visual Studio Code Editor window and select the menu option Add View to create a new view automatically (see Figure 1).
Figure 1 – Adding a new view from a controller action
When you select the Add View menu option, the Add View dialog opens (see Figure 2). This dialog enables you to set various view options. For example, you can specify whether a view uses a view master page.
*** Begin Note ***
We discuss master pages in detail in Chapter 10, Understanding View Master Pages and View User Controls.
*** End Note ***
Figure 2 – Using the Add View dialog
For example, if you open a controller named Customer controller and right-click the Index() action to add a view, and you leave the default options selected in the Add View dialog, then you’ll get the view in Listing 1.
Listing 1 – ViewsCustomerIndex.aspx
<%@ Page <title>Index</title> </asp:Content> <asp:Content <h2>Index</h2> </asp:Content>
The file in Listing 1 looks almost like a standard HTML document. This view contains two <asp:Content> tags. Any content that you place within the first <asp:Content> tag appears in the <head> tag of the resulting HTML document. Any content that you place within the second <asp:Content> tag appears in the <body> tag of the resulting HTML document.
For example, the modified Index view in Listing 2 has been modified to display the current time.
*** Begin Note ***
Notice that the first <asp:Content> tag has been removed from the view in Listing 2. If you don’t need to modify anything in the <head> tag, then there is no reason to keep the first <asp:Content> tag.
*** End Note ***
Listing 2 – ViewsProductIndex.aspx with time
<%@ Page <h1>Time</h1> <p> At the tone, the time will be <%= DateTime.Now.ToString("T") %>. </p> </asp:Content>
The view in Listing 2 contains familiar HTML tags such as the <h1>, and <p> tags. You can put anything that you would put in a normal HTML page within a view including images, iframes, Java applets, Flash, and Silverlight.
The view also contains a script that displays the time. The expression DateTime.Now.ToString(“T”) returns the current time (see Figure 3).
Figure 3 – Displaying the current time
You embed a script in a view by using the <% %> script delimiters. For example, if for some bizarre reason you wanted to display the string “Hello World!” nine-hundred and ninety-nine times in a page then you could embed the following script in a page (see Figure 4):
[C#]
<%
for (var i = 1; i < 999; i++)
Response.Write(“Hello World!”);
%>
[VB]
<%
For i = 1 to 999
Response.Write(“Hello World!”)
%>
Figure 4 – Displaying 999 Hello Worlds!
The <%= %> script delimiters are shorthand for <% Response.Write %>. You can use the <%= %> script delimiters to write the value of an expression to the browser. The following two scripts do exactly the same thing:
[C#]
<%= DateTime.Now.ToString(“T”) %>
<% Response.Write(DateTime.Now.ToString(“T”)); %>
[VB]
<%= DateTime.Now.ToString(“T”) %>
<% Response.Write(DateTime.Now.ToString(“T”)) %>
*** Begin Tip ***.
*** End Tip ***
Using View Data
You pass information between a controller and a view by taking advantage of something called view data. You can use view data to represent any type of information including strings, objects, and database records.
For example, imagine that you want to display a set of database records in a view. In that case, you would use view data to pass the set of records from a controller action to a view.
*** Begin Note ***
I like to think of view data as a UPS Truck delivering a package from a controller to a view.
*** End Note ***
You add an item to view data within a controller by adding the item to the view data dictionary exposed by the controller’s ViewData property. For example, you could add the following code to a controller action to add a new item to view data named message:
[C#]
ViewData[“message”] = “Hello World!”;
[VB]
ViewData(“message”) = “Hello World!”
View data works like a dictionary. You can add any key and value pair that you want to the view data dictionary. The key must be a string, but the value can be any type of data whatsoever.
After you add one or more items to view data, you can display these items in a view by accessing the view data dictionary exposed by the view’s ViewData property. For example, the view in Listing 3 displays the value of the message item from view data.
Listing 3 – ViewsPersonIndex.aspx [C#]
<%@ Page <%= ViewData["message"] %> </asp:Content>
Listing 3 – ViewsPersonIndex.aspx [VB]
<%@ Page <%= ViewData("message") %> </asp:Content>
Typed and Untyped Views
In the previous section, you learned how to add items to the view data dictionary. One problem with the view data dictionary is that it represents everything as untyped objects. This means that you must cast items to a particular data type before you can use the items in a view.
Consider, for example, the controller action in Listing 4. This action adds a set of database records representing products to view data.
Listing 4 – ControllersHomeController.cs [C#]
public ActionResult Index() { ViewData["products"] = _dataModel.ProductSet.ToList(); return View(); }
Listing 4 – ControllersHomeController.cs [VB]
Public Function Index() As ActionResult ViewData("products") = _dataModel.ProductSet.ToList() Return View() End Function
The view in Listing 5 illustrates how you can display the records from the database table in an HTML table.
Listing 5 – ViewsHomeIndex.aspx [C#]
<%@ Page <table> <% foreach (var item in (IEnumerable<Product>)ViewData["products"]) { %> <tr> <td> <%= Html.Encode(item.Id) %> </td> <td> <%= Html.Encode(item.Name) %> </td> <td> <%= Html.Encode(item.Description) %> </td> <td> <%= Html.Encode(item.Price) %> </td> </tr> <% } %> </table> </asp:Content>
Listing 5 – ViewsHomeIndex.aspx [VB]
<%@ Page <table> <% For Each item In CType(ViewData("products"), IEnumerable(Of Product))%> <tr> <td> <%=Html.Encode(item)%> </td> <td> <%=Html.Encode(item.Name)%> </td> <td> <%=Html.Encode(item.Description)%> </td> <td> <%=Html.Encode(item.Price)%> </td> </tr> <% Next%> </table> </asp:Content>
In Listing 5, notice that the products item from view data is cast to an IEnumerable of Product. An IEnumerable is something that you can enumerate over – something you can loop through. If you write your view with C# then you must cast the products item to an IEnumerable or you will get an error (see Figure 5). Regardless of whehter you are using C# or VB.NET, you won’t get Intellisense for Product properties unless you cast.
*** Begin Note ***
If you have Option Strict enabled then Visual Basic, like C#, will generate an error when you don’t cast the product item to an IEnumerable.
*** End Note ***
Figure 5 – Failing to cast to IEnumerable
If you don’t want to clutter your views by casting the view data, you can create a strongly typed view.
The view data dictionary exposes a property named Model. Within a controller you can assign anything you want to the view data model. For example, the controller action in Listing 6 assigns the Product database records to the Model property.
Listing 6 – ControllersHomeController.cs using model [C#]
public ActionResult Index() { ViewData.Model = _dataModel.ProductSet.ToList(); return View(); }
Listing 6 – ControllersHomeController.vb using model [VB]
Function Index() As ActionResult ViewData.Model = _dataModel.ProductSet.ToList() Return View() End Function
*** Begin Note ***
The following code does the exact same thing as the code in Listing 6:
[C#]
public ActionResult Index()
{
return View(_dataModel.ProductSet.ToList());
}
[VB]
Function Index() As ActionResult
Return View(_dataModel.ProductSet.ToList())
End Function
*** End Note ***
The advantage of assigning a value to the ViewData.Model property is that you can cast the Model property automatically in a view. In a view, you can specify the type of object that the Model represents with the <%@ Page %> directive Inherits attribute.
For example, the strongly typed view in Listing 6 renders the exact same page as the previous untyped view.
Listing 6 – ViewsHomeIndex.aspx with Model [C#]
<%@ Page <table> <% foreach (var item in ViewData.Model) { %> <tr> <td> <%= Html.Encode(item.Id) %> </td> <td> <%= Html.Encode(item.Name) %> </td> <td> <%= Html.Encode(item.Description) %> </td> <td> <%= Html.Encode(item.Price) %> </td> </tr> <% } %> </table> </asp:Content>
Listing 6 – ViewsHomeIndex.aspx with Model [VB]
<%@ Page <table> <% For Each item In ViewData.Model %> <tr> <td> <%=Html.Encode(item)%> </td> <td> <%=Html.Encode(item.Name)%> </td> <td> <%=Html.Encode(item.Description)%> </td> <td> <%=Html.Encode(item.Price)%> </td> </tr> <% Next%> </table> </asp:Content>
Notice the Inherits attribute in Listing 6. The view inherits from the following class:
[C#]
System.Web.Mvc.ViewPage<IEnumerable<ToyStore.Models.Product>>
[VB]
System.Web.Mvc.ViewPage(Of IEnumerable(Of ToyStore.Product))
This Inherits attribute casts the ViewData.Model property to an IEnumerable of Product. For this reason, in the body of the view, you do not need to cast the ViewData.Model property before looping through its items.
Creating Strongly Typed Views
Providing the proper value for the Inherits attribute for a typed view can be tricky. Because you don’t get any Intellisense for the Inherits attribute, you can easily make a mistake and your view will generate an error.
Instead of creating a strongly typed view by hand, you can take advantage of the Visual Studio Add View dialog to create a strongly typed view automatically (see Figure 6). You open this dialog by right-clicking a controller action and selecting the menu option Add View. Alternatively, you can right click a folder located in the Views folder and select the menu option Add, View.
Figure 6 – Creating a strongly typed view
The Add View dialog includes a checkbox labeled Create a strongly-typed view. If you check this checkbox, you can specify the View data class and the View content.
*** Begin Warning ***
The View data class dropdown list will be empty until you successfully build your application. It is a good idea to select the menu option Build, Build Solution before opening the Add View dialog.
*** End Warning ***
The View data class dropdown enables you to pick a class from your project. The Inherits directive will use this class. For example, you can pick the Product class.
The View content dropdown enables you to pick the type of view that you want to create. Your options are Create, Details, Edit, List, Empty, and List. If you pick List then your vide data model is cast to an IEnumerable of Products. Otherwise, if you pick any of the other options, your view data model is cast to a Product.
Preventing JavaScript Injection Attacks
When you submit form data using a view, the ASP.NET MVC framework validates the form data automatically. If the framework identifies potentially malicious markup, the framework will throw an exception (see Figure 7).
Figure 7 – Preventing an attacking JavaScript
What counts as malicious markup? Anything that could potentially open your website to a JavaScript injection attack. For example, submitting the following text will generate an error:
<script>alert(‘Boo!’)</script>
*** Begin Note ***
JavaScript injection attacks include cross-site scripting (XSS) attacks.
*** End Note ***
In some situations, you don’t want to perform this validation. For example, if you are hosting a discussion forum on building websites then you most likely will want to enable users to post messages that contain HTML tags.
You disable request validation with the [ValidateInput] attribute. You apply this attribute to the controller action that accepts the HTML form input. For example, the Create() action in Listing 7 has request validation disabled.
Listing 7 – ControllersHomeController.cs [C#]
// // POST: /Home/Create [ValidateInput(false)]  7 – ControllersHomeController.vb [VB]
' ' POST: /Home/Create <ValidateInput(false)> _ 
*** Begin Note ***
Unlike ASP.NET Web Forms, you cannot disable request validation with the <%@ Page ValidateRequest=”false” %> directive and you cannot disable request validation in the web configuration (web.config) file. In an ASP.NET MVC application, the only way to disable request validation is with the ValidateInput attribute.
*** End Note ***
If you disable request validation, ensure that you never display user submitted content without first HTML encoding the content. HTML encoding text converts potentially dangerous characters such as < and > into safe entities such as < and >. Use the Html.Encode() helper to HTML encode all user submitted content in your views.
Using Alternative View Engines
The default view engine for the ASP.NET MVC framework is the Web Forms View Engine. The Web Forms View Engine uses ASP.NET pages as views.
The ASP.NET MVC framework was designed to support alternative view engines and there are already several open source alternatives to the Web Forms View Engine. Here’s a list of some of the more interesting and popular ones:
· NHaml – (pronounced enamel) is an implementation of the popular RAILS Haml view engine for the ASP.NET MVC framework. Distributed under the open source MIT license.
· Spark – The idea behind the Spark view engine is to allow “the html to dominate the flow and the code to fit seamlessly.”
· Brail – A port of the Brail view engine from MonoRail to the ASP.NET MVC framework. The Brail view engine is part of the MVCContrib project.
· nVelocity – The nVelocity view engine is a port of the Java Apache Software Foundation Velocity project to the .NET framework. The nVelocity view engine is part of the MVCContrib project.
The different view engines enable you to write your views in radically different ways. For example, Listing 8 contains a sample of a view written with the NHaml view engine.
Listing 8 – ViewsHomeIndex.haml
!!! %html{xmlns=""} %head %title My Index View %body %h1 Product List %ul - foreach (var p in ViewData.Model) %li =m.Name
The NHaml view in Listing 8 loops through all of the products represented by the ViewData.Model property and renders the value of the Product Name property. Just like the Web Forms View Engine, the NHaml View Engine renders HTML. However, notice the terseness of the syntax. You are able to render a valid and complete HTML page that displays a set of database records with a minimum of effort.
You can use multiple view engines in the same ASP.NET MVC application by registering multiple engines in the Global.asax file. Each view engine can handle a file with a different extension. For example, all .aspx files can be rendered by the Web Forms View Engine while all .haml files can be rendered by the NHaml view engine.
Creating a Custom View Engine
Creating a simple custom view engine for the ASP.NET MVC framework is not difficult. When you create a custom view engine, you get to decide how you want to write your views.
The easiest approach to creating a custom view engine is to derive a new view engine from the abstract VirtualPathProviderViewEngine class. This is the base class of the WebFormsViewEngine (the default view engine). The VirtualPathProviderViewEngine class takes care of all of the low-level mechanics of finding and caching views.
The view engine in Listing 11 is the simplest view engine that I could imagine (that’s why I call it the Simple View Engine). The Simple View Engine derives from the VirtualPathProviderViewEngine class and returns Simple Views.
Listing 11 – MyViewEnginesSimpleViewEngine.cs [C#]
using System.Web.Mvc; namespace MvcApplication1.MyViewEngines { public class SimpleViewEngine : VirtualPathProviderViewEngine { public SimpleViewEngine() { this.ViewLocationFormats = new string[] { "~/Views/{1}/{0}.simple", "~/Views/Shared/{0}.simple"}; this.PartialViewLocationFormats = new string[] { "~/Views/{1}/{0}.simple", "~/Views/Shared/{0}.simple" }; } protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath) { var physicalPath = controllerContext.HttpContext.Server.MapPath(viewPath); return new SimpleView(physicalPath); } protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath) { var physicalPath = controllerContext.HttpContext.Server.MapPath(partialPath); return new SimpleView(physicalPath); } } }
Listing 11 – MyViewEnginesSimpleViewEngine.vb [VB]
Public Class SimpleViewEngine Inherits VirtualPathProviderViewEngine Public Sub New() Me.ViewLocationFormats = New String() { "~/Views/{1}/{0}.simple", "~/Views/Shared/{0}.simple"} Me.PartialViewLocationFormats = New String() { "~/Views/{1}/{0}.simple", "~/Views/Shared/{0}.simple" } End Sub Protected Overrides Function CreateView(ByVal controllerContext As ControllerContext, ByVal viewPath As String, ByVal masterPath As String) As IView Dim physicalPath = controllerContext.HttpContext.Server.MapPath(viewPath) Return New SimpleView(physicalPath) End Function Protected Overrides Function CreatePartialView(ByVal controllerContext As ControllerContext, ByVal partialPath As String) As IView Dim physicalPath = controllerContext.HttpContext.Server.MapPath(partialPath) Return New SimpleView(physicalPath) End Function End Class
When you implement the VirtualPathProviderViewEngine class, you are required to implement two methods named CreatePartial() and CreatePartialView(). In Listing 11, these methods simply return an instance of the SimpleView class.
Notice that two properties of the base VirtualPathProviderViewEngine class are set in the constructor. These properties indicate where the view engine should search to find a matching view or partial view. The parameter {1} represents the name of the controller and the parameter (0) represents the name of the action. Therefore, if you request the URL /Product/Index, the Simple View Engine will search in the following locations for a matching view:
ViewsProductIndex.simple
ViewsSharedIndex.simple
The SimpleView class implements the IView interface. The SimpleView class is responsible for actually rendering the view. This class is contained in Listing 12.
Listing 12 – MyViewEnginesSimpleView.cs [C#]
using System.IO; using System.Text.RegularExpressions; using System.Web.Mvc; namespace MvcApplication1.MyViewEngines { public class SimpleView : IView { private string _viewPhysicalPath; public SimpleView(string viewPhysicalPath) { _viewPhysicalPath = viewPhysicalPath; } #region IView Members public void Render(ViewContext viewContext, TextWriter writer) { // Load file string rawContents = File.ReadAllText(_viewPhysicalPath); // Perform replacements string parsedContents = Parse(rawContents, viewContext.ViewData); // Write results to HttpContext writer.Write(parsedContents); } #endregion public string Parse(string contents, ViewDataDictionary viewData) { return Regex.Replace(contents, "\{(.+)\}",m => GetMatch(m, viewData)); } protected virtual string GetMatch(Match m, ViewDataDictionary viewData) { if (m.Success) { string key = m.Result("$1"); if (viewData.ContainsKey(key)) { return viewData[key].ToString(); } } return string.Empty; } } }
Listing 12 – MyViewEnginesSimpleView.vb [VB]
Imports System.IO Public Class SimpleView Implements IView Private _viewPhysicalPath As String Public Sub New(ByVal viewPhysicalPath As String) _viewPhysicalPath = viewPhysicalPath End Sub #Region "IView Members" Public Sub Render(ByVal viewContext As ViewContext, ByVal writer As TextWriter) Implements IView.Render ' Load file Dim rawContents As String = File.ReadAllText(_viewPhysicalPath) ' Perform replacements Dim parsedContents As String = Parse(rawContents, viewContext.ViewData) ' Write results to HttpContext writer.Write(parsedContents) End Sub #End Region Public Function Parse(ByVal contents As String, ByVal viewData As ViewDataDictionary) As String Return Regex.Replace(contents, "{(.+)}", Function(m) GetMatch(m, viewData)) End Function Protected Overridable Function GetMatch(ByVal m As Match, ByVal viewData As ViewDataDictionary) As String If m.Success Then Dim key As String = m.Result("$1") If viewData.ContainsKey(key) Then Return viewData(key).ToString() End If End If Return String.Empty End Function End Class
In Listing 12, the Render() method loads the file that contains the view, performs regular expression replacements in the file, and writes the result to a text writer.
The regular expression replacements are used to inject view data into the view. For example, if the view contains the expression {message} then the message item from view data is injected into that location in the view.
In order to use a custom view engine, you must register the view engine in the Global.asax file. The SimpleViewEngine is registered in the Application_Start() method in Listing 13.
Listing 13 – Global.asax.cs [C#]
protected void Application_Start() { RegisterRoutes(RouteTable.Routes); ViewEngines.Engines.Add(new SimpleViewEngine()); }
Listing 13 – Global.asax.vb [VB]
Sub Application_Start() RegisterRoutes(RouteTable.Routes) ViewEngines.Engines.Add(new SimpleViewEngine()) End Sub
You don’t need to make any changes to your controllers to use a custom view engine. For example, the Index action in Listing 14 adds an item named message to view data and returns a view.
Listing 14 – ControllersSimpleController.cs [C#]
using System.Web.Mvc; namespace MvcApplication1.Controllers { public class SimpleController : Controller { public ActionResult Index() { ViewData["message"] = "Hello World!"; return View(); } } }
Listing 14 – ControllersSimpleController.vb [VB]
Public Class SimpleController Inherits System.Web.Mvc.Controller Function Index() As ActionResult ViewData("message") = "Hello World!" Return View() End Function End Class
The view in Listing 15 is named Index.simple. This view is returned by the Simple controller when you request the URL /Simple/Index.
Listing 15 – ViewsSimpleIndex.simple
<html> <head><title>Index Simple View</title></head> <body> <h1>{message}</h1> </body> </html>
The SimpleView class loads the Index.simple file, replaces {message} with Hello World! and renders the HTML page in Figure 8.
Figure 8 – The Index.simple view
*** Begin Note ***
Of course, Visual Studio does not contain a template for .simple files because we just made up this type of file. To add the file in Listing 15 to your project, add an HTML file and then change its extension.
*** End Note ***
Testing Views
You might want to build unit tests for the views in your ASP.NET MVC application. For example, you might want to test whether a particular view renders an HTML page that contains a particular string. After all, the more you can test, the stronger the safety net for your code.
Unfortunately, there is no easy way to build unit tests for views created with the default Web Forms View Engine. The default Web Forms View Engine relies on classes such as the VirtualPathProvider class and the HttpRuntime class down to its bones.
Therefore, if you want to test the views in your ASP.NET MVC application, you must seek an alternative. In this section, we discuss three methods of testing your views that do not depend on the default Web Forms View Engine.
Test the View Result
In many cases, what you really need to test is not the view, but the view result. In particular, you need to test whether a controller returns the view and view data expected.
Consider the controller in Listing 16. This controller returns a view named Index and an item in view data named message.
Listing 16 – ControllersHomeController.cs [C#]
using System.Web.Mvc; namespace MvcApplication1.Controllers { [HandleError] public class HomeController : Controller { public ActionResult Index() { ViewData["message"] = "Hello World!"; return View("Index"); } } }
Listing 16 – ControllersHomeController.vb [VB]
<HandleError()> _ Public Class HomeController Inherits System.Web.Mvc.Controller Function Index() As ActionResult ViewData("message") = "Hello World!" Return View("Index") End Function End Class
You can write unit tests that verify several properties of the view result returned by the Index action in Listing 16. The unit test in Listing 17 verifies the type of action result returned by the controller, the name of the view result, and the view data associated with the view result.
Listing 17 – ControllersHomeControllerTest.cs [C#]
using System.Web.Mvc; using Microsoft.VisualStudio.TestTools.UnitTesting; using MvcApplication1.Controllers; namespace MvcApplication1.Tests.Controllers { [TestClass] public class HomeControllerTest { [TestMethod] public void Index() { // Arrange var controller = new HomeController(); // Act var result = controller.Index(); // Did we get a view result? Assert.IsInstanceOfType(result, typeof(ViewResult)); // Did we get a view named Index? var indexResult = (ViewResult)result; Assert.AreEqual("Index", indexResult.ViewName); // Did we get message in view data? Assert.AreEqual("Hello World!", indexResult.ViewData["message"]); } } }
Listing 17 – ControllersHomeControllerTest.vb [VB]
Imports Microsoft.VisualStudio.TestTools.UnitTesting Imports System.Web.Mvc <TestClass()> _ Public Class HomeControllerTest <TestMethod()> _ Public Sub Index() ' Arrange Dim controller = New HomeController() ' Act Dim result = controller.Index() ' Did we get a view result? Assert.IsInstanceOfType(result, GetType(ViewResult)) ' Did we get a view named Index? Dim indexResult = CType(result, ViewResult) Assert.AreEqual("Index", indexResult.ViewName) ' Did we get message in view data? Assert.AreEqual("Hello World!", indexResult.ViewData("message")) End Sub End Class
*** Begin Note ***
You can test the view name (the ViewResult.ViewName property) only when a controller action returns a view with an explicit name. In other words, you need to return a view explicitly with View(“Index”) instead of implicitly with View().
*** End Note ***
Test HTML Helpers
When using the default Web Forms View Engine, the best option for testing your views is to move any view logic that you want to test into an HTML helper. Although you cannot easily write unit tests for views, you can easily write unit tests for HTML helpers.
*** Begin Note ***
We discuss HTML helpers in detail in Chapter 6, Using HTML Helpers.
*** End Note ***
For example, imagine that you want to display a list of products from a database in a view (see Figure 9). You could create the view in Listing 17. The problem with this view, however, is that it is not easily testable.
Figure 9 – Displaying a list of products
Listing 17 – ViewsProductIndex.aspx [C#]
<%@ Page <table> <% foreach (var item in Model) { %> <tr> <td> <%= Html.Encode(item.Name) %> </td> <td> <%= Html.Encode(String.Format("{0:c}", item.Price)) %> </td> </tr> <% } %> </table> </asp:Content>
Listing 17 – ViewsProductIndex.aspx [VB]
<%@ Page <table> <% For Each item In Model%> <tr> <td> <%= Html.Encode(item.Name) %> </td> <td> <%= Html.Encode(String.Format("{0:c}", item.Price)) %> </td> </tr> <% Next%> </table> </asp:Content>
Instead of placing the logic to display the database records in a view, you can place the logic in an HTML helper. The HTML helper in Listing 18 displays the set of products.
Listing 18 – HelpersProductHelper.cs [C#]
using System.Collections.Generic; using System.IO; using System.Web.Mvc; using System.Web.UI; using MvcApplication1.Models; namespace MvcApplication1.Helpers { public static class ProductHelper { public static string ProductList(this HtmlHelper helper) { // Get products from view data var products = (IEnumerable<Product>)helper.ViewData.Model; // Create HTML TextWriter var html = new HtmlTextWriter(new StringWriter()); // Open table html.RenderBeginTag(HtmlTextWriterTag.Table); // Render product rows foreach (var(); } // Close table html.RenderEndTag(); return html.InnerWriter.ToString(); } } }
Listing 18 – HelpersProductHelper.vb [VB]
Imports System.IO Public Module ProductHelper <System.Runtime.CompilerServices.Extension()> _ Function ProductList(ByVal helper As HtmlHelper) As String ' Get products from view data Dim products = CType(helper.ViewData.Model, IEnumerable(Of Product)) ' Create HTML TextWriter Dim html = New HtmlTextWriter(New StringWriter()) ' Open table html.RenderBeginTag(HtmlTextWriterTag.Table) ' Render product rows For Each() Next ' Close table html.RenderEndTag() Return html.InnerWriter.ToString() End Function End Module
The view in Listing 17 uses the Html.ProductList() helper to render the list of products. All of the view logic for rendering the HTML table is now encapsulated within the helper method.
Listing 17 – ViewsProductIndex2.aspx [C#]
<%@ Page <%= Html.ProductList() %> </asp:Content>
Listing 17 – ViewsProductIndex2.aspx [VB]
<%@ Page <%= Html.ProductList() %> </asp:Content>
Unlike a view, an HTML helper can be tested. The unit test in Listing 18 verifies that the first row of the HTML table rendered by the ProductList() helper matches the string “<td>Laptop</td><td>$878.23</td>”.
Listing 18 – HelpersProductHelperTest.cs [C#]
using System.Collections.Generic; using Microsoft.VisualStudio.TestTools.UnitTesting; using MvcApplication1.Helpers; using MvcApplication1.Models; using MvcFakes; namespace MvcApplication1.Tests.Helpers { [TestClass] public class ProductHelperTest { [TestMethod] public void ContainsHtmlRow() { // Arrange products var products = new List<Product>(); products.Add(Product.CreateProduct(-1, "Laptop", "A laptop", 878.23m)); products.Add(Product.CreateProduct(-1, "Telescope", "A telescape", 200.19m)); // Arrange HTML helper var helper = new FakeHtmlHelper(); helper.ViewData.Model = products; // Act var result = ProductHelper.ProductList(helper); // Assert StringAssert.Contains(result, "<td>Laptop</td><td>$878.23</td>"); } } }
Listing 18 – HelpersProductHelperTest.vb [VB]
Imports Microsoft.VisualStudio.TestTools.UnitTesting Imports MvcFakes <TestClass()> _ Public Class ProductHelperTest <TestMethod()> _ Public Sub ContainsHtmlRow() ' Arrange products Dim products = New List(Of Product)() products.Add(Product.CreateProduct(-1, "Laptop", "A laptop", 878.23D)) products.Add(Product.CreateProduct(-1, "Telescope", "A telescape", 200.19D)) ' Arrange HTML helper Dim helper = New FakeHtmlHelper() helper.ViewData.Model = products ' Act Dim result = ProductHelper.ProductList(helper) ' Assert StringAssert.Contains(result, "<td>Laptop</td><td>$878.23</td>") End Sub End Class
*** Begin Note ***
The unit test in Listing 18 takes advantage of the MvcFakes project. The FakeHtmlHelper class is defined in the MvcFakes project included on the CD in the CommonCode folder.
*** End Note ***
Whenever you need to build unit tests for your view logic, consider moving the view logic into a separate HTML helper. You can’t easily unit test the page rendered by a view, but you can easily test the content rendered by an HTML helper.
Test a Custom View Engine
The final option for building unit tests for view is (most likely) the least appealing option. You can easily test views when you use a view engine other than the default Web Forms View Engine. I claim that this is the least appealing option because I expect the vast majority of developers to use the default Web Forms View Engine.
For example, earlier in this chapter, we created a Simple View Engine (see Listings 11). The Simple View Engine returns Simple Views (see Listing 12). Simple views are easy to test as illustrated by the unit test in Listing 19.
Listing 19 – ControllersSimpleControllerTest.cs [C#]
using System.IO; using System.Web.Mvc; using Microsoft.VisualStudio.TestTools.UnitTesting; using MvcApplication1.MyViewEngines; namespace MvcApplication1.Tests.Controllers { [TestClass] public class SimpleControllerTest { private TestContext testContextInstance; public TestContext TestContext { get { return testContextInstance; } set { testContextInstance = value; } } [TestMethod] public void IndexView() { // Create simple view var viewPhysicalPath = testContextInstance.TestDir + @"....MvcApplication1ViewsSimpleIndex.simple"; var indexView = new SimpleView(viewPhysicalPath); // Create view context var viewContext = new ViewContext(); // Create view data var viewData = new ViewDataDictionary(); viewData["message"] = "Hello World!"; viewContext.ViewData = viewData; // Render simple view var writer = new StringWriter(); indexView.Render(viewContext, writer); // Assert StringAssert.Contains(writer.ToString(), "<h1>Hello World!</h1>"); } } }
Listing 19 – ControllersSimpleControllerTest.vb [VB]
Imports Microsoft.VisualStudio.TestTools.UnitTesting Imports System.Web.Mvc Imports System.IO <TestClass()> _ Public Class SimpleControllerTest Private testContextInstance As TestContext Public Property TestContext() As TestContext Get Return testContextInstance End Get Set(ByVal value As TestContext) testContextInstance = value End Set End Property <TestMethod()> _ Public Sub IndexView() ' Create simple view Dim viewPhysicalPath = testContextInstance.TestDir & "....MvcApplication1ViewsSimpleIndex.simple" Dim indexView = New SimpleView(viewPhysicalPath) ' Create view context Dim context = New ViewContext() ' Create view data Dim viewData = New ViewDataDictionary() viewData("message") = "Hello World!" context.ViewData = viewData ' Render simple view Dim writer = New StringWriter() indexView.Render(context, writer) ' Assert StringAssert.Contains(writer.ToString(), "<h1>Hello World!</h1>") End Sub End Class
The unit test in Listing 18 instantiates a simple view by passing the physical path to the view to the SimpleView class constructor. Next, the SimpleView.Render() method is called to render the view to a string writer. Finally, an assertion is made that the writer contains the text “<h1>Hello World!</h1>”.
Summary
This chapter was devoted to the subject of ASP.NET MVC views. First, you learned how to create views and you learned about the distinction between typed and untyped views. You learned how to pass data from a controller to a view by using view data.
Next, we discussed the important subject of preventing JavaScript injection attacks. You learned how to disable request validation by using the [ValidateInput] attribute.
You also were provided with an overview of alternative view engines and you learned how to create a custom view engine of your own. You learned how to create the Simple View Engine and Simple Views.
Finally, we leapt into the very important topic of testing your views. You learned several different strategies for building unit tests for your views. You learned how to test view results, how to test HTML helpers, and how to test custom views.
Fantastic series, Stephen. Can’t wait for the book
Great Post, This is the best article on view I have read so far. Thanks Stephen!
Hello Stephen…
Here there are other recommendations.
1. Before Figure 2 – Using the Add View dialog there is a note about Master Page. Why don´t you add one about “Partial Views”? What is a partial View?
2. After the Listing 2 you mention the 2
tags. I think you can add some text like: tags. Both appears because the chosen Master view also has 2 tags, but this initial number can vary in accordance with the Master. For our case, any content that you place within the first tag appears in the tag of the resulting HTML document. And any content that you place within the second tag appears in the tag of the resulting HTML document”.
“The file in Listing 1 looks almost like a standard HTML document. This view contains two
3. Before Figure 3 you mention script. This is different of JavaScript, so it´ll be better you explicit tell about “.Net Script”. Also you can mention that in any view you can add JavaScript too.
4. In the Listing 4 you use the Model. You can change the second paragraph of Typed and Untyped Views like: “Consider, for example, the controller action in Listing 4. This action uses a Model (as seen in Chapter 2) and adds a set of database records representing products to view data. We discuss how use the model to get records of a Database in Chapter Z…”
5. In the paragraph after listing 5. There is a typo: whehter.
6. In the paragraph before Listing 6 you mention the Model property of View Data dictionary. Why don’t you add a text like this: “As a best practice you use the View Data Dictionary to pass short values to the view (as messages, strings, ints…) and the ViewData.Model to pass Objects from your Model. Consider that you can only pass one type of the model in the ViewData.Model”.
Question: What if in the same page I need to have 2 or 3 different models?
7. I think in the MVC team have discussed about the “practice” you mention in Listing 18, but I don’t like this Helper. I prefer the code of Listing 17 because presentation and the model are separated. If I use the code of Listing 18 will be terrible to change presentation. What if we need to change the presentation from a table to an ordered list? A lot of work.
Well… Congratulations again.
@Gabriel — Thanks Gabriel, those are great comments.
Good article
On first glance, wouldn’t that “999 Hello Worlds” actually print 998?
Great info as usual.
minor oops — in listing 5 [VB] the code for the first cell just has … Encode(item) and it s/b Encode(item.Id) — c# is OK
Also there is no listing 9 & 10, goes from 8 to 11.
A couple of ideas….
In the paragraph after listing 8, regarding the sentence “Each view engine can handle a different extension.” It might be helpful to clarify by prepending/appending something like “because controllers search for a view to render without regard to file extension, and then render the view with the appropriate view engine based on the file extension found, …” A little wordy, but then you’re the writer 🙂
Also, unless it’s going to be covered later, it would be nice to know how to change the “ViewLocationFormats” and “PartialViewLocationFormats” values for an application. I can envision a scenario where one might want to either change or extend the paths to be searched in complex applications, particularly when 3rd-party projects are in a solution.
Which raises a question I have, is there a way to do that without creating a custom view engine? Maybe a partial class override or something? Sure would be nice.
Thanks — looking forward to the next chapter..
Dave
Stephen – Like the chapter a lot. Only one thing: I understand the testablity reseason for “Listing 18 – HelpersProductHelper” but doesnt that sevearlly limit how the View could present the data. You might put a NOTE in mentioning that it would not be a good idea to use this technique if you were handing the View off to a designer. Just a thought.
The first C# example will only write Hello world 998 times… 🙂
Hi Stephen,
I think Paul is right, you need to put < = 999. The other typo i noticed is you put the parameter (0) when it should be the parameter {0}. Finally the information about xss security seems slightly out of place and doesn’t flow with the rest of the article. Maybe it is best placed in the controllers chapter as you have not gone into much detail in terms of the view. Although it is a slightly tricky one because xss is largely specific to the view. Another idea is to add a note when talking about the attributes in the controllers section.
Hi Stephen,
For reuseable purpose, can I store views and/or controllers in a separate assembly e.g. shared assembly instead default website assembly? Does ASP.NET MVC support it?
Thanks,
Hi
Thanks for the article – looking forward to book publication.
Could you clarify something?
I found the c# line 27 in listing 17 didn’t work. My controller returned implicitly with return View(TheTasks.TaskSet.ToList()).
(Different data table but effectively same).
Did you mean to imply this also needs explicit as the Assert prior to it?
>>Unfortunately, there is no easy way to build unit tests for views created with the default Web Forms View Engine. The default Web Forms View Engine relies on classes such as the VirtualPathProvider class and the HttpRuntime class down to its bones.
Stephen, thank you for all of your tips.
Is there any chance the team will some day take a shot at re-factoring the default view engine so it can be tested?
Thanks,
Andrew
Hi Stephen.
I too have a couple of remarks.
– Between listing 1 and 2 there the sentence : For example, the modified Index view in Listing 2 has been modified to display the current time.
Does this imply that you modified listing 1? If so, shouldn’t the listing 2 then not be “ViewsCustomerIndex.aspx with time” instead of “ViewsProductIndex.aspx with time”?
– In one of the tips you state that: .”
Of course this may be valid, but can you give a reason why one should want to do this???
Thanks again for another chapter. I’m looking forward to the next one.
Cheeers Dick.
Thanks for posting these chapters, much appreciated. Is there an estimated publication date?
Underneath Listing 11 you have
‘… implement two methods named CreatePartial() and …”
it should be
‘… implement two methods named CreateView() and …”
Hi,
I am a little disappointed by this chapter, I expected more detail on
‘helper’ (Html and Ajax). Maybe a little on the MvcContrib’s helper too.
But it still a great work !
Thank
Typo found: If you pick List then your vide data model is cast to an IEnumerable…
Hi Stephen,
Very nice job!
ASP NET MVC sounds amazing to me.
Rafael
When will the book be out?
Great Article,
Stephen, I have question for you can I aggregate multiple views from different MVC projects into one view.
Hello Stephen,
I’m trying to find a better way to create Custom Helpers. A way that HTML presentation is not embedded into them.
I see first time in “Providing Website Navigation with SiteMaps” of the ASP.NET MVC Tutorials and I don´t like this Menu Helper.
Well, I wrote you because I´m working my Custom Helpers in a different way. Just think about it:
I just need to add my own BreadCrumbs helper. Then in I create a DataTable with 2 columns: url and text in my controller. Then, I pass to the view this DataTable via ViewData.
In my View I just call my menu helper:
< %= Html.BreadCrumb(( (System.Data.DataTable) ViewData["Path"] ) ) %>
And my BreadCrumb Helper create an XML with the data in the DataTable and transform it with this XLS:
You are in:
<xsl:value-of select=”.”/> >
Well… In this way I have my breadcrumb helper for all my views and I easily can change the HTML presentation of it.
As I`m not an expert in .Net I think “this technique” can be improved and shared with people that is learning MNV. What do you think?
Again thanks for help us learning this godly .Net MNC.
Ups.. Ths xls is:
<xsl:stylesheet version=”1.0″
xmlns:xsl=”“>
<xsl:template match=”/Breadcrumb”>
<p id=”breadcrumb”>
<strong>You are in: </strong>
<xsl:for-each select=”/Breadcrumb/page”>
<a href=”{@url}”><xsl:value-of select=”.”/></a> >
</xsl:for-each>
</p>
</xsl:template>
</xsl:stylesheet>
FYI, there’s also a StringTemplate view engine out there.
code.google.com/p/string-template-view-engine-mvc/
websitelogic.net/…/stringtemplate-viewengine-…
Stephen – great work!
Question about views -> what happens if you wish to send _MULTIPLE_ objects back to the view? In your example above, you’re sending back an IEnumerable
. Simple. What happens if you want to send back 3 IEnumerable ‘s, 1 Orders object and 5 different User objects (etc.. you get the idea). Currently, I create a simple IndexViewData class which has properties to expose the view data requirements, for that individual view.
eg.
PurchasedProducts { get; set; } NewAndSpecialProducts { get; set; }
public class IndexViewData
{
public IEnumerable
public IEnumerable
.. etc …
}
If this is the way to handle this scenario, where is the correct placement of this class? I used to place it in the code-behind for the view (cause i didn’t know of any other place) … which i suppose i still can .. but i’m curious to a ‘best practice’. If i place them all in a single folder, i’ll have naming issues as i have a few Index.aspx pages (even though they exist in different view folders, etc).
thoughts?
Hello Pure,
I would put your class “IndexViewData.cs”, in the folder Models, make sense no?
Really excellent stuff Stephen. Thanks for sharing.
Very description is given about views. Thanks for sharing such a useful information.
Really useful Blog! Thanks for this.
It sounds great.
It should make it a whole lot easier to write good code.
It looks great.
Hope this helps.
Great Things coming up.
wow so many new things coming in Asp.Net.
Anyway, keep up the good work.
Thanks for all the great work so far!
Awesome!
I’m very excited about this.
Thanks, Very nice information.
rock star 🙂
Thanks,
shares some thoughts about MVC framework for ASP.NET on his web site.
I’m sure! Thanks!
sound great,heading for a preview.
Awesome.
this project is just too interesting!
Thanks very much for the article.
We look forwards to see your posts on MVC. Thanks.
Look great.
Wow. It sounds grate.
Thanks, and keep up the good work.
f3q4 r this is given in attachment. I understand that very well. Thanks.
nice post thanks!!!
YOUR GAME MUST FIRST CHANGE IN YOUR TRAIN ‘O THOUGHT…LIVE IT, THINK IT, FEEL IT AND IT WILL HAPPEN…IT MIGHT TAKE LONGER THAN YOU LIKE BUT IT WILL HAPPEN. YOU MUST STUDY THE GAME, UNDERSTAND IT, WANT IT, DREAM IT AND YOU WILL HAVE A BREAK-THROUGH TO THE NEXT LEVEL. IF YOUR NOT SERIOUS IT WILL SHOW…IT HAPPENS IN THE BRAIN BEFORE THE BODY…DON’T GIVE UP…I DON’T CARE IF U R USEING CAVITYBACKS, BLADES, VINTAGE OR NEW CLUBS IT DOESN’T MATTER. YOU WILL ONLY GET BETTER IF YOU ARE STRUGGLING TO BECOME BETTER…IT WILL HAPPEN…i KNOW.
Consider that you can only pass one type of the model in the ViewData.Model”.
Question: What if in the same page I need to have 2 or 3 different models?
hello fellas let me advise you a great medication viagra use it you wont regret it
One of the best tutorials i have read this month! Thanks
this is very good. Very good insight here. I will have to learn this in the future.
Hi, I think your article its very important and interesting,good work, thanks for sharing!! Have a nice day!
Marianna J. Turner
Excellent code, Gabriel also had some good points and cleared up a few things. Thanks guys.
nice post thanks!!!
f3 It’s lucky to know this, if it is really true. Companies tend not to realize when they create security holes from day-to-day operation.
I Am Legend movie download
I Hate Valentine’s Day movie download
I Love You, Man movie download
Ice age movie download
Ice Age: Dawn of the Dinosaurs movie download
Ice Age: The Meltdown movie download
Igor movie download
Imagine That movie download
In the Electric Mist movie download
In the Loop movie download
In the Valley of Elah movie download
Inkheart movie download
Invader movie download
Iron ManR