基于业务对象的排序

引言

在上一篇文章 基于业务对象的筛选 中,我们讨论了如何实现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();     // 对列表进行排序
     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这样的代码是枯燥无味的,以后我们再一起看看如果利用反射来编写一个小程序为我们自动地生成这些代码。

感谢阅读,希望这篇文章能给你带来帮助。