ASP.NET MVC 3 新HTML Helper筆記(1) -- WebGrid Class

事情永遠是自己想太多。 -- By KKBruce
我在三月時寫了 ASP.NET MVC 3概觀正體中文版及五月時寫了 ASP.NET MVC 3 RTM Tools Update for CHT,算是把整個 ASP.NET MVC 3 所有更新的內容整個灠覽一篇,但其中一直有一點一直讓我心癢癢的,就是在ASP.NET MVC 3概觀正體中文版裡關於 Razor 提供新的 HTML Helper (Chart, WebGrid, Crypto, WebImage, WebMail),為什麼好東西不和好朋友分享呢?因為相關文章都把這些 ASP.NET MVC 3 新 Helper 和 Razor 放在一起使用,造成我的誤解,以為要使用這些新 Helper 必須要和 Razor 一起才能使用,後來發現我錯了。

在查詢 ASP.NET MVC 3參考 (英文) (ASP.NET MVC 3 Reference) 時,發現了一些好玩的事,怎麼我找不到任何關於 ASP.NET MVC 3 新增的 HTML Helper 的相關類別,ASP.NET MVC 3 只新增了一個 System.Web.Mvc.Razor 類別,其他四個與ASP.NET MVC 2 一樣,經查詢,這些 ASP.NET MVC 3 的新 HTML Helper 是在 System.Web.Helper 類別,而且此類別是位於 Extensions 之下,而非 Frameowrk,如果你專案是開 ASP.NET MVC 3 專案,預設是會加入參考。


都已經預設加入參考,那我可不可以在非 Razor 下使用它呢?答案當然是 No problem。

MVC 3 New HTML Helper 1 -- WebGrid

在MVC 3之前(不就 MVC 2呢?),如果我們要對一個表格(或說 Model)要做一些進階的效果,例如,我們想做分頁,或除了分頁還是做分頁的Ajax效果…?網路上都有範例可以參考,例如使用jqGrid,或使用Martijn Boland實作的 MvcPaging函式庫,或大陸有位杨涛所實作 MvcPager分页控件等,都是很棒的解決方案。

現在我們能有還能使用 ASP.NET MVC 3 所提供的 WebGrid。我們使用預設的 HomeController 的 Index Action來示範,我們先清除Index Action的程式碼及刪除Index.aspx這個View。

Function Index() As ActionResult

    Dim nw As New NorthwindEntities
    Dim customers = nw.Customers().ToList()

    Return View(customers)
End Function

Index()動作很簡單,使用Entiry Framework來傳入Customer資料表。

然後我們使用強型別(Customers)來建置 Index.aspx 這個View,請直接拉到最後面

<%@ Page Title="" Language="VB" MasterPageFile="~/Views/Shared/Site.Master" 
Inherits="System.Web.Mvc.ViewPage(Of IEnumerable (Of MvcWebGrid.Customers))" %>

<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
    Index
</asp:Content>

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

<h2>Index</h2>

<p>
    <%: Html.ActionLink("Create New", "Create") %>
</p>
<table>
    <tr>
        <th>
            CompanyName
        </th>
        <th>
            ContactName
        </th>
        <th>
            ContactTitle
        </th>
        <th>
            Address
        </th>
        <th>
            City
        </th>
        <th>
            Region
        </th>
        <th>
            PostalCode
        </th>
        <th>
            Country
        </th>
        <th>
            Phone
        </th>
        <th>
            Fax
        </th>
        <th></th>
    </tr>

<% For Each item In Model %>
    <% Dim currentItem = item %>
    <tr>
        <td>
            <%: Html.DisplayFor(Function(modelItem) currentItem.CompanyName) %>
        </td>
        <td>
            <%: Html.DisplayFor(Function(modelItem) currentItem.ContactName) %>
        </td>
        <td>
            <%: Html.DisplayFor(Function(modelItem) currentItem.ContactTitle) %>
        </td>
        <td>
            <%: Html.DisplayFor(Function(modelItem) currentItem.Address) %>
        </td>
        <td>
            <%: Html.DisplayFor(Function(modelItem) currentItem.City) %>
        </td>
        <td>
            <%: Html.DisplayFor(Function(modelItem) currentItem.Region) %>
        </td>
        <td>
            <%: Html.DisplayFor(Function(modelItem) currentItem.PostalCode) %>
        </td>
        <td>
            <%: Html.DisplayFor(Function(modelItem) currentItem.Country) %>
        </td>
        <td>
            <%: Html.DisplayFor(Function(modelItem) currentItem.Phone) %>
        </td>
        <td>
            <%: Html.DisplayFor(Function(modelItem) currentItem.Fax) %>
        </td>
        <td>
            <%: Html.ActionLink("Edit", "Edit", New With {.id = currentItem.CustomerID}) %> |
            <%: Html.ActionLink("Details", "Details", New With {.id = currentItem.CustomerID}) %> |
            <%: Html.ActionLink("Delete", "Delete", New With {.id = currentItem.CustomerID}) %>
        </td>
    </tr>
<% Next %>

</table>

<h2>WebGrid (有分頁及排序)</h2>
<p>
<%: New WebGrid(Model).GetHtml()%>
</p>
</asp:Content>

我們使用List Scaffold建置出Index.aspx這個View,在最後面新增一行「<%: New WebGrid(Model).GetHtml()%>」,讓List Scaffold 所產生效果與 WebGrid 產生的效果比較一下,


上半部分,原始的 List 是使用 For Each來一筆一筆跑出資料,利用 79行程式碼跑出一個超長的Table。下半部分,WebGrid 產生的 Table ,預設有含有「排序及分頁」兩大功能,而且全部只有一行程式加一個參數,有沒有感動到。

WebGrid 能做的不只有這樣,我們針對傳入的Model及Html控制到很細項目,例如,針對Model的控制,

例如,

<%: New WebGrid(Model, defaultSort:="ContactName", rowsPerPage:=5).GetHtml()%>

預設我想針對「ContactName」來排序,且每一分頁使用 5 筆資料。另外,由於預設參數很多,建議可以使用「指名參數(:=, 冒號加等號)」的寫法來設定。能不能有分頁,使用「CanPage」,能不能排序,使用「canSort」,實在很方便。


再來,我們再針對HTML (table) 來進行設置,


這邊我們改寫一下Code,先產生WebGrid的執行個體,這樣才有辨法傳入GetHtml()方法當參數:

<% Dim wg = New WebGrid(Model, defaultSort:="CustomerID", rowsPerPage:=5)%>
<%: wg.GetHtml(columns:=wg.Columns(
                        wg.Column("Address", "地址"),
                        wg.Column("City", "城市"),
                        wg.Column("CompanyName", "公司")))%>

我們看一下結果,


我們希望產生三個Column,而且對應欄位顯示中文。那麼我們可不可以產生CURD呢?還是那一句話「No problem!」,不然就像桌子少一支腳,怎麼行。

<% Dim wg = New WebGrid(source:=Model, defaultSort:="CustomerID", rowsPerPage:=5)%>
<%: wg.GetHtml(columns:=wg.Columns(
                           wg.Column(format:=Function(item) Html.ActionLink("編輯", "Edit", New With {.id = item.CustomerID})),
                           wg.Column(format:=Function(item) Html.ActionLink("刪除", "Delete", Nothing,
                                                                            New With {.onclick =
                                                                                      String.Format("deleteRecord('CustomerID', '{0}')", item.CustomerID),
                                                                                      .href = "JavaScript:void(0)"})),
                           wg.Column("Address", "地址"),
                           wg.Column("City", "城市"),
                           wg.Column("CompanyName", "公司"))
                        )%>

我們產生兩個新欄位,我們透過匿名函式來產生對應的Link,編輯及刪除所產生對應的HTML Code,

<!-- 編輯 -->
<a href="/Home/Edit/ALFKI">編輯</a>
<!-- 刪除 -->
<a onclick="deleteRecord('CustomerID', 'ALFKI')" href="JavaScript:void(0)">刪除</a>

對照HTML Code來看,就比較能了解我們設定那些參數的用意。再來,我們在進行分頁時,可以看得出來是使用 Postback 的方法,如果要使用 Ajax 的方法,WebGrid也是支援的,

<div id="DivCustomer">
<% Dim wg = New WebGrid(source:=Model, defaultSort:="CustomerID", rowsPerPage:=5, ajaxUpdateContainerId:="DivCustomer")%>
<%: wg.GetHtml(columns:=wg.Columns(
                           wg.Column(format:=Function(item) Html.ActionLink("編輯", "Edit", New With {.id = item.CustomerID})),
                           wg.Column(format:=Function(item) Html.ActionLink("刪除", "Delete", Nothing,
                                                                            New With {.onclick =
                                                                                      String.Format("deleteRecord('CustomerID', '{0}')", item.CustomerID),
                                                                                      .href = "JavaScript:void(0)"})),
                           wg.Column("Address", "地址"),
                           wg.Column("City", "城市"),
                           wg.Column("CompanyName", "公司"))
                        )%>

我們在第一行的程式碼內指定屬性「ajaxUpdateContainerId:="DivCustomer"」,意用是我們要使用Ajax,更新到「HTML元素id為『DivCustomer』內」,這裡有兩點需注意,
  1. 需把WebGrid包在指定的HTML元素內;
  2. 需引用jQuery;

最後我們來討論「分頁」效能問題,其實也不用討論,直接看它所傳入SQL Server的T-SQL:

SELECT 
[Extent1].[CustomerID] AS [CustomerID], 
[Extent1].[CompanyName] AS [CompanyName], 
[Extent1].[ContactName] AS [ContactName], 
[Extent1].[ContactTitle] AS [ContactTitle], 
[Extent1].[Address] AS [Address], 
[Extent1].[City] AS [City], 
[Extent1].[Region] AS [Region], 
[Extent1].[PostalCode] AS [PostalCode], 
[Extent1].[Country] AS [Country], 
[Extent1].[Phone] AS [Phone], 
[Extent1].[Fax] AS [Fax]
FROM [dbo].[Customers] AS [Extent1]

這不是件好事,它不會先做分頁再回傳,而是每一次回傳所有資料再進行分頁及排序等動作。( 因為你原始的 Action 裡的資料,本來就沒進行分頁 )

這時候你有兩種解決方法,一是前面所提使用Martijn Boland實作的 MvcPaging函式庫,兩是參考這篇「Efficient Paging with WebGrid Web Helper - ASP.NET MVC 3」所提供的方法。

解決 WebGrid 分頁問題 -- 使用MvcPaging函式庫

  1. 引用MvcPaging專案,然後編譯;
  2. 在HomeController.vb裡 Imports MvcPaging
  3. 在HomeController.vb 加上page 參數,撰寫分頁程式,
  4. Index.aspx裡Inherits的Of IEnumerable改為Of MvcPaging.IPagedList
  5. 在Index.aspx撰寫分頁程式

HomeController.vb
Function Index(page As Integer?) As ActionResult
    Dim nw As New NorthwindEntities
    'Dim customers = nw.Customers().ToList()
    Dim customers = nw.Customers().OrderBy(Function(c) c.CustomerID).ToPagedList(If(page >= 1, page - 1, 0), 5)

    Return View(customers)
End Function

Index.aspx:在原WebGrid程式碼之加上
<%= Html.Pager(Model.PageSize,Model.PageNumber ,Model.TotalItemCount) %>

我們利用MvcPaging來分頁,因為MvcPaging設定的分頁數量與我們設定WebGrid的「rowsPerPage:=5」一樣,所以我們只是單純的把WebGrid當成一個顯示用的物件,另外ajaxUpdateContainerId設定也可以移除了,因為我們不是使用 WebGrid 來進行分頁。

解決 WebGrid 分頁問題 -- 使用Malcolm Sheridan MVP提供的方案(修正運作無誤版)

原網頁上提供的 jQuery 無法正常運作,我修正了一下,在 Chrome 11.x, Firefox 4.x, IE 9 都能正常運作。

  1. 在 HomeController.vb 匯入 System.Web.Helpers
  2. 新增兩個Action,一個用來檢示,一個用來處理分頁要求
  3. 撰寫 jQuery 分頁程式碼
  4. 在檢示View中,引用 jQuery 程式碼。
建立兩個Action,撰寫相關程式碼,WebGridPager 產生 View ( WebGridPager.aspx )
''' <summary>
''' 檢示結果
''' </summary>
''' <returns></returns>
''' <remarks></remarks>
Function WebGridPager() As ActionResult
    Return View()
End Function

''' <summary>
''' 處理分頁
''' </summary>
''' <param name="page">第幾頁</param>
''' <returns>JSON物件</returns>
''' <remarks></remarks>
Function EfficientPaging(page As Integer?) As ActionResult
    ' 使用 Northwind 資料庫
    Dim nw As New NorthwindEntities
    Dim skip As Integer = If(page.HasValue, page.Value - 1, 0)
    ' 以 Customers 資料表為例
    Dim data = nw.Customers().OrderBy(Function(c) c.CustomerID).Skip(skip * 5).Take(5).ToList()
    ' 傳入資料
    Dim grid As New WebGrid(data)
    ' 產生Table Tag
    Dim htmlstring = grid.GetHtml(tableStyle:="webGrid",
                                  headerStyle:="header",
                                  alternatingRowStyle:="alt",
                                  htmlAttributes:=New With {.id = "DataTable"})

    ' 回傳JSON物件
    ' 無安全性問題,可使用 HTTPGET
    Return Json(New With {.Data = htmlstring.ToHtmlString(),
                          .Count = nw.Customers.Count() / 5},
                JsonRequestBehavior.AllowGet)
End Function

我們新增一個 Pager.js ,撰寫相關 jQuery Code (修正原程式碼分頁無法運作的問題)

$(function () {
    $.getJSON('/Home/EfficientPaging', null, function (html) {
        // 把JSON取得動態 WebGrid 的HTML加入body
        $('body').append(html.Data);

        // 建立 footer
        var footer = createFooter(html.Count);
        
        // 動態為分頁 link 加上 click 事件       
        $('#DataTable tfoot a').live('click', function (e) {
            e.preventDefault();

            // 取得文字,當成 page 參數回傳
            var data = {
                page: $(this).text()
            };

            // 當有 click 時,重新取得分頁資料
            $.getJSON('/Home/EfficientPaging', data, function (html) {
                // 先將舊Table移除
                $('#DataTable').remove();
                // 把JSON取得動態 WebGrid 的HTML加入body
                $('body').append(html.Data);

                // 預設Table沒有tfoot,先加上
                $('#DataTable thead').after('');
                // 加入link到tfoot
                $('#DataTable tfoot').append(footer);
            });
        });
    });
});

function createFooter(count) {
    
    // 預設Table沒有tfoot,先加上
    $('#DataTable thead').after('');

    // 產生link
    var footer = "";
    for (var i = 1; i < (count + 1); i++) {
        footer = footer + '<a href="#">' + i + '</a>&nbsp;';
    }

    // 加入link到tfoot
    $('#DataTable tfoot').append(footer);

    return footer;    
}   

在 WebGridPager.aspx 的 View 中加入引用 Pager.js (需 jQuery,如果 MasterPage 已經引用,只需引用 Pager.js ) 即可。

<body>
  <script src="../../Scripts/jquery-1.6.1.min.js" type="text/javascript"></script>
  <script src="../../Scripts/Pager.js" type="text/javascript"></script>
</body>

那上兩種方法都是使用到 LINQ 的 Skip() 與 Take() 來進行分頁,些種分頁方法能很有效率的只取回相關的記錄,你以 5 筆為一頁,它一次就只會拿回 5 筆,這讓我們可以考慮把 WebGrid 使用在大量資料處理上。

WebGrid 參數

1 Source WebGrid指定資料來源。
2 columnNames Source中欄位的集合。
3 defaultSort 預設序排欄位名稱
4 rowsPerPage 每頁幾筆資料
5 canPage 是否允許分頁,預設true
6 canSort 是否允許排序,預設true
7 ajaxUpdateContainerId 指定Ajax更新的HTML元素ID
8 fieldNamePrefix 給WebGrid產生的參數欄位加前綴字串,例如,未加時"?sort=CustomerID&sortdir=ASC",加一個前綴"grid_"後,"?grid_sort=CustomerID&grid_sortdir=ASC”
9 pageFieldName 自定義分頁QueryString參數名稱
10 sortFieldName 自定義排序QueryString參數名稱
11 sortDirectionFieldName 自定義排序QueryString參數名稱

參考


1 則留言:

  1. 您好, 請問一下, 這個WebGrid我用在MVC4會出現
    CS0246: The type or namespace name 'WebGrid' could not be found
    應該要怎麼解決呢?

    回覆刪除

感謝您的留言,如果我的文章你喜歡或對你有幫助,按個「讚」或「分享」它,我會很高興的。