The ASP.NET MVC framework ships with a limited number of HTML helpers. The members of the ASP.NET MVC team identified the most common scenarios in which you would need a helper and focused on creating helpers for these scenarios.
Fortunately, creating new HTML helpers is a very easy process. You create a new HTML helper by creating an extension method on the HtmlHelper class. For example, Listing 7 contains a new Html.SubmitButton() helper that renders an HTML form. submit button.
创建自定义的HTML helpers
ASP.NET MVC框架自带了少量的HTML helpers。ASP.NET MVC团队成员挑出了一些最常见的场景,在这些场景中用户会很需要利用helper,团队成员致力于为这些使用场景创建helper。
11. public static string SubmitButton(this HtmlHelper helper, string buttonText)
12. {
13. return String.Format("", buttonText);
14. }
15.
16. }
17.}
Listing 7 contains an extension method named SubmitButton(). The SubmitButton() helper simply returns a string that represents an HTML tag.
Because the SubmitButton() method extends the HtmlHelper class, this method appears as a method of the HtmlHelper class in Intellisense (see Figure 6).
Figure 6 – Html.SubmitButton() Html helper included in Intellisense
The view in Listing 8 uses the new Html.SubmitButton() helper to render the submit button for a form. Make sure that you import the namespace associated with your helper or the helper won’t appear in Intellisense. The correct namespace is imported in Listing 8 with the help of the <%@ Import %> directive.
*** Begin Note ***
As an alternative to registering a namespace for a particular view with the <%@ Import %> directive, you can register a namespace for an entire application in the system.web.pages.namespaces section of the web configuration (web.config) file.
All of the standard HTML helpers, such as the Html.TextBox() helper, are also implemented as extension methods. This means that you can swap the standard set of helpers for a custom set of helpers.
The TagBuilder class is a utility class included in the ASP.NET MVC framework that you can use when building HTML helpers. The TagBuilder class, as it name suggests, makes it easier to build HTML tags.
Here’s a list of the methods of the TagBuilder class:
• AddCssClass() – Enables you to add a new class=”” attribute to a tag.
• GenerateId() – Enables you to add an id attribute to a tag. This method automatically replaces periods in the id (by default, periods are replaced by underscores)
• MergeAttribute() – Enables you to add attributes to a tag. There are multiple overloads of this method.
• SetInnerText() – Enables you to set the inner text of the tag. The inner text is HTML encode automatically.
• ToString() – Enables you to render the tag. You can specify whether you want to create a normal tag, a start tag, an end tag, or a self-closing tag.
The TagBuilder class has four important properties:
• Attributes – Represents all of the attributes of the tag.
• IdAttributeDotReplacement – Represents the character used by the GenerateId() method to replace periods (the default is an underscore).
• InnerHTML – Represents the inner contents of the tag. Assigning a string to this property does notHTML encode the string.
• TagName – Represents the name of the tag.
These methods and properties give you all of the basic methods and properties that you need to build up an HTML tag. You don’t really need to use the TagBuilder class. You could use a StringBuilder class instead. However, the TagBuilder class makes your life a little easier.
The helper in Listing 9, the Html.ImageLink() helper, is created with a TagBuilder. The Html.ImageLink() helper renders an image link.
Notice that an instance of the UrlHelper class is created. Two methods of this class are called. First, the UrlHelper.Action() method is used to generate the link to the controller action.
Second, the UrlHelper.Content() method is used to convert an application relative path into a full relative path. For example, if your application is named MyApplication then the UrlHelper.Content() method would convert the application relative path “~/Content/Delete.png” into the relative path “/MyApplication/Content/Delete.png”.
例如,可以按下述方式调用Html.ImageLink() helper来输出一个删除链接:
[C#]
<%= Html.ImageLink("Delete", "~/Content/Delete.png", "Delete Account", new {AccountId=2}, null, new {border=0}) %>
As an alternative to using the TagBuilder class to build up HTML content in an HTML helper, you can use the HtmlTextWriter class. Like the TagBuilder class, the HtmlTextWriter class has specialized methods for building up a string of HTML.
Here is a list of some of the more interesting methods of the HtmlTextWriter class (this is not a comprehensive list):
• AddAttribute() – Adds an HTML attribute. When RenderBeginTag() is called, this attribute is added to the tag.
• AddStyleAttribute() – Adds a style. attribute. When RenderBeginTag() is called, this style. attribute is added to the tag.
• RenderBeginTag() – Renders an opening HTML tag to the output stream.
• RenderEndTag() – Closes the last tag opened with RenderBeginTag().
• Write() – Writes text to the output stream.
• WriteLine() – Writes a newline to the output stream (good for keeping your HTML readable when you do a browser View Source).
For example, the HTML helper in Listing 10 uses the HtmlTextWriter to create a bulleted list.
11. public static string BulletedList(this HtmlHelper helper, string name)
12. {
13. var items = helper.ViewData.Eval(name) as IEnumerable;
14. if (items == null)
15. throw new NullReferenceException("Cannot find " + name + " in view data");
16.
17. var writer = new HtmlTextWriter(new StringWriter());
18.
19. // Open UL
20. writer.RenderBeginTag(HtmlTextWriterTag.Ul);
21. foreach (var item in items)
22. {
23. writer.RenderBeginTag(HtmlTextWriterTag.Li);
24. writer.Write(helper.Encode(item));
25. writer.RenderEndTag();
26. writer.WriteLine();
27. }
28. // Close UL
29. writer.RenderEndTag();
30.
31. // Return the HTML string
32. return writer.InnerWriter.ToString();
33. }
34. }
35.}
The list of customers is retrieved from view state with the help of the ViewData.Eval() method. If you call ViewData.Eval(“Customers”), the method attempts to retrieve an item from the view data dictionary named Customers. However, if the Customers item cannot be retrieved from the view data dictionary, then the method attempts to retrieve the value of a property with the name Customers from the view data model (the view data dictionary take precedence over the view data model).
The HtmlTextWriter class is used to render the HTML
and
tags needed to create the bulleted list. Each item from the items collection is rendered into the list.
You can call the Html.BulletedList() helper in a view like this:
<%= Html.BulletedList("Customers") %>
You can add the list of customers to view data with the controller action in Listing 11.
利用ViewData.Eval()方法从view state(视图状态)中获取了客户清单。如果调用ViewData.Eval(“Customers”),则该方法视图从view data字典中获取一个名为Customers的数据项。然而,倘若Customers数据项无法从view data字典中读取,则该方法视图从view data model(视图数据模型)中读取一个名为Customers的属性(view data dictionary视图数据字典优先于view data model视图数据模型)。
3. ViewData["Customers"] = from c in _entities.CustomerSet
4. select c.LastName;
5. return View();
6.}
When you invoke the List() action, a list of customer last names are added to view state. When you call the Html.BulletedList() helper method, you get the bulleted list displayed in Figure 7.
There are multiple ways that you can build up HTML content within an HTML helper. You can use the TagBuilder class, the HtmlTextWriter class, or even the StringBuilder class. The choice is entirely a matter of preference.
In this section, we tackle a more complicated HTML helper: we build an Html.DataGrid() helper that renders a list of database records in an HTML table. We start with the basics and then we add sorting and paging to our Html.DataGrid() helper.
The basic Html.DataGrid() helper is contained in Listing 12.
In Listing 12, the Html.DataGrid() helper method has three overloads. All three overloads are generic overloads. You must supply the type of object – for example, Product – that the Html.DataGrid() should render.
Here are some examples of how you can call the html.DataGrid() helper:
[C#]
<%= Html.DataGrid()%>
<%= Html.DataGrid(ViewData["products"]) %>
<%= Html.DataGrid(Model, new string[] {"Id", "Name"})%>
In the first case, the Html.DataGrid() helper renders all of the items in the view data model into an HTML table. All of the public properties of the Product class are rendered in each row of the HTML table.
In the second case, the contents of the products item in view data is rendered into an HTML table. Again all of the public properties of the Public class are rendered.
In the third case, once again, the contents of the view data model are rendered into an HTML table. However, only the Id and Name properties are rendered (see Figure 8).
第三个例子里,依旧是将view data model(视图数据模型)的内容输出为一个HTML表。然而,仅仅输出Id和Name两个属性(见图8)。
Figure 8 – Rendering an HTML table with the Html.DataGrid() helper
The Html.DataGrid() helper uses an HtmlTextWriter to render the HTML table
, ,
,
,
, and
tags. Rendering these tags by taking advantage of the HtmlTextWriter results in cleaner and more readable code then using string concatenation (Please try to avoid string concatenation whenever possible!).
A tiny bit of reflection is used in the DataGrid() helper. First, reflection is used in the second DataGrid() method to retrieve the list of columns to display when no explicit list of columns is supplied to the helper:
Also, reflection is used to retrieve the value of a property to display within the RenderRow() method:
[C#]
var value = typeof(T).GetProperty(columnName).GetValue(item, null) ?? String.Empty;
*** Begin Note ***
Reflection is a .NET framework feature that enables you get information about classes, methods, and properties at runtime. You can even use reflection to dynamically load assemblies and execute methods at runtime.
*** End Note ***
The Html.DataGrid() helper displays any collection of items that implements the IEnumerable interface. For example, the controller action in Listing 13 assigns a set of products to the view data model property. This list of products can be displayed by the Html.DataGrid() helper.
9. private ToyStoreDBEntities _entities = new ToyStoreDBEntities();
10.
11. public ActionResult Index()
12. {
13. return View(_entities.ProductSet.ToList());
14. }
15. }
16.}
Adding Sorting to the DataGrid Helper
Let’s make our Html.DataGrid() helper just a little more fancy. In this section, we’ll add sorting support. When you click a column header in the HTML table rendered by the Html.DataGrid() helper, the HTML table is sorted by the selected column (see Figure 9).
In order to add the sorting support, we need to modify just one method of the existing DataGridHelper class. We need to modify the RenderHeader() method so that it renders links for headers. The modified RenderHeader() method is contained in Listing 14.
7. var currentAction = (string)helper.ViewContext.RouteData.Values["action"];
8. var link = helper.ActionLink(columnName, currentAction, new {sort=columnName});
9. writer.Write(link);
10. writer.RenderEndTag();
11. }
12. writer.RenderEndTag();
13.}
The modified RenderHeader() method in Listing 14 creates a link for each header column by calling the HtmlHelper.ActionLink() method. Notice that the name of the header column is included as a route value in the link. For example, the following link is rendered for the Price header:
/Product/SortProducts?sort=Price
The actual database sorting happens within the Product controller. The SortProducts action in Listing 15 returns the products in different sort orders depending on the value of the sort parameter passed to the action.
Listing 15 – Controllers\ProductController.cs with SortProducts [C#]
1.public ActionResult SortProducts(string sort)
2.{
3. IEnumerable products;
4. sort = sort ?? string.Empty;
5. switch (sort.ToLower())
6. {
7. case "name":
8. products = from p in _entities.ProductSet
9. orderby p.Name select p;
10. break;
11. case "price":
12. products = from p in _entities.ProductSet
13. orderby p.Price
14. select p;
15. break;
16. default:
17. products = from p in _entities.ProductSet
18. orderby p.Id
19. select p;
20. break;
21. }
22.
23. return View(products);
24.}
Adding Paging to the DataGrid Helper
It really wouldn’t be a proper Html.DataGrid() helper unless the helper supported paging. In this section, we modify our Html.DataGrid() helper so that it supports efficient paging through a large set of database records (see Figure 10).
In order to add paging support, we need to create two new supporting classes:
• PagedList – An instance of this class is passed to the Html.DataGrid() helper to represent a single page of records.
• PagingLinqExtensions – This class contains extension methods that extend the IQueryable interface with ToPagedList() methods that return a PagedList from a query.
The PagedList class is contained in Listing 16. This class
22. public int TotalPageCount { get; private set; }
23.
24. }
25.}
The PagedList class inherits from the base generic List class and adds specialized properties for paging. The PageList class represents the following properties:
• PageIndex – The currently selected page (zero based).
• PageSize – The number of records to display per page.
• SortExpression – The column that determines the sort order of the records.
• TotalItemCount – The total number of items in the database.
• TotalPageCount – The total number of page numbers to display.
The second class, the PagingLinqExtensions class, extends the IQueryable interface to make it easier to return a PagedList from a LINQ query. The PagingLinqExtensions class is contained in Listing 17.
29. var pageOfItems = allItems.Skip(itemIndex).Take(pageSize);
30. var totalItemCount = allItems.Count();
31. return new PagedList(pageOfItems, truePageIndex, pageSize, totalItemCount, sort);
32.
33. }
34. }
35.}
The PagingLinqExtensions class makes it possible to return a PagedList like this:
[C#]
var products = _entities.ProductSet
.OrderBy(p => p.Id)
.ToPagedList(page, 2);
Notice how you can call ToPagedList() directly on a LINQ query. The PagingLinqExtensions class simplifies your code.
Finally, we need to modify our Html.DataGrid() class to use the PagedList class to represent database records. The modified Html.DataGrid() class includes the new RenderPagerRow() method contained in Listing 18.
11. var currentAction = (string)helper.ViewContext.RouteData.Values["action"];
12. for (var i = 0; i < items.TotalPageCount; i++)
13. {
14. if (i == items.PageIndex)
15. {
16. writer.Write(String.Format("{0} ", i + 1));
17. }
18. else
19. {
20. var linkText = String.Format("{0}", i + 1);
21. var link = helper.ActionLink(linkText, currentAction, new { page = i, sort=items.SortExpression});
22. writer.Write(link + " ");
23. }
24. }
25. writer.RenderEndTag();
26. writer.RenderEndTag();
27.}
The RenderPagerRow() method in Listing 18 renders the user interface for paging. This method simply renders a list of page numbers that act as hyperlinks. The selected page number is highlighted with an HTML tag.
The modified Html.DataGrid() helper requires an instance of the PagedList class for its data parameter. You can use the controller action in Listing 19 to add the right data to view state.
Listing 19 – Controllers\ProductController.cs with PagedProducts [C#]
1.public ActionResult PagedProducts(int? page)
2.{
3. var products = _entities.ProductSet
4. .OrderBy(p => p.Id).ToPagedList(page, 2);
5.
6. return View(products);
7.}
*** Begin Warning ***
In Listing 19, notice that the ToPagedList() method is called on a LINQ query that includes a call to the OrderBy() method. When using the Entity Framework, you must order the results of a query before you can extract a page of records from the query.
*** End Warning ***
If you want to both page and sort the products, then you can use the controller action in Listing 20.
Notice that when you want to support sorting, you must pass the current sort column to the ToPageList() method. If you don’t pass the current sort column then clicking a page number causes the Html.DataGrid() to forget the sort order.
In general, you should place any complicated view logic in an HTML helper. There is a simple reason for this: you can test a helper but you cannot test a view.
The Html.DataGrid() helper that we created in the previous section is a good example of a helper that requires unit tests. There are several things that I could have gotten wrong while writing this helper. You should never trust anything that you write!
Here are some expectations for our helper that we might want to test:
• The helper displays the right number of table rows. For example, if you specify that the page size is 2 rows then calling the Html.DataGrid() helper method should render an HTML table that contains 4 rows (1 header row + 2 data rows + 1 pager row).
• The helper selects the right page number. For example, if you specify that the current page index is 1 then page number 2 should be highlighted in bold in the pager user interface.
The test class in Listing 21 contains unit tests for both of these expectations.
If I want to feel completely confident about the Html.DataGrid() helper then I would need to write several more unit tests than the two tests contained in Listing 21. However, Listing 21 is a good start.
Both of the unit tests in Listing 21 take advantage of a utility method named CreateItems() that creates a list that contains a specific number of products.
Both unit tests also take advantage of the FakeHtmlHelper class from the MvcFakes project. When you call an HTML helper, you must supply an instance of the HtmlHelper class as the first parameter. The FakeHtmlHelper enables you to easily fake this helper.
*** Begin Note ***
Before you can run the tests in Listing 21, you must add a reference to the MvcFakes project to your Test project. The MvcFakes project is included in the CommonCode folder on the CD that accompanies this book.
This chapter was devoted to the topic of HTML helpers. You learned how to create views more easily by using HTML helpers to render HTML content.
In the first part of this chapter, you learned how to use the standard set of HTML helpers included with the ASP.NET MVC framework. For example, you learned how to create HTML links with the Html.ActionLink() and Url.Action() helpers. You also learned how to use the form. helpers to render standard HTML form. elements such as dropdown lists and textboxes.
We also discussed how you can make your websites more secure against JavaScript. injection attacks by taking advantage of the Html.AntiForgeryToken() and Html.Encode() helpers.
In the next part of this chapter, we examined how you can create custom HTML helpers. We talked about the different utility classes that you can use when building a custom helper. In particular, you learned how to use the TagBuilder and HtmlTextWriter classes.
We then tackled building a real-world HTML helper. We created an Html.DataGrid() helper that renders database records in an HTML table. We added both sorting and paging support to our custom Html.DataGrid() helper.
In the final section of this chapter, you learned how to build unit tests for your custom HTML helpers. We created two unit tests for our Html.DataGrid() helper.