基于業務對象的排序
引言
在上一篇文章
基于業務對象的篩選中,我們讨論了如何實作Predicate<T>(T object)委托,自定義DateFilter 類來對業務對象進行篩選。與篩選一樣,排序也是常見且重要的操作。在對業務對象進行排序時,不能使用ObjectDataSource作為資料源,因為它隻對 DataView、DataTable 和 DataSet 支援自動排序。但你仍可以對GridView編寫Sorting事件的處理方法,通過拼裝SQL語句,使用“Order By”子句來完成排序。
和進行篩選的思路一樣,如果我們将業務對象緩存在伺服器上,第一次通路時從資料庫提取資料,然後進行緩存,後繼的請求隻針對緩存了的業務對象進行,則可以降低對資料庫的依賴,提高效率。本文将讨論如何對擷取的業務對象進行排序,包括簡單排序、任意列排序、以及多列複合排序。
NOTE:本文是接着上一篇寫的,一些重複的内容本文将不再講述,建議先閱讀
。
簡單排序 - 對固定屬性的預設排序
與上篇文章不同,我不再說明使用拼裝SQL來完成排序的方式,我們直接看基于List<Order>對象的排序。我們知道List<T>提供了Sort()方法來進行排序操作,那麼它又如何使用呢?我們先建立一個ObjSort.aspx檔案,然後在代碼後置中添加如下代碼:
protected void Page_Load(object sender, EventArgs e)
{
Label lb1 = new Label();
List<int> list = new List<int>();
list.Add(4);
list.Add(5);
list.Add(2);
list.Add(9);
list.Add(1);
foreach (int item in list) {
lb1.Text += item.ToString() + ", ";
}
form1.Controls.Add(lb1);
HtmlGenericControl hr = new HtmlGenericControl("hr");
form1.Controls.Add(hr);
Label lb2 = new Label();
list.Sort(); // 對清單進行排序
lb2.Text += item.ToString() + ", ";
form1.Controls.Add(lb2);
}
可以看到,通過在List<int>上使用Sort()方法,對清單中的元素進行了排序。現在我們在OrderManager.cs中新添一個方法GetSortList(),它用于擷取清單對象,因為GetList()方法傳回的記錄數太多,而在本文中我們僅關注排序,是以我們僅傳回15條記錄。
// 擷取用于排序的清單
public static List<Order> GetSortList() {
List<Order> list = HttpContext.Current.Cache["sortList"] as List<Order>;
if (list == null) {
list = GetList("Select Top 15 OrderId, CustomerId, ShipCountry, OrderDate From Orders");
HttpContext.Current.Cache.Insert("sortList", list);
}
return list;
}
如果你沒有看上一篇文章,那麼隻要知道這個方法傳回一個List<Order>類型的業務對象,代表一個訂單清單就可以了(Order對象包含四個公共屬性,分别是OrderId, CustomerId, OrderDate, Country)。然後我們建立 ObjSort2.aspx檔案,在它上面拖放一個Reperter控件,并編寫一些代碼,用于顯示一個表格:
<asp:Repeater runat="server" ID="rpOrderList" >
<HeaderTemplate>
<table>
<tr>
<th>
<asp:LinkButton ID="lbtOrderId" runat="server">OrderId</asp:LinkButton>
</th>
<asp:LinkButton ID="lbtCustomerId" runat="server">CustomerId</asp:LinkButton>
<asp:LinkButton ID="lbtOrderDate" runat="server">OrderDate</asp:LinkButton>
<th>
<asp:LinkButton ID="lbtCountry" runat="server">Country</asp:LinkButton>
</tr>
</HeaderTemplate>
<ItemTemplate>
<td><%#Eval("OrderId") %></td>
<td><%#Eval("CustomerId") %></td>
<td><%#Eval("OrderDate") %></td>
<td><%#Eval("Country") %></td>
</ItemTemplate>
<FooterTemplate>
</table>
</FooterTemplate>
</asp:Repeater>
然後,我們在後置代碼ObjSort2.aspx.cs的Page_Load事件中,添加這樣兩行語句:
rpOrderList.DataSource = OrderManager.GetSortList();
rpOrderList.DataBind();
然後再打開頁面,可以看到在頁面上輸出了清單。現在我們想對這個清單進行排序,那麼我們仿照List<int>的做法,修改上面的代碼:
List<Order> list = OrderManager.GetSortList();
list.Sort(); // 期望可以進行排序
rpOrderList.DataSource = list;
實際上,我們會得到錯誤:必須至少有一個對象實作 IComparable。
IComparable<T>接口
我們就是自己想也應該想到為什麼會出錯:Order對象包含了四個屬性OrderId、CustomerId、OrderDate、Country,而int隻有它本身的值,是以,當我們在List<Order>上調用Sort()的時候,清單對象根本不知道應該如何排序,也不知道以哪個屬性來進行排序。而IComparable接口,定義了如何進行排序的規則,如果我們想要對List<Order>對象進行排序,那麼我們就需要讓清單的元素,也就是Order對象實作這個接口。實際上,List<int>之是以可以直接調用Sort()方法,是因為int,以及幾乎全部的基本類型(比如string,char,datetime等),本身就實作了IComparable<T>。
public interface IComparable<T> {
int CompareTo(T other);
這個接口隻需要實作一個方法,CompareTo(),它傳遞與要比較的對象(清單中的目前對象)同類型的另一個對象 other,傳回一個int類型的值:小于零 目前對象小于 other 參數。零 此對象等于 other。大于零 目前對象大于 other。
現在我們讓Order對象(Order參見下載下傳的代碼)實作這個接口:
// 實作 IComparable<T> 接口
public int CompareTo(Order other) {
return this.CustomerId.CompareTo(other.CustomerId);
我們将排序的規則委托給了CustomerId去處理,因為CustomerId是一個string類型,調用了它的CompareTo()方法。這樣,在List<Order>上調用Sort()的時候就會依據這裡定義的規則,以CustomerId進行排序了。再次打開ObjSort.aspx,應該可以看到清單按CustomerId進行了排序。
進階排序 - 多個屬性組合排序
IComparer<T> 接口
上面僅僅是為清單提供了一個預設排序,實際上,我們經常要求對多個列進行排序,我們還會要求按降序或者升序進行排序,我們甚至會要求對多個列的組合進行排序,比如:先對CustomerId進行升序排列,再對OrderDate降序排列。此時雖然使用CompareTo(Order other)也可以實作,但是要給Order對象添加額外的字段或者屬性,這些.Net Framewok已經考慮到了,并提供了IComparer<T>接口封裝了排序規則,我們可以通過實作這個接口來完成排序。
public interface IComparer<T> {
int Compare(T x, T y);
IComparer<T>隻需要實作一個方法,Compare()它接受兩個同一類型的參數,并傳回int類型的結果,與IComparable<T>類似,當傳回值小于0時,x小于y;等于0時,x等于y;大于0時,x大于y。需要注意的是:這個接口不是要求我們讓Order對象實作它,而是要求另外一個對象實作它,比如OrderComparer,而在調用Sort()方法時,将它作為參數傳遞進去。因為這個OrderComparer隻是用于對Order對象進行排序,不能應用于其他對象,是以我們将它聲明為Order的嵌套類。
實作 IComparer<T>接口
打開Order.cs檔案,對它進行如下修改,先添加一個枚舉SortDirection,用于表示排序的方向:
// 可複用的枚舉,表示排序的方向
public enum SortDirection {
Ascending = 0,
Descending
在Order類的内部,添加一個枚舉,這個枚舉類型代表了可以進行排序的屬性:
// 嵌套枚舉,僅應用于此業務對象,可排序的屬性
public enum SortField {
OrderId,
CustomerId,
OrderDate,
Country
我們還需要再定義一個結構Sorter,這個結構包含兩個字段,一個SortDirection類型,一個SortField類型,它封裝了排序的必要資訊:對于哪個屬性按照哪種方式(升序或降序)排序。由于這個結構依然是隻針對Order對象的,是以我們還是把它定義在Order内部:
// 嵌套結構,僅應用于此業務對象,排序的屬性和方式
public struct Sorter {
public SortField field;
public SortDirection direction;
public Sorter(SortField field, SortDirection direction) {
this.field = field;
this.direction = direction;
public Sorter(SortField field) {
this.direction = SortDirection.Ascending;
接着,我們在Order内部定義實作IComparer<T>的類OrderComparer:
// 嵌套類,僅對于此業務對象進行排序
public class OrderComparer : IComparer<Order> {
現在考慮如何實作它:因為我們要實作對某個屬性,按某種方式排序,那麼我們至少要将這兩個參數傳進去,是以OrderCompare應該包含字段用于維護SortDirection和SortField;因為我們期望可以對多個屬性組合排序,是以應該維護一個它們的清單,而SortDirection和SortFiled,已經包含在了Sorter結構中,是以它隻要維護一個List<Sorter>結構就可以了:
private List<Sorter> list;
// 構造函數,設定排序字段清單
public OrderComparer(List<Sorter> list) {
this.list = list;
接着考慮如何排序,先從簡單入手,我們不考慮對于多個屬性的排序,隻對某個屬性按某種方式排序,那麼我們需要添加一個方法CompareTo(),它接受排序的屬性、排序的方式,以及排序的兩個對象,最後傳回int類型,說明這兩個對象的大小(位置的先後):
// 對單個屬性按某種方式進行排序
public int Compare(Order x, Order y, SortField field, SortDirection direction) {
int result = 0; // 預設排序位置不變化
switch (field) {
case SortField.Country:
if (direction == SortDirection.Ascending)
result = x.Country.CompareTo(y.Country);
else
result = y.Country.CompareTo(x.Country);
break;
case SortField.CustomerId:
result = x.CustomerId.CompareTo(y.CustomerId);
result = y.CustomerId.CompareTo(x.CustomerId);
case SortField.OrderDate:
result = x.OrderDate.CompareTo(y.OrderDate);
result = y.OrderDate.CompareTo(x.OrderDate);
case SortField.OrderId:
result = x.OrderId.CompareTo(y.OrderId);
result = y.OrderId.CompareTo(x.OrderId);
return result;
但是這個方法不會實作IComparer<T>接口,也沒有辦法進行多個列的排序。繼續進行之前,我們考慮下如何對兩個對象的多個屬性(比如A、B、C)來進行排序:先對屬性A進行比較,如果屬性A相同,繼續比較屬性B,如果屬性B相同,繼續比較屬性C。在這個過程中,隻要有任意一個屬性不相同,就可以決定兩個對象的先後順序,也就是不再進行後面屬性的比較。
有了思路,我們現在實作IComparer<T>接口,編寫方法
// 實作 IComparer接口
public int Compare(Order x, Order y) {
int result = 0;
foreach (Sorter item in list) {
result = Compare(x, y, item.field, item.direction);
if (result != 0) // 一旦result不為0,則已經區分出位置大小,跳出循環
在這個方法中,我們周遊了List<Sorter>,并且在foreach語句中調用了我們前面定義的對單個屬性的比較方法Compare(Order x, Order y, SortField field, SortDirection direction),一旦比較結果不為0,那麼就跳出循環。
好了OrderComparer類的實作已經完成了,我們再看下還有什麼可以完善的地方:如果以後每次調用Sort進行排序的時候,都要先需要先建立清單,指定排序規則,構造OrderCompare對象,顯然會很麻煩,是以我們給在Book類中添加一組重載了的方法GetComparer(),用來簡化以後調用時的操作步驟:
// 指定排序屬性 和 排序方式
public static OrderComparer GetComparer(SortField field, SortDirection direction) {
List<Sorter> list = new List<Sorter>();
Sorter sorter = new Sorter(field, direction);
list.Add(sorter);
return new OrderComparer(list);
// 指定排序屬性,預設為升序
public static OrderComparer GetComparer(SortField field) {
return new OrderComparer(field, SortDirection.Ascending);
// 預設為以OrderId升序排列
public static OrderComparer GetComparer() {
return new OrderComparer(SortField.OrderId, SortDirection.Ascending);
// 排序清單
public static OrderComparer GetComparer(List<Sorter> list) {
好了,現在OrderComparer類就全部建立好了,我們接下來看一下如何使用它:
頁面調用
我們修改一下代碼後置檔案,來看下如何進行設定,我們将Sort()改成這樣:
list.Sort(Order.GetComparer(Order.SortField.Country, SortDirection.Descending)); // 以Country倒序排列
然後檢視頁面,發現清單以Country屬性進行了倒序排列。那如果我們想先按Country倒序排列,再按CustomerId順序排列,又該如何呢?
// 以Country降序, CustomerId升序排列
List<Order.Sorter> sorterList = new List<Order.Sorter>(); //先建立sorterList
sorterList.Add(new Order.Sorter(Order.SortField.Country, SortDirection.Descending)); // 先以Country Desc排序
sorterList.Add(new Order.Sorter(Order.SortField.CustomerId));//再以CustomerId Asc
list.Sort(Order.GetComparer(sorterList));
現在打開頁面,可以看到清單以我們期望的方式進行了排序。在本文中,由于僅僅是出于示範的目的,是以我們在代碼中直接書寫了用于排序的SortList,實際上這些應該是基于使用者選擇而動态建立的。在ObjSort2.aspx頁面上,表格的标題我使用了LinkButton,有興趣的話可以編寫LinkButton的Click事件,來動态地實作這一排序過程。
總結
本文詳細的讨論了如何對清單(業務對象)進行排序。
我們首先了解IComparable<T>接口,學習了如何實作這個接口以實作針對某一字段的一個預設排序。接着,我們詳細地讨論了如何通過實作一個IComparer<T>接口,來實作可以對任意單個屬性以及多個屬性組合的排序。
大家可以看到,一旦掌握了方法以後,再編寫諸如OrderComparer這樣的代碼是枯燥無味的,以後我們再一起看看如果利用反射來編寫一個小程式為我們自動地生成這些代碼。
感謝閱讀,希望這篇文章能給你帶來幫助。