基於業務對象(列表)的排序

基於業務對象(列表)的排序

2008-3-21 作者: 張子陽 分類: 設計與模式

引言

在上一篇文章 基於業務對象的篩選 中,我們討論瞭如何實現Predicate<T>(T object)委託,自定義DateFilter 類來對業務對象進行篩選。與篩選一樣,排序也是常見且重要的操作。在對業務對象進行排序時,不能使用ObjectDataSource作爲數據源,因爲它只對 DataView、DataTable 和 DataSet 支持自動排序。但你仍可以對GridView編寫Sorting事件的處理方法,通過拼裝SQL語句,使用“Order By”子句來完成排序。

和進行篩選的思路一樣,如果我們將業務對象緩存在服務器上,第一次訪問時從數據庫提取數據,然後進行緩存,後繼的請求只針對緩存了的業務對象進行,則可以降低對數據庫的依賴,提高效率。本文將討論如何對獲取的業務對象進行排序,包括簡單排序、任意列排序、以及多列複合排序。

本文是接着上一篇寫的,一些重複的內容本文將不再講述,建議先閱讀 基於業務對象的篩選

簡單排序 - 對固定屬性的默認排序

與上篇文章不同,我不再說明使用拼裝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();     // 對列表進行排序
     foreach (int item in list) {
        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>
            <th>
               <asp:LinkButton ID="lbtCustomerId" runat="server">CustomerId</asp:LinkButton>
            </th>
            <th>
               <asp:LinkButton ID="lbtOrderDate" runat="server">OrderDate</asp:LinkButton>
            </th>
             <th>
                <asp:LinkButton ID="lbtCountry" runat="server">Country</asp:LinkButton>
            </th>
        </tr>
    </HeaderTemplate>
    <ItemTemplate>
        <tr>
            <td><%#Eval("OrderId") %></td>
            <td><%#Eval("CustomerId") %></td>
            <td><%#Eval("OrderDate") %></td>
             <td><%#Eval("Country") %></td>
        </tr>
    </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;
rpOrderList.DataBind();

實際上,我們會得到錯誤:必須至少有一個對象實現 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.field = field;
       this.direction = SortDirection.Ascending;
    }
}

接着,我們在Order內部定義實現IComparer<T>的類OrderComparer:

// 嵌套類,僅對於此業務對象進行排序 
public class OrderComparer : IComparer<Order> { 

}

現在考慮如何實現它:因爲我們要實現對某個屬性,按某種方式排序,那麼我們至少要將這兩個參數傳進去,所以OrderCompare應該包含字段用於維護SortDirection和SortField;因爲我們期望可以對多個屬性組合排序,所以應該維護一個它們的列表,而SortDirection和SortFiled,已經包含在了Sorter結構中,所以它只要維護一個List<Sorter>結構就可以了:

public class OrderComparer : IComparer<Order> {
    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:
           if (direction == SortDirection.Ascending)
              result = x.CustomerId.CompareTo(y.CustomerId);
           else
              result = y.CustomerId.CompareTo(x.CustomerId);
           break;
       case SortField.OrderDate:
           if (direction == SortDirection.Ascending)
              result = x.OrderDate.CompareTo(y.OrderDate);
           else
              result = y.OrderDate.CompareTo(x.OrderDate);
           break;
       case SortField.OrderId:
           if (direction == SortDirection.Ascending)
              result = x.OrderId.CompareTo(y.OrderId);
           else
              result = y.OrderId.CompareTo(x.OrderId);
           break;
    }

    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,則已經區分出位置大小,跳出循環
           break;
    }

    return result;
}

在這個方法中,我們遍歷了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) {
    return new OrderComparer(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這樣的代碼是枯燥無味的,以後我們再一起看看如果利用反射來編寫一個小程序爲我們自動地生成這些代碼。

感謝閱讀,希望這篇文章能給你帶來幫助!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章