建立自訂的排序使用者介面 (C#)
演講者:Scott Mitchell
當顯示一長串已排序資料時,透過引入分隔符號列對相關資料進行分組非常有幫助。 在本教學課程中,我們將了解如何建立這樣的排序使用者介面。
簡介
當顯示一長串排序資料時,其中排序列中只有少數不同值,終端使用者可能會發現很難準確辨別差異邊界出現的位置。 例如,資料庫中有 81 個產品,但只有 9 個不同的類別選擇 (八個獨特的類別加上 NULL
選項)。 考慮一個有興趣檢查屬於海鮮類別的產品的使用者的情況。 在單一 GridView 中列出所有產品的頁面中,使用者可能會決定最好的選擇是按類別對結果進行排序,這會將所有海鮮產品組合在一起。 按類別排序後,使用者需要瀏覽清單,尋找海鮮分組產品的開始和結束位置。 由於結果是按類別名稱的字母順序排序的,因此尋找海鮮產品並不困難,但仍需要仔細掃描網格中的項目清單。
為了幫助突出顯示規則群組之間的邊界,許多網站採用在這些組之間新增分隔符的使用者介面。 如圖 1 所示的分隔符號使使用者能夠更快地找到特定群組並識別其邊界,並確定資料中存在哪些不同群組。
圖 1:每個類別組都清晰可辨 (點擊查看完整圖片)
在本教學課程中,我們將了解如何建立這樣的排序使用者介面。
第 1 步:建立標準的可排序 GridView
在我們探索如何增強 GridView 以提供增強的排序介面之前,我們首先建立一個標準的、可排序的 GridView 來列出產品。 首先打開 PagingAndSorting
資料夾中的 CustomSortingUI.aspx
頁面。 將 GridView 新增至頁面,將其 ID
屬性設為 ProductList
,並將其繫結到新的 ObjectDataSource。 設定 ObjectDataSource 以使用 ProductsBLL
類別的 GetProducts()
方法來選擇記錄。
接下來,設定 GridView,使其僅包含 ProductName
、CategoryName
、SupplierName
和 UnitPrice
BoundFields 以及 Discontinued CheckBoxField。 最後,透過勾選 GridView 智慧標籤中的「啟用排序」核取方塊 (或將其 AllowSorting
屬性設為 true
),將 GridView 設定為支援排序。 在 CustomSortingUI.aspx
頁面中加入這些內容後,宣告性標記應類似於以下內容:
<asp:GridView ID="ProductList" runat="server" AllowSorting="True"
AutoGenerateColumns="False" DataKeyNames="ProductID"
DataSourceID="ObjectDataSource1" EnableViewState="False">
<Columns>
<asp:BoundField DataField="ProductName" HeaderText="Product"
SortExpression="ProductName" />
<asp:BoundField DataField="CategoryName" HeaderText="Category"
ReadOnly="True" SortExpression="CategoryName" />
<asp:BoundField DataField="SupplierName" HeaderText="Supplier"
ReadOnly="True" SortExpression="SupplierName" />
<asp:BoundField DataField="UnitPrice" DataFormatString="{0:C}"
HeaderText="Price" HtmlEncode="False" SortExpression="UnitPrice" />
<asp:CheckBoxField DataField="Discontinued" HeaderText="Discontinued"
SortExpression="Discontinued" />
</Columns>
</asp:GridView>
<asp:ObjectDataSource ID="ObjectDataSource1" runat="server"
OldValuesParameterFormatString="original_{0}" SelectMethod="GetProducts"
TypeName="ProductsBLL"></asp:ObjectDataSource>
花點時間在瀏覽器中查看到目前為止我們的進度。 圖 2 顯示了資料按類別按字母順序排序時的可排序 GridView。
圖 2:可排序的 GridView 資料按類別排序 (點擊查看完整圖片)
步驟 2:探索新增分隔符號列的技術
完成通用的可排序 GridView 後,剩下的就是能夠在 GridView 中的每個唯一規則群組之前新增分隔符號列。 但是如何將這些行注入到 GridView 中呢? 本質上,我們需要迭代 GridView 的行,確定排序列中的值之間出現差異的位置,然後新增適當的分隔符號列。 當考慮這個問題時,解決方案似乎很自然地位於 GridView 的 RowDataBound
事件處理常式中的某個位置。 正如我們在「根據資料的自訂格式」教學課程中所討論的,在套用基於列資料的列級格式時,通常會使用此事件處理常式。 但是,RowDataBound
事件處理常式不是此處的解決方案,因為無法透過此事件處理常式以程式設計方式將列新增至 GridView。 事實上,GridView 的 Rows
集合是唯讀的。
要為 GridView 新增額外的列,我們有三種選擇:
- 將這些中繼資料分隔符號列新增至繫結到 GridView 的實際資料中
- 將 GridView 繫結到資料後,將其他
TableRow
執行個體新增至 GridView 的控制項集合中 - 建立一個自訂伺服器控制項,該控制項會擴展 GridView 控制項並重寫負責建構 GridView 結構的那些方法
如果許多網頁或跨多個網站需要此功能,那麼建立自訂伺服器控制項將是最好的方法。 然而,這需要相當多的程式碼和對 GridView 內部工作原理的深入探索。 因此,我們不會在本教學課程中考慮該選項。
另外兩種選擇是將分隔符號列新增到實際繫結到 GridView 的資料中,以及在資料繫結後操作 GridView 的控制集合,這兩種方法各有不同的解決方式,值得進一步討論。
將列新增至繫結到 GridView 的資料
當 GridView 繫結到資料來源時,它會為資料來源傳回的每筆記錄建立 GridViewRow
。 因此,我們可以透過在將分隔符號記錄繫結到 GridView 之前,將分隔符號記錄新增至資料來源,來注入所需的分隔符號列。 圖 3 說明了這個概念。
圖 3:一種技術是將分隔符號列新增到資料來源中
我將「分隔記錄」這個術語放在引號中,因為並不存在特殊的分隔符號記錄;相反,我們必須以某種方式標記資料來源中的特定記錄,以使其作為分隔符號,而不是普通的資料列。 對於我們的範例,我們將 ProductsDataTable
執行個體重新繫結到由 ProductRows
組成的 GridView。 我們可以透過將記錄的 CategoryID
屬性設為 -1
(因為這樣的值通常不存在),來將其標記為分隔符號列。
要利用此技術,我們需要執行以下步驟:
- 以程式設計方式檢索資料以繫結到 GridView (
ProductsDataTable
執行個體) - 根據 GridView 的
SortExpression
和SortDirection
屬性對資料進行排序 - 迭代
ProductsDataTable
中的ProductsRows
,找出排序欄中的差異所在 - 在每個群組邊界處,將分隔符號記錄
ProductsRow
執行個體注入 DataTable,該執行個體的CategoryID
設定為-1
(或決定將記錄標記為分隔符號記錄的任何名稱) - 注入分隔符號列後,以程式設計方式將資料繫結到 GridView
除了這五個步驟之外,我們還需要為 GridView 的 RowDataBound
事件提供一個事件處理常式。 在這裡,我們檢查每個 DataRow
,並確定它是否為分隔符號列,其 CategoryID
設定為 -1
。 如果是這樣,我們可能想要調整其格式或儲存格中顯示的文字。
使用此技術來注入規則群組邊界需要比上面概述的更多工作,因為您還需要為 GridView 的 Sorting
事件提供事件處理常式並追蹤 SortExpression
和 SortDirection
值。
在資料繫結後操作 GridView 的控制項集合
我們可以在將資料繫結到 GridView 之後新增分隔符號列,而不是在將資料繫結到 GridView 之前傳送訊息。 資料繫結過程建構了 GridView 的控制項階層,實際上它只是一個由列集組成的 Table
執行個體,每個列又由儲存格集合組成。 具體而言,GridView 的控制集合在其根目錄包含一個 Table
物件,對於繫結到 GridView 的每則 DataSource
記錄,則有一個 GridViewRow
(其衍生自 TableRow
類別),並且在每個 GridViewRow
執行個體中對於每個 DataSource
的資料欄位則有一個 TableCell
物件。
若要在每個規則群組之間新增分隔符號列,我們可以在建立此控制項階層後直接對其進行操作。 我們可以確信,在呈現頁面時,GridView 的控制項階層已最後一次建立。 因此,此方法會重寫 Page
類別的 Render
方法,此時 GridView 的最終控制項階層將更新以包含所需的分隔符號列。 圖 4 說明了此過程。
圖 4:操縱 GridView 控制項階層的替代技術 (點擊查看完整圖片)
在本教學課程中,我們將使用後一種方法來自訂排序使用者體驗。
注意
我在本教學課程中展示的程式碼是基於 Teemu Keiski 的部落格文章「Playing a Bit with GridView Sort Grouping」 (玩轉 GridView 排序分組) 中提供的範例。
步驟 3:將分隔符號列新增至 GridView 的控制項階層中
由於我們只想在 GridView 的控制階層建立,並在目前造訪頁面中最後一次建立後新增分隔符號列,因此我們希望在頁面生命週期的末尾進行這一操作,但在實際的 GridView 控制階層渲染成 HTML 之前。 我們可以完成此任務的最新可能點是 Page
類別的 Render
事件,我們可以使用以下方法簽署在程式碼隱藏類別中重寫該事件:
protected override void Render(HtmlTextWriter writer)
{
// Add code to manipulate the GridView control hierarchy
base.Render(writer);
}
當 Page
類別的原始 Render
方法被叫用時,base.Render(writer)
頁面中的每個控制項都會被呈現,並根據其控制項階層產生標記。 因此,我們必須呼叫 base.Render(writer)
,以便呈現頁面,並且我們必須在呼叫 base.Render(writer)
之前操作 GridView 的控制項階層,以便在呈現之前將分隔行新增至 GridView 的控制項階層中。
要注入規則群組標頭,我們首先需要確保使用者已要求對資料進行排序。 預設情況下,GridView 的內容未排序,因此我們不需要輸入任何群組排序標題。
注意
如果您希望 GridView 在頁面首次載入時按特定列排序,請在第一個頁面造訪時呼叫 GridView 的 Sort
方法 (但不在後續回傳中)。 若要實現此目的,請在 Page_Load
條件條件內的 if (!Page.IsPostBack)
事件處理常式中新增此呼叫。 有關 Sort
方法的更多資訊,請參閱「分頁和排序報表資料」教學課程資訊。
假設資料已排序,我們的下一個任務是確定資料按哪一個欄排序,然後掃描行以查找該欄值的差異。 以下程式碼確保資料已排序並查找資料已排序的欄:
protected override void Render(HtmlTextWriter writer)
{
// Only add the sorting UI if the GridView is sorted
if (!string.IsNullOrEmpty(ProductList.SortExpression))
{
// Determine the index and HeaderText of the column that
//the data is sorted by
int sortColumnIndex = -1;
string sortColumnHeaderText = string.Empty;
for (int i = 0; i < ProductList.Columns.Count; i++)
{
if (ProductList.Columns[i].SortExpression.CompareTo(ProductList.SortExpression)
== 0)
{
sortColumnIndex = i;
sortColumnHeaderText = ProductList.Columns[i].HeaderText;
break;
}
}
// TODO: Scan the rows for differences in the sorted column�s values
}
如果 GridView 尚未排序,則不會設定 GridView 的 SortExpression
屬性。 因此,如果此屬性具有某些值,我們只想新增分隔符號列。 如果是,我們接下來需要確定資料排序所依據的列的索引。 這是透過循環遍歷 GridView 的 Columns
集合,搜尋 SortExpression
屬性等於 GridView 的 SortExpression
屬性的欄來完成的。 除了欄的索引之外,我們還會取得顯示分隔符號列時使用的 HeaderText
屬性。
利用資料排序所依據欄的索引,最後一步是列舉 GridView 的列。 對於每一列,我們需要確定排序資料行的值是否與前一行排序資料列的值不同。 如果是這樣,我們需要將一個新 GridViewRow
執行個體注入到控制項階層中。 這是透過以下程式碼完成的:
protected override void Render(HtmlTextWriter writer)
{
// Only add the sorting UI if the GridView is sorted
if (!string.IsNullOrEmpty(ProductList.SortExpression))
{
// ... Code for finding the sorted column index removed for brevity ...
// Reference the Table the GridView has been rendered into
Table gridTable = (Table)ProductList.Controls[0];
// Enumerate each TableRow, adding a sorting UI header if
// the sorted value has changed
string lastValue = string.Empty;
foreach (GridViewRow gvr in ProductList.Rows)
{
string currentValue = gvr.Cells[sortColumnIndex].Text;
if (lastValue.CompareTo(currentValue) != 0)
{
// there's been a change in value in the sorted column
int rowIndex = gridTable.Rows.GetRowIndex(gvr);
// Add a new sort header row
GridViewRow sortRow = new GridViewRow(rowIndex, rowIndex,
DataControlRowType.DataRow, DataControlRowState.Normal);
TableCell sortCell = new TableCell();
sortCell.ColumnSpan = ProductList.Columns.Count;
sortCell.Text = string.Format("{0}: {1}",
sortColumnHeaderText, currentValue);
sortCell.CssClass = "SortHeaderRowStyle";
// Add sortCell to sortRow, and sortRow to gridTable
sortRow.Cells.Add(sortCell);
gridTable.Controls.AddAt(rowIndex, sortRow);
// Update lastValue
lastValue = currentValue;
}
}
}
base.Render(writer);
}
此程式碼會先以程式設計方式參考在 GridView 控制項階層根目錄找到的 Table
物件,並建立一個名為 lastValue
的字串變數。 lastValue
用於將目前資料列的排序資料行值與前一列的值進行比較。 接下來,列舉 GridView 的 Rows
集合,並將每列的排序欄的值儲存在 currentValue
變數中。
注意
為了確定特定列的排序欄的值,我使用儲存格的 Text
屬性。 這對於 BoundFields 效果很好,但對於 TemplateFields、CheckBoxFields 等則無法如預期般運作。 我們很快就會了解如何考慮備用 GridView 欄位。
然後比較 currentValue
和 lastValue
變數。 如果它們不同,我們需要在控制項階層中新增一個新的分隔符號列。 這是透過確定 Table
物件的 Rows
集合中 GridViewRow
的索引、建立新的 GridViewRow
和 TableCell
執行個體、然後將 TableCell
和 GridViewRow
新增到控制階層來完成的。
請注意,分隔符號列的單獨 TableCell
的格式設定為跨越 GridView 的整個寬度,使用 SortHeaderRowStyle
CSS 類別進行格式設定,並具有其 Text
屬性,以便它顯示排序群組名稱 (例如類別) 和群組的值 (例如飲料)。 最後 lastValue
更新為 currentValue
的值。
需要在 Styles.css
檔案中指定用於格式化排序群組標題列 SortHeaderRowStyle
的 CSS 類別。 隨意使用任何您喜歡的樣式設定;我使用了以下設定:
.SortHeaderRowStyle
{
background-color: #c00;
text-align: left;
font-weight: bold;
color: White;
}
使用目前代碼,排序介面會在按任何 BoundField 排序時新增排序群組標題 (見圖 5,顯示了按供應商排序時的截圖)。 但是,按照任何其他欄位類型 (例如 CheckBoxField 或 TemplateField) 排序時,將找不到排序群組標題 (請參閱圖 6)。
圖 5:按照 BoundFields 排序時,排序介面會包括規則群組標題 (點擊查看完整圖片)
圖 6:對 CheckBoxField 進行排序時缺少規則群組標題 (點擊查看完整圖片)
按 CheckBoxField 排序時缺少排序組標題的原因是程式碼目前僅使用 TableCell
的 Text
屬性來決定每行的排序欄的值。 對於 CheckBoxFields,TableCell
的 Text
屬性是空字串;相反,該值可透過駐留在 TableCell
的 Controls
集合中的 CheckBox Web 控制項取得。
為了處理 BoundFields 以外的欄位類型,我們需要擴充充分配 currentValue
變數的程式碼,以檢查 TableCell
的 Controls
集合中是否存在 CheckBox。 請將此程式碼替換為以下內容,而不是使用 currentValue = gvr.Cells[sortColumnIndex].Text
:
string currentValue = string.Empty;
if (gvr.Cells[sortColumnIndex].Controls.Count > 0)
{
if (gvr.Cells[sortColumnIndex].Controls[0] is CheckBox)
{
if (((CheckBox)gvr.Cells[sortColumnIndex].Controls[0]).Checked)
currentValue = "Yes";
else
currentValue = "No";
}
// ... Add other checks here if using columns with other
// Web controls in them (Calendars, DropDownLists, etc.) ...
}
else
currentValue = gvr.Cells[sortColumnIndex].Text;
此程式碼檢查目前行的排序列 TableCell
以確定 Controls
集合中是否有任何控制項。 如果有,且第一個控制項是 CheckBox,則 currentValue
變數將設定為「是」或「否」,取決於 CheckBox 的 Checked
屬性。 否則,該值取自 TableCell
的 Text
屬性。 可以複製此邏輯來處理 GridView 中可能存在的任何 TemplateField 的排序。
透過新增上述程式碼,現在按 Discontinued CheckBoxField 排序時會出現規則群組標題 (請參閱圖 7)。
圖 7:對 CheckBoxField 進行排序時,現在會出現排序群組標題 (點擊查看完整圖片)
注意
如果您的產品的 CategoryID
、SupplierID
或 UnitPrice
欄位具有 NULL
資料庫值,則預設情況下這些值將在 GridView 中顯示為空字串,這表示具有 NULL
值的那些產品的分隔符號列文字將如下所示:是的,「類別:」之後沒有名稱,就像「類別:飲料」一樣)。 如果您希望在此處顯示一個值,您可以將 BoundFields NullDisplayText
屬性 設為您想要顯示的文字,也可以在將 currentValue
指派給分隔符號列的 Text
屬性時在 Render 方法中新增條件陳述式。
摘要
GridView 不包含許多用於自訂排序介面的內建選項。 但是,透過一些低階程式碼,可以調整 GridView 的控制項階層以建立更自訂的介面。 在本教學課程中,我們了解如何為可排序的 GridView 新增規則群組分隔符號列,這可以更輕鬆地識別不同的群組和這些群組的邊界。 有關自訂排序介面的其他範例,請查看 Scott Guthrie 的「A Few ASP.NET 2.0 GridView Sorting Tips and Tricks」(一些 ASP.NET 2.0 GridView 排序技巧和竅門) 部落格文章。
快樂程式!
關於作者
Scott Mitchell 是七本 ASP/ASP.NET 書籍的作者,也是 4GuysFromRolla.com 的創辦人,自 1998 年以來一直在使用 Microsoft 網路技術。 Scott 是一位獨立顧問、培訓師兼作家。 他的最新著作是 Sams Teach Yourself ASP.NET 2.0 in 24 Hours。 您可以透過電子郵件聯繫他:mitchell@4GuysFromRolla.com。 或者透過他的部落格與他聯繫,網址為 http://ScottOnWriting.NET。