共用方式為


本文章是由機器翻譯。

ASP.NET WebGrid

充分利用 ASP.NET MVC 中的 WebGrid

Stuart Leeks

下載代碼示例

今年早些時候,Microsoft 發佈了 ASP.NET MVC 版本 3 (asp.net/mvc) 以及一款名為 WebMatrix 的新產品 (asp.net/webmatrix)。該 WebMatrix 版本中提供了幾個工作效率説明元件,可以簡化諸如圖表和表格資料呈現等任務。其中一個説明元件是 WebGrid,該元件支援通過 AJAX 自訂列的格式、分頁、排序和非同步更新,使表格呈現變得非常簡單。

本文介紹 WebGrid 及其在 ASP.NET MVC 3 中的使用方式,然後討論如何在 ASP.NET MVC 解決方案中充分利用 WebGrid 的功能。有關 WebMatrix 的概述以及本文所用 Razor 語法的相關資訊,請參見 Clark Sell 在 2011 年 4 月期刊中的文章“WebMatrix 簡介”(msdn.microsoft.com/magazine/gg983489)。

本文介紹如何在 ASP.NET MVC 環境中安裝 WebGrid 元件,以提高表格資料的呈現效率。我將從 ASP.NET MVC 角度重點介紹以下與 WebGrid 有關的功能:創建具有完全 IntelliSense 支援的強類型版 WebGrid;利用 WebGrid 支援實現伺服器端分頁;以及添加可在禁用腳本編寫時從容降級的 AJAX 功能。本文所用示例以一個現成的服務為基礎進行構建,該服務通過實體框架提供對 AdventureWorksLT 資料庫的訪問。如果您對資料存取碼感興趣,可在代碼下載部分下載這些代碼,也可查閱 Julie Lerman 在 2011 年 3 月期刊中的文章“使用實體框架和 ASP.NET MVC 3 實現伺服器端分頁”(msdn.microsoft.com/magazine/gg650669)。

WebGrid 入門

為了提供一個簡單的 WebGrid 示例,我設置了一個 ASP.NET MVC 操作,它執行向視圖傳遞 IEnumerable<Product> 的簡單功能。本文中我大多使用 Razor 視圖引擎,但後面我也會討論如何使用 WebForms 視圖引擎。我的 ProductController 類有如下操作:

public ActionResult List()
  {
    IEnumerable<Product> model =
      _productService.GetProducts();
 
    return View(model);
  }

List 視圖中包含如下 Razor 代碼,用於呈現圖 1 所示的網格:

    @model IEnumerable<MsdnMvcWebGrid.Domain.Product>
    @{
      ViewBag.Title = "Basic Web Grid";
    }
    <h2>Basic Web Grid</h2>
    <div>
    @{
      var grid = new WebGrid(Model, defaultSort:"Name");
    }
    @grid.GetHtml()
    </div>

(按一下進行縮放)

圖 1 呈現的基本 Web 網格

該視圖中的第一行指定型號(例如我們在視圖中訪問的 Model 屬性的類型)為 IEnumerable<Product>。然後,我在 div 元素內通過傳入型號資料產生實體一個 WebGrid,我將代碼放入 @{...} 代碼塊中是要告訴 Razor 不要試圖呈現結果。我還在構造函數中將 defaultSort 參數設置為“Name”,告知 WebGrid 傳給它的資料已按 Name 排序。最後,我用 @grid.GetHtml() 生成網格的 HTML 並在回應中呈現網格。

這段代碼雖然不多,卻提供了豐富的網格功能。該網格限制了顯示的資料量,並包含翻閱資料所需的分頁器連結,而且列標題呈現為連結以支援分頁。如果需要自訂該行為,可在 WebGrid 構造函數和 GetHtml 方法中指定一些選項。通過這些選項可以禁用分頁和排序、更改每頁顯示的行數、更改分頁器連結中的文本等等。圖 2顯示了 WebGrid 構造函數參數,圖 3 顯示了 GetHtml 參數。

圖 2 WebGrid 構造函數參數

名稱 類型 備註
source IEnumerable<dynamic> 要呈現的資料。
columnNames IEnumerable<string> 篩選呈現的列。
defaultSort string 指定作為排序依據的預設列。
rowsPerPage int 控制每頁顯示的行數(預設值為 10)。
canPage bool 啟用或禁用資料分頁。
canSort bool 啟用或禁用資料排序。
ajaxUpdateContainerId string 網格中包含元素的 ID,用來啟用 AJAX 支援。
ajaxUpdateCallback string 完成 AJAX 更新後調用的用戶端函數。
fieldNamePrefix string 支援多個網格時查詢字串欄位使用的首碼。
pageFieldName string 頁碼的查詢字串欄位名稱。
selectionFieldName string 所選行號的查詢字串欄位名稱。
sortFieldName string 排序列的查詢字串欄位名稱。
sortDirectionFieldName string 排序方向的查詢字串欄位名稱。

圖 3 WebGrid.GetHtml 參數

名稱 類型 備註
tableStyle string 樣式使用的表類。
headerStyle string 樣式使用的標題行類。
footerStyle string 樣式使用的頁腳行類。
rowStyle string 樣式使用的行類(僅限奇數行)。
alternatingRowStyle string 樣式使用的行類(僅限偶數行)。
selectedRowStyle string 所選的樣式行類。
caption string 顯示為表標題的字串。
displayHeader bool 指示是否應顯示標題行。
fillEmptyRows bool 指示表中是否可以通過添加空行來保證 rowsPerPage 的行數。
emptyRowCellValue string 空行內填充的值,僅在設置了 fillEmptyRows 時使用。
columns IEnumerable<WebGridColumn> 用於自訂列呈現的列模型。
exclusions IEnumerable<string> 自動填充列時要排除的列。
mode WebGridPagerModes 分頁器呈現模式(預設值為 NextPrevious 和 Numeric)。
firstText string 第一頁連結的文本。
previousText string 上一頁連結的文本。
nextText string 下一頁連結的文本。
lastText string 最後一頁連結的文本。
numericLinksCount int 要顯示的數位連結的數量(預設值為 5)。
htmlAttributes object 包含為元素設置的 HTML 屬性。

前一段 Razor 代碼將呈現每一行的所有屬性,但您也可能希望對顯示哪些列作出限制。有多種方法可以實現這一目的。第一種方法(也是最簡單的方法)是將這一組列傳遞到 WebGrid 構造函數。例如,以下代碼只呈現 Name 和 ListPrice 屬性:

var grid = new WebGrid(Model, columnNames: new[] {"Name", "ListPrice"});

也可在 GetHtml 調用而不是在構造函數中指定這些列。 這種方法雖然要編寫稍多的代碼,但好處是可以指定更多關於如何呈現列的資訊。 在下麵的示例中,我指定了 header 屬性,以使 ListPrice 列更便於閱讀:

@grid.GetHtml(columns: grid.Columns(
 grid.Column("Name"),
 grid.Column("ListPrice", header:"List Price")
 )
)

在呈現一組專案時,我們通常希望讓使用者通過點擊一個專案來導航到詳細資訊視圖。 通過 Column 方法的 format 參數可以自訂資料項目的呈現。 以下代碼演示如何更改名稱的呈現方式,以輸出指向某個專案詳細資訊視圖的連結。這段代碼輸出帶兩位小數的“List Price”(貨幣值慣用的小數位數),得到的輸出如圖 4 所示。

@grid.GetHtml(columns: grid.Columns(
 grid.Column("Name", format: @<text>@Html.ActionLink((string)item.Name,
            "Details", "Product", new {id=item.ProductId}, null)</text>),
 grid.Column("ListPrice", header:"List Price", 
             format: @<text>@item.ListPrice.ToString("0.00")</text>)
 )
)

圖 4 採用自訂列的基本網格

雖然我指定格式時發生的情況看似有些神秘,但 format 參數實際就是一個 Func<dynamic,object>,即一個利用動態參數返回物件的委託函數。Razor 引擎採用為 format 參數指定的程式碼片段,並將其轉變為一個委託。該委託採用一個名為 item 的動態參數,format 程式碼片段中正是使用了這個 item 變數。有關這些委託的工作方式的更多資訊,請參見 Phil Haack 在以下位址發表的博客文章:bit.ly/h0Q0Oz

由於 item 參數屬於動態類型,所以在編寫代碼時無法獲得 IntelliSense 支援和編譯器檢查(請參見 Alexandra Rusina 在 2011 年 2 月期刊中發表的關於動態類型的文章msdn.microsoft.com/magazine/gg598922)。而且,也不支援用動態參數調用擴展方法。也就是說,當調用擴展方法時,一定要使用靜態類型。正因為如此,我在前面的代碼中調用 Html.ActionLink 擴展方法時,item.Name 轉換成了 string。由於 ASP.NET MVC 中對擴展方法的使用較為普遍,動態和擴展方法之間的這種衝突可能會讓人疲于應付(在使用 T4MVC 等其他元件時情況甚至更糟:bit.ly/9GMoup)。

添加強類型化

雖然動態類型化可能很適合 WebMatrix,但強類型化視圖也有其優點。實現強類型化的一種辦法是創建一個派生類型 WebGrid<T>,如圖 5 所示。如您所見,這是個非常輕型的包裝!

圖 5 創建派生 WebGrid

public class WebGrid<T> : WebGrid
  {
    public WebGrid(
      IEnumerable<T> source = null,
      ...
parameter list omitted for brevity)
    : base(
      source.SafeCast<object>(), 
      ...
parameter list omitted for brevity)
    { }
  public WebGridColumn Column(
              string columnName = null, 
              string header = null, 
              Func<T, object> format = null, 
              string style = null, 
              bool canSort = true)
    {
      Func<dynamic, object> wrappedFormat = null;
      if (format != null)
      {
        wrappedFormat = o => format((T)o.Value);
      }
      WebGridColumn column = base.Column(
                    columnName, header, 
                    wrappedFormat, style, canSort);
      return column;
    }
    public WebGrid<T> Bind(
            IEnumerable<T> source, 
            IEnumerable<string> columnNames = null, 
            bool autoSortAndPage = true, 
            int rowCount = -1)
    {
      base.Bind(
           source.SafeCast<object>(), 
           columnNames, 
           autoSortAndPage, 
           rowCount);
      return this;
    }
  }

  public static class WebGridExtensions
  {
    public static WebGrid<T> Grid<T>(
             this HtmlHelper htmlHelper,
             ...
parameter list omitted for brevity)
    {
      return new WebGrid<T>(
        source, 
        ...
parameter list omitted for brevity);
    }
  }

這樣做有什麼好處呢?通過實現這個新的 WebGrid<T>,我添加了一個新的 Column 方法,該方法以 Func<T, object> 作為 format 參數,這意味著在調用擴展方法時不必再進行轉換。不僅如此,現在還能夠獲得 IntelliSense 支援和編譯器檢查(假定專案檔案中已經打開 MvcBuildViews,它預設處於關閉狀態)。

通過這種 Grid 擴展方法,您能夠利用編譯器針對范型參數的類型推斷功能。因此,本例中我們只需要編寫 Html.Grid(Model),而不必編寫新的 WebGrid<Product>(Model)。無論採用哪種方式,返回的類型都是 WebGrid<Product>。

添加分頁和排序

如您所見,WebGrid 能讓我們毫不費力的獲得分頁和排序功能。您還瞭解到如何通過 rowsPerPage 參數(位於構造函數中,或通過 Html.Grid 説明程式實現)配置頁面大小,使網格自動顯示單頁數據並呈現頁面導航所使用的分頁控制項。但是,這種預設行為可能滿足不了您的需求。為了說明這一點,我添加了一行代碼,用於在呈現網格後顯示資料來源中包含的項數,如圖 6 所示。

圖 6 資料來源中的項數

可以看到,我們傳遞的資料中包含完整的產品清單(本例中為 295 個產品,但檢索更多資料的情形想來並不少見)。隨著返回資料量的增加,雖然依舊是呈現單頁數據,但服務和資料庫所承受的負荷會越來越大。但是有一種更好的辦法:伺服器端分頁。採用這種方式,只需要取回需要在當前頁面中顯示的資料(例如只顯示五行資料)。

實現 WebGrid 伺服器端分頁的第一步是限制從資料來源檢索的資料量。為此,需要知道請求的是哪一頁數據,以便檢索正確的資料頁。WebGrid 在呈現分頁連結時,會重複使用頁面的 URL,並在頁碼中附加一個查詢字串參數,例如 http://localhost:27617/Product/DefaultPagingAndSorting?page=3(該查詢字串參數的名稱可通過説明程式參數進行配置,這在支援同一頁面中多個網格的分頁時非常有用)。也就是說,您可以在自己的操作方法中採用一個名為 page 的參數,然後使用查詢字串值填充該參數。

如果只是通過修改現有代碼向 WebGrid 傳遞單頁數據,則 WebGrid 只會看到單頁數據。由於它不知道還有別的頁面,因而不再呈現分頁器控制項。幸運的是,WebGrid 還有一種名為 Bind 的方法,可用來指定資料。Bind 不僅能夠接受資料,而且有一個表示總行數的參數,從而據此計算頁數。為了使用此方法,需要更新 List 操作以檢索更多資訊並將其傳入視圖,如圖 7所示。

圖 7 更新 List 操作

public ActionResult List(int page = 1)
{
  const int pageSize = 5;
 
  int totalRecords;
  IEnumerable<Product> products = productService.GetProducts(
    out totalRecords, pageSize:pageSize, pageIndex:page-1);
            
  PagedProductsModel model = new PagedProductsModel
                                 {
                                   PageSize= pageSize,
                                   PageNumber = page,
                                   Products = products,
                                   TotalRows = totalRecords
                                 };
  return View(model);
}

利用這些附加資訊,即可更新視圖以使用 WebGrid 的 Bind 方法。 通過調用 Bind 可提供要呈現的資料和總行數,並將 autoSortAndPage 參數設置為 false。 autoSortAndPage 參數告知 WebGrid 不需要應用分頁,因為這由 List 方法負責。 對此可用下麵代碼說明:

    <div>
    @{
      var grid = new WebGrid<Product>(null, rowsPerPage: Model.PageSize, 
        defaultSort:"Name");
      grid.Bind(Model.Products, rowCount: Model.TotalRows, autoSortAndPage: false);
    }
    @grid.GetHtml(columns: grid.Columns(
     grid.Column("Name", format: @<text>@Html.ActionLink(item.Name, 
       "Details", "Product", new { id = item.ProductId }, null)</text>),
      grid.Column("ListPrice", header: "List Price", 
        format: @<text>@item.ListPrice.ToString("0.00")</text>)
      )
     )
     
    </div>

經過如此改造,WebGrid 又恢復了生機,重新呈現分頁控制項,但分頁發生在服務中而不是視圖中! 但是,由於關閉了 autoSortAndPage,排序功能遭到破壞。 WebGrid 利用查詢字串參數來傳遞排序列和方向,但我們已命令它不執行排序。 解決辦法是在操作方法中添加 sort 和 sortDir 參數,然後將它們傳入服務,讓服務執行必要的排序,如圖 8所示。

圖 8 在操作方法中添加排序參數

public ActionResult List(
           int page = 1, 
           string sort = "Name", 
           string sortDir = "Ascending" )
{
  const int pageSize = 5;
 
  int totalRecords;
  IEnumerable<Product> products =
    _productService.GetProducts(out totalRecords,
                                pageSize: pageSize,
                                pageIndex: page - 1,
                                sort:sort,
                                sortOrder:GetSortDirection(sortDir)
                                );
 
  PagedProductsModel model = new PagedProductsModel
  {
    PageSize = pageSize,
    PageNumber = page,
    Products = products,
    TotalRows = totalRecords
  };
  return View(model);
}

AJAX:用戶端改動

WebGrid 支援通過 AJAX 非同步更新網格內容。 為了利用此功能,應確保包含網格的 div 有一個 id,然後通過 ajaxUpdateContainerId 參數將該 id 傳入網格的構造函數。 還需要對 jQuery 的引用,但這已經包括在佈局視圖中。 指定 ajaxUpdateContainerId 以後,WebGrid 會修改自己的行為,使分頁和排序連結能夠利用 AJAX 進行更新:

    <div id="grid">
     
    @{
      var grid = new WebGrid<Product>(null, rowsPerPage: Model.PageSize, 
      defaultSort: "Name", ajaxUpdateContainerId: "grid");
      grid.Bind(Model.Products, autoSortAndPage: false, rowCount: Model.TotalRows);
    }
    @grid.GetHtml(columns: grid.Columns(
     grid.Column("Name", format: @<text>@Html.ActionLink(item.Name, 
       "Details", "Product", new { id = item.ProductId }, null)</text>),
     grid.Column("ListPrice", header: "List Price", 
       format: @<text>@item.ListPrice.ToString("0.00")</text>)
     )
    )
     
    </div>

儘管內置的使用 AJAX 的功能很不錯,但如果腳本編寫被禁用,生成的輸出將不起作用。 其原因在於,在 AJAX 模式下,WebGrid 在呈現定位標記時將 href 設置為“#”,並通過 onclick 處理常式注入 AJAX 行為。

我一直熱衷於創建能在禁用腳本編寫時從容降級的頁面,最後往往發現做到這一點最好的辦法是漸進式增強(基本原理是提供一個無需腳本即可正常工作的頁面,然後通過腳本對該頁面加以豐富)。 為達到此目的,可恢復為非 AJAX 的 WebGrid,然後創建圖 9 所示的腳本以重新應用 AJAX 行為:

圖 9 重新應用 AJAX 行為

$(document).ready(function () {
 
  function updateGrid(e) {
    e.preventDefault();
    var url = $(this).attr('href');
    var grid = $(this).parents('.ajaxGrid'); 
    var id = grid.attr('id');
    grid.load(url + ' #' + id);
  };
  $('.ajaxGrid table thead tr a').live('click', updateGrid);
  $('.ajaxGrid table tfoot tr a').live('click', updateGrid);
 });

為使腳本只應用到一個 WebGrid 中,它利用 jQuery 選擇器標識出設置了 ajaxGrid 類的元素。 腳本通過 jQuery live 方法 (api.jquery.com/live) 建立排序和分頁連結的 click 處理常式(通過網格容器內的表標題和頁腳進行標識)。 這將為符合選擇器要求的現有和未來元素設置事件處理常式,由於腳本將取代內容,因此這樣做非常方便。

updateGrid 方法被設置為事件處理常式,它首先要做的是調用 preventDefault 以抑制預設行為。 在此之後,該方法獲取要使用的 URL(通過定位標記的 href 屬性獲取),然後通過調用 AJAX 將更新的內容載入到容器元素之中。 為了採用這種做法,一定要禁用預設的 WebGrid AJAX 行為,將 ajaxGrid 類添加到容器 div,然後加入圖 9所示的腳本。

AJAX:伺服器端改動

還有一點需要指出,就是腳本使用 jQuery load 方法中的功能從返回的文檔中分離出一個片段。 只需調用 load(‘http://example.com/someurl’) 就能載入 URL 的內容。 但是,load(‘http://example.com/someurl #someId’) 將從指定 URL 載入內容,然後返回 id 為“someId”的片段。這反映了 WebGrid 的預設 AJAX 行為,意味著不必通過更新伺服器代碼添加部分呈現行為。WebGrid 首先載入整個頁面,然後從中剝離出新的網格。

儘管這樣在快速獲得 AJAX 功能方面非常有效,但也意味著需要通過網路發送不必要的資料,而且可能在伺服器中也要查詢不必要的資料。 幸運的是,ASP.NET MVC 能夠輕鬆解決這個問題。 基本做法是將要在 AJAX 及非 AJAX 請求中共用的呈現內容提取到一個部分視圖中。 隨後,控制器中的 List 操作既可以為 AJAX 調用僅呈現部分視圖,也可以為非 AJAX 調用呈現完整視圖(該完整視圖又使用該部分視圖)。

這種做法非常簡單,只需在操作方法內部測試 Request.IsAjaxRequest 擴展方法的結果即可。 當 AJAX 與非 AJAX 代碼途徑之間的差別非常小時,這種方法十分適用。 然而,兩者之間的差別往往比較大(例如,完全呈現需要的資料比部分呈現多)。 在這種情況下,可能需要編寫一個 AjaxAttribute,以便單獨編寫相應的方法,然後讓 MVC 框架根據請求是否為 AJAX 請求來選擇合適的方法(與 HttpGet 和 HttpPost 屬性的工作方式相同)。 關於這方面的例子,請參閱我在bit.ly/eMlIxU 的博客文章。

WebGrid 和 WebForms 視圖引擎

到目前為止,所有舉例都使用了 Razor 視圖引擎。 在最簡單的情況下,我們不必執行任何修改即可將 WebGrid 用於 WebForms 視圖(暫不論視圖引擎的語法差別)。 在前面的示例中,我演示了如何使用 format 參數自訂行資料的呈現:

grid.Column("Name", 
  format: @<text>@Html.ActionLink((string)item.Name, 
  "Details", "Product", new { id = item.ProductId }, null)</text>),

format 參數實際上是一個 Func,但 Razor 視圖引擎對我們隱藏了這一點。 不過,您還是可以傳遞 Func,例如用 lambda 運算式:

grid.Column("Name", 
  format: item => Html.ActionLink((string)item.Name, 
  "Details", "Product", new { id = item.ProductId }, null)),

借助于這種簡單的轉換,現在我們可以輕鬆地在 WebForms 視圖引擎中使用 WebGrid!

總結

本文介紹了如何通過幾項簡單的調整,在不犧牲強類型化、IntelliSense 和高效伺服器端分頁的情況下利用 WebGrid 為我們提供的功能。 WebGrid 有一些非常棒的功能,可説明我們提高表格資料的呈現效率。 希望本文能為您在 ASP.NET MVC 應用程式中充分利用 WebGrid 提供有益的提示。

Stuart Leeks 是英國高級開發支援團隊的應用程式開發經理,他對於鍵盤快速鍵有著超乎尋常的熱愛。他的博客網站在blogs.msdn.com/b/stuartleeks,他在那裡討論自己感興趣的技術主題(包括但不限於 ASP.NET MVC、實體框架和 LINQ)。

衷心感謝以下技術專家對本文的審閱:Simon InceCarl Nolan