2013年9月10日 星期二

[轉貼] 全能的資料繫結控制項 ListView


隨附於 Visual Studio® 2008 的 ASP.NET 3.5 版引進了新的資料繫結控制項 -- ListView。我了解您正在想什麼:我們為何還要在 ASP.NET 中加入另一個資料繫結控制項?畢竟在顯示資料集合時,我們已經有超過 10 個控制項可供選用,其中包括半退休的 DataGrid、改進過的新 GridView、一直很可靠且單純的 Repeater、獨特且很有彈性的 DataList、方便的 FormView,以及有一點多餘的同層級控制項 DetailsView。當然還有單一維度的清單控制項:BulletedList、ListBox、DropDownList、RadioButtonList 及 CheckBoxList。
這是因為 ListView 可以真正取代 ASP.NET 中的其他所有資料繫結控制項。對的,就是一切所有的資料繫結控制項。使用 ListView 控制項,您就可以避免使用以上清單中的其他每一個控制項。比起先前的控制項,ListView 還可以簡化數個資料繫結工作,包括 CSS 樣式、彈性分頁,以及排序、插入、刪除及更新等輔助功能。
讓我們從說明 ListView 的一般使用模型開始,然後我會介紹此控制項的功能,並說明其彈性及威力。閱讀本專欄之後,您就可以決定要在 ASP.NET 工具箱中保留多少個資料繫結控制項。

ListView 基本概念
ListView 是一種以樣板導向的控制項,這表示它預設並不會呈現任何東西 -- 您必須以樣板的形式,完整指定希望它呈現的 HTML。與大多數樣板控制項一樣,ItemTemplate 也將成為您大部分工作的重點,因為這就是您放置 HTML 內容的位置,且會針對繫結資料集的每一個資料列重覆執行。
ListView 中的新功能以及使它能夠在控制項中脫穎而出的原因 -- 就是引進了 LayoutTemplate。LayoutTemplate 就是可在最上層定義的 HTML,這會輸出成控制項呈現資訊的一部分。例如,如果您希望 ListView 會呈現出資料表,就可以在 LayoutTemplate 中納入最上層的 <table>,也可以納入 <thead> 項目,然後由 ItemTemplate 呈現資料列和儲存格,如 [圖 1] 所示 (在此案例中,是繫結至包含電影標題和發行日期之簡單資料表的資料來源)。[圖 2] 顯示瀏覽器的呈現情形。
<asp:ListView runat="server" ID="_simpleTableListView" 
  DataSourceID="_moviesDataSource">
  <LayoutTemplate>
    <table>
      <thead>
        <tr>
          <th>ID</th>
          <th>Title</th>
          <th>Release Date</th>
        </tr>
      </thead>
      <tbody>
        <asp:PlaceHolder runat="server" ID="itemPlaceholder" />
      </tbody>
    </table>
  </LayoutTemplate>
  <ItemTemplate>
    <tr>
      <td><%# Eval("movie_id") %></td>
      <td><%# Eval("title") %></td>
      <td><%# Eval("release_date", "{0:d}") %></td>
    </tr>
  </ItemTemplate>
</asp:ListView>

Figure 2 在資料表中顯示的清單 (按影像可放大)
LayoutTemplate 與 ItemTemplate 之間的關聯,是利用 LayoutTemplate 中伺服器端的單一控制項,並將 ID 設定為 itemPlaceholder 來達成的 (您可以使用 ListView 的 ItemPlaceholderID 屬性,來變更 ID 字串的預設值)。在第一個範例中,我在想要加入 ItemTemplate 內容的樣板位置,使用了放置 PlaceHolder 控制項之執行個體的技巧。請注意,雖然可做為預留位置的控制項型別並沒有限制,但是它必須支援子控制項 -- ID 才是最重要的。例如,我如果使用伺服器端的資料表資料列方式來撰寫 LayoutTemplate,而不使用 PlaceHolder 控制項,也能達到相同的目的:
<LayoutTemplate>
  <table>
    <thead>
      <tr>
        <th>ID</th>
        <th>Title</th>
        <th>Release Date</th>
      </tr>
    </thead>
    <tbody>
      <tr runat="server" ID="itemPlaceholder" />
    </tbody>
  </table>
</LayoutTemplate>
一般來說,基於兩項理由,我比較喜歡使用泛型的 PlaceHolder 控制項。第一項是因為名稱可以清楚對應。此外,這個控制項不會呈現自己的 HTML,而是會使用 ItemTemplate 的內容加以取代,因此除了在控制項階層中保留位置以外並沒有其他用途,如此使用控制項看起來是比較合乎邏輯的選擇。
當然,ListView 這麼有彈性的原因,是您可以完全控制 LayoutTemplate 的資訊。您並不會受限於只能使用資料表 -- 您可以在 LayoutTemplate 中放置希望呈現的任何 HTML,只要在 itemPlaceholder 控制項位置插入的 ItemTemplate 內容有意義即可。以下是 ListView 繫結至同一個電影資料來源的範例,但是這一回不使用資料表,而是以項目符號清單顯示電影標題和發行日期 (結果顯示於 [圖 3] 中):
Figure 3 相同的清單,不同的格式 (按影像可放大)
   <asp:ListView runat="server"
     ID="_simpleTableListView" 
     DataSourceID="_moviesDataSource">
     <LayoutTemplate>
       <ul>
         <asp:PlaceHolder runat="server" 
              ID="itemPlaceholder" />
       </ul>
     </LayoutTemplate>
     <ItemTemplate>
       <li><%# Eval("title") %>, 
           <%#Eval("release_date","{0:d}") %></li>
     </ItemTemplate>
                         </asp:ListView>

ListView 和 CSS
ASP.NET 開發人員在著手建立以 CSS 導向的網站時,已長久飽受個別控制項的束縛。許多預設的控制項會呈現內嵌的樣式,或是會使 CSS 類別與其部分 HTML 輸出的關聯很難建立。Microsoft 其實在 2006 年 4 月就推出了名為 CSS Control Adapter Toolkit 的工具組,其中為數個控制項 (包括 GridView) 提供了替代的呈現機制,這些都是完全以 CSS 導向的,以協助排解這個問題 (如需詳細的資訊,請參閱 2006 年 10 月份的 Extreme ASP.NET 專欄,網址為 msdn.microsoft.com/msdnmag/issues/06/10/ExtremeASPNET)。這些替代的呈現方式從未整合到完整的版本中,因此它們依然需要另行安裝,且缺乏設計人員的支援。
ListView 提供了令人耳目一新的單純性,可以在網站中輕鬆運用 CSS,讓您完全控制要在何處與在何時套用樣式表。一個常見的情況,就是開發人員會收到預先設計好的頁面,通常其中包含 HTML 和 CSS。要使用傳統 GridView 呈現資料表的特定設計,以往都很不容易,因為 GridView 類別為修改所產生 HTML 而提供的攔截行為很有限。
我曾看過許多開發人員採用重複嘗試的方式,亦即將樣式屬性套用到方格上,再檢視頁面的來源以確定放置樣式的位置,然後一再重複直到方格呈現所需樣貌為止。使用 ListView 之後,猜測的工作就消失了,因為這樣您就同時需要負責配置以及呈現內容。
舉例來說,假設您收到一個看起來必須如 [圖 4] 所示的資料表,其中的設計包含如 [圖 5] 所示的 .htm 和 .css 檔案。


HTML
<div class="PrettyGrid">
  <table cellpadding="0" cellspacing="0" summary="">
    <thead>
      <tr>
        <th scope="col"><a href="http://.">ID</a></th>
        <th scope="col"><a href="http://.">Title</a></th>
        <th scope="col"><a href="http://.">Release date</a></th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>1</td>
        <td>Where the Wild Things Are</td>
        <td>12/15/2008</td>
      </tr>
  <!-- ... -->
    </tbody>
  </table>
  <div class="Pagination">
    <span>1</span>
    <a href="http://.">2</a>
    <a href="http://.">3</a>
  </div>
</div>
CSS
.PrettyGrid
{
  width: 100%;
}

.PrettyGrid div.Pagination,
.PrettyGrid div.Pagination a,
.PrettyGrid div.Pagination span
{
  color: #00FFFF;
  background: #284775;
  font-weight: normal;
  padding: 2px;
}
.PrettyGrid table
{
  border: solid 1px #CCCCCC;
  width: 100%;
}
/*...*/
Figure 4 資料表的目標設計 (按影像可放大)
您可以迅速建構可呈現與 HTML/CSS 組合完全一樣的 ListView,只要從 HTML 中取得適當的區段,並且將其放置在對應的樣板內,如 [圖 6] 所示。最終的結果看起來會與完全使用 CSS 樣式建立的原始 HTML 一模一樣。若要修改設計也很容易,只要更新 HTML 或對應的 CSS 就可以了。
<asp:ListView ID="_moviesGrid" runat="server" DataKeyNames="movie_id" 
  DataSourceID="_moviesDataSource">            
  <LayoutTemplate>
    <div class="PrettyGrid">
      <table cellpadding="0" cellspacing="0" summary="">
        <thead>
          <tr>
            <th scope="col"><a href="http://.">ID</a ></th>
            <th scope="col"><a href="http://.">Title</a></th>
            <th scope="col"><a href="http://.">Release date</a></th>
          </tr>
        </thead>
        <tbody>
          <asp:PlaceHolder ID="itemPlaceholder" runat="server" />  
        </tbody>
      </table>
      <div class="Pagination">
        <span>1</span>
        <a href="http://.">2</a>
        <a href="http://.">3</a>
      </div>
    </div>
  </LayoutTemplate>

  <AlternatingItemTemplate>
    <tr class="Alternate">
      <td><asp:Label ID="movie_idLabel" runat="server" 
        Text='<%# Eval("movie_id") %>' /></td>
      <td><asp:Label ID="titleLabel" runat="server" 
        Text='<%# Eval("title") %>' /></td>
      <td><asp:Label ID="release_dateLabel" runat="server" 
        Text='<%# Eval("release_date", "{0:d}") %>' />  </td>
    </tr>
  </AlternatingItemTemplate>  

  <ItemTemplate>
    <tr>
      <td><asp:Label ID="movie_idLabel" runat="server" 
        Text='<%# Eval("movie_id") %>' /></td>
      <td><asp:Label ID="titleLabel" runat="server" 
        Text='<%# Eval("title") %>' /></td>
      <td><asp:Label ID="release_dateLabel" runat="server" 
        Text='<%# Eval("release_date", "{0:d}") %>' /> </td>
    </tr>
  </ItemTemplate>
</asp:ListView>


分頁
我在前一節開始使用的原始 HTML 設計,在設計中即隱含有分頁和排序的功能,因此我基於此規格的方格實作工作尚未完成。讓我們從分頁開始,其次再探討排序。
分頁在 ListView 控制項中會透過另一個新控制項 (DataPager) 的引進來完成。藉由將分頁分隔成不同的控制項,DataPager 就會解除分頁 UI 與 ListView 用來呈現資料之項目的聯繫。這就表示您可以將分頁 UI 放在頁面上的任何地方,而且您還可以建立任何數目的 DataPager 控制項。多重分頁控制項的常見用法,是在資料方格的頂端和底部皆提供分頁介面,讓使用者不必捲動方格即可導覽到下一頁 -- 這是很容易用 DataPager 達成的一件事。
讓我們先以前一節的 ListView 範例來實作分頁。建立與 ListView 相關聯的 DataPager 控制項最簡單的方式,就是將 DataPager 控制項嵌入 ListView 的 LayoutTemplate 內:
<asp:ListView ID="_moviesGrid"
  runat="server" DataKeyNames="movie_id" 
  DataSourceID="_moviesDataSource">            
  <LayoutTemplate>
    <!-- ... -->
    <div class="Pagination">
      <asp:DataPager ID="_moviesGridDataPager" runat="server">
        <Fields>
          <asp:NumericPagerField />
        </Fields>
      </asp:DataPager>
    </div>
  </LayoutTemplate>
</asp:ListView>
藉由將 DataPager 嵌入 ListView 的 LayoutTemplate 內,它們就會隱含地相關聯。另一個選項是將 DataPager 放在頁面上的其他位置,然後將其 PagedControlID 設定為相關聯 ListView 的 ID。
在此案例中,NumericPagerField 會顯示我要的介面 -- 就是一系列的數字,顯示成可巡覽頁面的超連結。DataPager 能支援三種型別的欄位:
  • NumericPagerField 會顯示 1 2 3...分頁介面。
  • NextPreviousPagerField 會顯示「下一頁」、「上一頁」、「第一頁」及「最後一頁」按鈕,以便在資料列之間反覆執行。
  • TemplatePagerField 可以讓您使用 PagerTemplate 來定義分頁介面的設計和功能。
DataPager 控制項採用泛型實作,以提供分頁支援給實作 IPageableItemContainer 介面的任何控制項 (目前 ListView 是實作這個介面的唯一控制項),如下所示:
public interface IPageableItemContainer
{
    event EventHandler<PageEventArgs> TotalRowCountAvailable;
    void SetPageProperties(int startRowIndex, int maximumRows, 
                           bool databind);
    int MaximumRows { get; }
    int StartRowIndex { get; }
}
[圖 7] 顯示 ListView、DataPager 以及相關聯之 DataSource 控制項之間的關聯性。DataPager 絕對不會直接與用來填滿 ListView 的 DataSource 互動,但是卻會透過這個介面查詢它需要的資料。
Figure 7 ListView、DataPager 與 DataSource 之間的關聯性 (按影像可放大)
準備分頁時所發生的第一件事,就是 ListView 會向 DataSource 查詢,以了解其是否支援分頁。如果是的話,便會了解其是否可以傳回資料列總數。如果可以的話,ListView 就會擷取資料來源中資料列的總數,然後引發 TotalRowCountAvailable 事件,該事件是隨著其 IPageableItemContainer 介面實作的。任何相關聯的 DataPager 控制項都會訂閱這個事件,並且會使用資料列總數來初始化呈現分頁介面所需要的欄位。然後 DataPager 會叫用 ListView 的 SetPageProperties 方法,來設定初始資料列索引,以及要傳回的資料列數目上限。
當 ListView 從相關聯的資料來源擷取資料時,它會依據 DataPager 所設定的值,僅要求資料列的子集。每當 DataPager 變更其目前的頁面索引 (一般都是由於與使用者的互動),就會再次呼叫 ListView 的 SetPageProperties,以反映目前要擷取的資料列子集。您可以設定 DataPager 控制項的 PageSize 屬性,來變更頁面上所顯示的記錄數目,這個數目會影響它在所對應 ListView 中設定的最大資料列數目資訊。
DataPager 還能支援 QueryStringField 屬性,此屬性徹底的變更了 DataPager 的工作方式。藉由將 QueryStringField 屬性設定為某個字串 (例如 pageNum),就等於是命令 DataPager 發出 HTTP GET 要求,來回應使用者按一下頁面號碼的動作,要求的頁面號碼會透過查詢字串參數來傳送,但會使用您指定的字串,而非傳統的回傳 (POST-Back) 模型。
這項變更有一個有用的副作用,亦即用戶端可以建立書籤,以指向繫結資料之 ListView 控制項中的特定頁面,因為在 URL 中就可以看見頁面號碼。請注意,如果您切換到這種 GET 模型的通訊方式,ASP.NET AJAX UpdatePanel 控制項所使用的回傳攔截機制,就無法攔截分頁的要求並將其轉換成非同步的回傳:
<asp:DataPager ID="_moviesGridDataPager" runat="server"
  QueryStringField="pageNum" >
  <Fields>
    <asp:NumericPagerField />
  </Fields>
</asp:DataPager>
請注意,因為 DataPager 完全依賴 ListView 來執行實際的資料分頁,後者又依賴相關聯的 DataSource 控制項,所以對於其他繫結資料之控制項的限制,分頁也會受到相同的限制。例如,唯有當 SqlDataSource 控制項設定為 DataSet 模式時,分頁才能用於 SqlDataSource 控制項,這表示整個結果集會載入記憶體以執行分頁。當然,您也可以用自訂 DataSource 控制項或使用 ObjectDataSource 控制項,來實作自己的自訂分頁。

排序、編輯、插入及刪除
ListView 若不能支援排序,以及建立、讀取、更新及刪除 (CRUD) 等輔助作業,就不算完整。其針對這些命令的實作,皆類似於 FormView 控制項實作命令的方式。
由於 ListView 完全是以樣板導向的,所以能辨認在其樣板內且將 CommandName 屬性設定為以下七個特定命令字串其中之一的某些按鈕:Cancel、Delete、Select、Edit、Insert、Update 及 Sort。每個命令都會初始化 ListView 上的對應動作 -- 因此如果您想要新增排序功能到 ListView,就必須將一個按鈕 ([圖 8] 中的範例使用了 LinkButton) 放進 LayoutTemplate 內,並且將 CommandName 屬性設定為 Sort,以及將 CommandArgument 設定為您想要據以排序資料來源的資料欄名稱。在 [圖 8] 中,我已修改方格中的每個資料欄,使先前的靜態標頭成為可以按一下的連結,來要求 ListView 根據該資料欄排序資料。
<asp:ListView ID="_moviesGrid" runat="server" DataKeyNames="movie_id" 
  DataSourceID="_moviesDataSource">            
  <LayoutTemplate>
    <div class="PrettyGrid">
      <table cellpadding="0" cellspacing="0" summary="">
        <thead>
          <tr>
            <th scope="col">
              <asp:LinkButton ID="_movieIdSortLink" 
                CommandName="Sort" CommandArgument="movie_id" 
                runat="server">ID</asp:LinkButton>
            </th>
            <th scope="col">
              <asp:LinkButton ID="_titleSortLink" 
                CommandName="Sort" CommandArgument="title" 
                runat="server">Title</asp:LinkButton>
            </th>
            <th scope="col">
              <asp:LinkButton ID="_releaseDateSortLink"
                CommandName="Sort" CommandArgument="release_date" 
                runat="server">Release date</asp:LinkButton>
            </th>
          </tr>
        </thead>  
    <!-- ... -->
  </LayoutTemplate>
</asp:ListView>

您也可以新增命令按鈕來初始化編輯模式、刪除資料列,或將新的資料列插入資料集內,其中的細節基本上與其他以樣板為基礎的繫結資料之控制項相同 (就像 FormView 和 GridView),所以我就不在此說明。

群組
ListView 最後一項主要的功能,就是能夠將資料群組成子集,非常像 DataList 控制項所提供的功能。DataList 是一種表格式控制項,它會在呈現的資料表的每個儲存格中,呈現單一資料列。您可以藉由設定 RepeatColumns 屬性,來控制要將基礎資料集的多少資料列,群組成單一資料表。
因為 ListView 未限定要呈現資料表,所以它需要用泛型的方法來指定如何將群組項目,以呈現在一起,這就是 GroupTemplate 所執行的作業。[圖 9]顯示 ListView 內的 LayoutTemplate、GroupTemplate 及 ItemTemplate 項目之間的關聯性。GroupTemplate 可以讓您針對基礎資料集的每 n 個項目指定週邊 HTML,其中 n 是由 ListView 的 GroupItemCount 屬性來指定。
Figure 9 ListView 中的樣板 (按影像可放大)
當您在 ListView 中使用 GroupTemplate 時,不會在 LayoutTemplate 中指定具有 ID 為 itemPlaceholder 的控制項 -- 現在這個控制項必須在 GroupTemplate 中。反之,您會在 LayoutTemplate 中指定具有 ID 為 groupPlaceholder 的控制項 (您可以設定 ListView 的 GroupPlaceholderID 屬性來變更控制項 ID),來說明針對基礎資料集中遇到的每 n 個項目,應該將 GroupTemplate 的內容插入在何處。
例如,[圖 10] 中的 ListView 顯示如何藉由定義 GroupTemplate 來描述資料列,讓 ItemTemplate 只配置儲存格,使得在資料表的每個資料列中,皆顯示來自資料庫的四部電影。結果如 [圖 11] 所示。
<asp:ListView ID="_groupListView" runat="server" 
  DataKeyNames="movie_id" DataSourceID="_moviesDataSource" 
  GroupItemCount="4" >
  <GroupTemplate>
    <tr>
      <asp:PlaceHolder runat="server" ID="itemPlaceholder" />
    </tr>
  </GroupTemplate>
  <LayoutTemplate>
    <table>
      <asp:PlaceHolder ID="groupPlaceholder" runat="server" />
    </table>
  </LayoutTemplate>
  <ItemTemplate>
    <td>
      movie_id:
      <asp:Label ID="_movie_idLabel" runat="server" 
        Text='<%# Eval("movie_id") %>' /> <br />
      title:
      <asp:Label ID="_titleLabel" runat="server" 
        Text='<%# Eval("title") %>' /> <br />
      release_date:
      <asp:Label ID="_release_dateLabel" runat="server" 
        Text='<%# Eval("release_date", "{0:d}") %>' /> <br />
      <br />
    </td>
  </ItemTemplate>
</asp:ListView>

Figure 11 所產生網頁的 GroupTemplate 資料列 (按影像可放大)
這與您可以用 DataList 執行的作業非常類似,但是因為您使用的是 ListView,所以可以輕易地新增分頁和排序功能,就像先前用方格呈現所做的一樣簡單,這項工作若用 DataList 執行則會相當令人怯步。本文的可下載程式碼含有實作分頁和排序功能的範例,可供您參考。

開始發揮 ListView 的威力
建議您使用 Visual Studio 2008 中的設計工具,來開始試用 ListView 控制項,此舉可讓您從以下五種不同的配置中挑選:方格、並排、項目符號清單、流程及單一資料列。您可以快速地看見不同的配置選項 -- 但是 ListView 真正的威力在於您可以對其呈現之 HTML 執行的控制項,因此您在真實的專案中,很可能需要花費較多的時間自行建立 LayoutTemplate。未來您會不會針對每個資料繫結的情況下都使用 ListView?這樣可能有一點太極端了 -- 但是知道您可以選擇這麼做,將會給您更大的彈性空間。像我就會盡量使用這個彈性的資料繫結控制項。

沒有留言:

張貼留言