網頁

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

 

以上

沒有留言:

張貼留言