2013年10月10日 星期四

ASP.NET MVC 使用 jQuery EasyUI DataGrid - 多欄排序 (Multiple Column Sorting) Part.1

上一篇「ASP.NET MVC 使用 jQUery EasyUI DataGrid - 排序 (Sorting)」說明了如何讓 DataGrid 加入資料排序的功能,而 jQuery EasyUI 1.3.4 有提供了一個新的屬性「multiSort」,這個屬性可以讓 DataGrid 有多欄排序的功能,之前的排序都是針對某一個欄位做排序顯示,而多欄排序是可以同時選擇多個不同欄位,而且欄位的排序順序可以不同,這一篇文章就來說明要如何處理多欄排序的操作。

 


要讓 jQuery EasyUI DataGrid 有多欄排序的功能,在 DataGrid 的設定裡增加「multiSort」屬性然後設定為 true,

image

增加了「multiSort: true」屬性設定後,我們就可以選擇多個欄位做資料的排序,

image

觀察 POST 分頁與排序資料到後端 Controller 的內容,如下:

image

可以看到 order 與 sort 將有選擇排序的欄位與順序以逗點分隔的格式將資料傳送到 Controller,不過我們還沒有對 CustomerService 的程式作修改,而因應多欄排序的需求,將對原本的程式作適度的調整。

 

Step.1

多欄排序的操作是不必修改 CustomerController 的程式內容,但是需要對 CustomerService 的 GetJsonForGrid method 的程式做修改,排序欄位名稱與排序順序是依照先後順序成對排列的,所以我們需要動態的去增加 OrderBy 與 ThenBy 的 LINQ Expression,另外原本為了防止傳進來的 propertyName 與 oder 資料有錯而增加的資料判斷就必須做修改。

所以我們要先來解決 PropertyName 與其對應的 oder 成對的部分,在 EntityHelper.cs 裡增加了 GetPropertySortTuples<T> 方法,

/// <summary>
/// Gets the property sort tuples.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="propertyName">Name of the property.</param>
/// <param name="order">The order.</param>
/// <returns></returns>
public static List<Tuple<string, string>> GetPropertySortTuples<T>(
    string propertyName,
    string order)
{
    var result = new List<Tuple<string, string>>();
 
    //取得 Entity 所有的 Property 名稱
    var entityPropertyNames = EntityHelper.EntityPropertyNames<T>();
 
    var propertyNameOptions = propertyName.Split(',').ToList();
    var orderOptions = order.Split(',').ToList();
 
    for (int i = 0; i < propertyNameOptions.Count; i++)
    {
        var columnName = propertyNameOptions[i].Trim();
        var sortOrder = string.IsNullOrWhiteSpace(orderOptions[i]) ? "asc" : orderOptions[i];
 
        var propertyNames = entityPropertyNames as string[] ?? entityPropertyNames.ToArray();
        if (!propertyNames.Contains(columnName))
        {
            columnName = string.Empty;
        }
        if (!(sortOrder.Equals("asc", StringComparison.OrdinalIgnoreCase)
              || sortOrder.Equals("desc", StringComparison.OrdinalIgnoreCase)))
        {
            order = "asc";
        }
 
        if (!string.IsNullOrEmpty(columnName))
        {
            result.Add(new Tuple<string, string>(columnName, sortOrder));
        }
    }
    return result;
}

如果有多個排序欄位時,所得到的結果就會如下圖裡的內容,

image

 

Step.2

解決了多個排序欄位的 Property 與 Sort Order 對應之後,接著就是要去使用這個資料來去動態組合 LINQ Expression 的 Sort Expresion 部分。

之前我們使用了 Dynamic Expression API 來解決單一欄位排序的動態 Sort Expression 需求,如下:

image

正當我想要用下面的方式來動態組合 Sort Expression 部份的時候,卻看到 Dynamic Expression API 沒有提供 ThenBy 方法,

image

image

沒有支援 ThenBy 的處理就問題大了,因為多欄位排序一定要使用到 ThenBy,第一個欄位使用 OrderBy 或 OrderByDescending,那麼接續的排序就需要用到 ThenBy 或 ThenByDescending,不然多欄位排序的功能就無法實做出來,所以這邊我們就無法繼續使用 Dynamic Expression API 來完成,所以必須要另外想辦法。

註:
Dynamic Expression API 不是不支援多重排序,而是沒有提供 ThenBy 與 ThenByDescending 的方法,而至於要如何使用 Dynamic Expression API 操作都多欄位排序的處理,在之後的文章裡將會做說明。

我還在寫這篇文章的時候(這一篇寫了三天),我正當煩惱無法用比較清楚的方式來說明有關 OrderBy 與 ThenBy 的時候,微軟 MVP – 91 就發佈了一篇有關 OrderBy, ThenBy 的文章:

[.NET]快快樂樂學LINQ系列-OrderBy(), ThenBy() 簡介

此文章裡詳述了 IOrderedEnumerable<T>, OrderBy, ThenBy 以及多重排序的內容,所以請各位務必先看過 91 哥的文章,如此後續的操作才不致於看得模模糊糊的。

 

Step.3

確定無法使用 Dynamic Expression API 來解決多欄位排序的需求後,就直接從 LINQ Expression 下手,而我們先看 MSDN 裡 IOrderedEnumerable<T> 的說明:

MSDN - IOrderedEnumerable(TElement) 介面 (System.Linq)

摘錄其中的說明:

擴充方法 ThenBy 和 ThenByDescending 會在 IOrderedEnumerable<TElement> 型別物件上操作。 藉由呼叫其中一個主要排序方法 (OrderBy 或 OrderByDescending),傳回 IOrderedEnumerable<TElement>,即可取得 IOrderedEnumerable<TElement> 型別物件。 而 ThenBy 和 ThenByDescending 次要排序方法會傳回 IOrderedEnumerable<TElement> 型別物件。 這個設計允許對 ThenBy 或 ThenByDescending 進行任意數目的連續呼叫,而每個呼叫都會在上一個呼叫所傳回的排序資料上執行次要排序。

所以我們要把 Step.1 裡從 EntityHelper.GetPropertySortTuples<T>() 所取得的 propertySortTuples 去想辦法產生 IOrderedEnumerable<T> 的結果,而這一部份的程式我是取用「Sorting in IQueryable using string as column name | Jiří {x2} Činčura」這一篇文章裡的作法,稍做調整後的程式如下:

IOrderedQueryableHelper.cs

public static class IOrderedQueryableHelper
{
    /// <summary>
    /// Orders the by.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="source">The source.</param>
    /// <param name="propertyName">Name of the property.</param>
    /// <returns></returns>
    public static IOrderedQueryable<T> OrderBy<T>(this IQueryable<T> source,
        string propertyName)
    {
        return OrderingHelper(source, propertyName, false, false);
    }
 
    /// <summary>
    /// Orders the by descending.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="source">The source.</param>
    /// <param name="propertyName">Name of the property.</param>
    /// <returns></returns>
    public static IOrderedQueryable<T> OrderByDescending<T>(this IQueryable<T> source,
        string propertyName)
    {
        return OrderingHelper(source, propertyName, true, false);
    }
 
    /// <summary>
    /// Thens the by.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="source">The source.</param>
    /// <param name="propertyName">Name of the property.</param>
    /// <returns></returns>
    public static IOrderedQueryable<T> ThenBy<T>(this IOrderedQueryable<T> source,
        string propertyName)
    {
        return OrderingHelper(source, propertyName, false, true);
    }
 
    /// <summary>
    /// Thens the by descending.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="source">The source.</param>
    /// <param name="propertyName">Name of the property.</param>
    /// <returns></returns>
    public static IOrderedQueryable<T> ThenByDescending<T>(this IOrderedQueryable<T> source,
        string propertyName)
    {
        return OrderingHelper(source, propertyName, true, true);
    }
 
    /// <summary>
    /// Orderings the helper.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="source">The source.</param>
    /// <param name="propertyName">Name of the property.</param>
    /// <param name="descending">if set to <c>true</c> [descending].</param>
    /// <param name="anotherLevel">if set to <c>true</c> [another level].</param>
    /// <returns></returns>
    private static IOrderedQueryable<T> OrderingHelper<T>(IQueryable<T> source,
        string propertyName,
        bool descending,
        bool anotherLevel)
    {
        ParameterExpression param = Expression.Parameter(typeof(T), string.Empty);
        MemberExpression property = Expression.PropertyOrField(param, propertyName);
        LambdaExpression sort = Expression.Lambda(property, param);
 
        MethodCallExpression call = Expression.Call(
            typeof (Queryable),
            string.Concat((!anotherLevel ? "OrderBy" : "ThenBy"), (descending ? "Descending" : string.Empty)),
            new[] {typeof (T), property.Type},
            source.Expression,
            Expression.Quote(sort));
 
        return (IOrderedQueryable<T>)source.Provider.CreateQuery<T>(call);
    }
 
 
}

 

Step.4

建立好 IOrderedQueryableHelper.cs 之後,我們就可以在 CustomerService 的 GetJsonForGrid 方法裡使用,如下:

image

CustomerService 的 GetJsonForGrid 方法的完整程式如下:

public JArray GetJsonForGrid(
    int page = 1,
    int pageSize = 10,
    string propertyName = "CustomerID",
    string order = "asc")
{
    // 取得多個排序欄位與順序的 Tuple 結果
    var propertySortTuples =
        EntityHelper.GetPropertySortTuples<Customer>(propertyName, order);
 
    JArray ja = new JArray();
 
    var query = db.Customers.AsQueryable();
 
    IOrderedQueryable<Customer> orderQuery = null;
 
    for (int index = 0; index < propertySortTuples.Count; index++)
    {
        if (propertySortTuples[index].Item2.Equals("asc", StringComparison.OrdinalIgnoreCase))
        {
            orderQuery = index.Equals(0)
                ? query.OrderBy<Customer>(propertySortTuples[index].Item1)
                : orderQuery.ThenBy<Customer>(propertySortTuples[index].Item1);
        }
        else

{

            orderQuery = index.Equals(0)
                ? query.OrderByDescending<Customer>(propertySortTuples[index].Item1)
                : orderQuery.ThenByDescending<Customer>(propertySortTuples[index].Item1);
        }
    }
    query = orderQuery;
    query = query.Skip((page - 1) * pageSize).Take(pageSize);
 
    foreach (var item in query)
    {
        var itemObject = new JObject
        {
            {"CustomerID", item.CustomerID},
            {"CompanyName", item.CompanyName},
            {"ContactName", item.ContactName},
            {"ContactTitle", item.ContactTitle},
            {"Address", item.Address},
            {"City", item.City},
            {"Region", item.Region},
            {"PostalCode", item.PostalCode},
            {"Country", item.Country},
            {"Phone", item.Phone},
            {"Fax", item.Fax}
        };
        ja.Add(itemObject);
    }
    return ja;
}

 

執行結果:

首先只選擇 Country 欄位進行排序,

image

image

 

然後再選擇 City 欄位進行排序,

image

image

 

再點選 City 欄位以進行降冪排序,可以看到多重排序的結果,先對 Country 做升冪排序然後再做 City 的降冪排序,

image

image

從 EF 所產出的 SQL Command 就可以清楚看到確實有進行多個欄位的且順序不同的排序,

SELECT TOP (10) 
[Extent1].[CustomerID] AS [CustomerID], 
[Extent1].[CompanyName] AS [CompanyName], 
[Extent1].[ContactName] AS [ContactName], 
[Extent1].[ContactTitle] AS [ContactTitle], 
[Extent1].[Address] AS [Address], 
[Extent1].[City] AS [City], 
[Extent1].[Region] AS [Region], 
[Extent1].[PostalCode] AS [PostalCode], 
[Extent1].[Country] AS [Country], 
[Extent1].[Phone] AS [Phone], 
[Extent1].[Fax] AS [Fax]
FROM ( SELECT [Extent1].[CustomerID] AS [CustomerID], [Extent1].[CompanyName] AS [CompanyName], [Extent1].[ContactName] AS [ContactName], [Extent1].[ContactTitle] AS [ContactTitle], [Extent1].[Address] AS [Address], [Extent1].[City] AS [City], [Extent1].[Region] AS [Region], [Extent1].[PostalCode] AS [PostalCode], [Extent1].[Country] AS [Country], [Extent1].[Phone] AS [Phone], [Extent1].[Fax] AS [Fax], row_number() OVER (ORDER BY [Extent1].[Country] ASC, [Extent1].[City] DESC) AS [row_number]
    FROM [dbo].[Customers] AS [Extent1]
)  AS [Extent1]
WHERE [Extent1].[row_number] > 10
ORDER BY [Extent1].[Country] ASC, [Extent1].[City] DESC

 

操作動畫:

jquery_easyui_datagrid_sorting_multiple

 


好像到這邊就講完了多欄位排序的處理,有需要分成兩篇文章嗎?

其實還有後續的處理以及程式的修改,為避免文章過長,就拆成兩篇做說明,基本上多欄位排序的功能已經完成了。

 

參考連結:

MSDN - IOrderedEnumerable(TElement) 介面 (System.Linq)

[.NET]快快樂樂學LINQ系列-OrderBy(), ThenBy() 簡介 - In 91- 點部落

Sorting in IQueryable using string as column name | Jiří {x2} Činčura

Multiple Field Sorting by Field Names Using Linq - CodeProject

 

以上

沒有留言:

張貼留言

提醒

千萬不要使用 Google Talk (Hangouts) 或 Facebook 及時通訊與我聯繫、提問,因為會掉訊息甚至我是過了好幾天之後才發現到你曾經傳給我訊息過,請多多使用「詢問與建議」(在左邊,就在左邊),另外比較深入的問題討論,或是有牽涉到你實作程式碼的內容,不適合在留言板裡留言討論,請務必使用「詢問與建議」功能(可以夾帶檔案),謝謝。

最近的留言