2012年6月29日 星期五

Dynamic LINQ + Entity Framework - Part.4:ASP.NET MVC 進階應用

 

其實這一篇原本要寫在「Dynamic LINQ + Entity Framework - Part.3:ASP.NET MVC 應用」的最後面,

但是那一篇文章已經是長長一大篇了,為了避免大家看到睡著或是沒耐心看完全部就離開,

所以就把後面要接續寫的內容給放到這一篇文章裡,

這一篇的進階應用會多加一些功能以及調整一些顯示的方式,都可以應用在實務專案裡。

 



需求

上一篇「Dynamic LINQ + Entity Framework - Part.3:ASP.NET MVC 應用」的查詢條件只是用下拉選單來處理,

而且還只有一個「City」的過濾條件而已,所以這一次我們就讓使用者可以選擇欄位並輸入關鍵字來做過濾查詢,

另外我們已經有做資料排序欄位以及排序類型的選擇,但是在頁面上並不能夠明顯地看出來是用哪一個欄位做何種排序,

所以這次我們頁面所顯示的 Table 上,在表格標題列上面做比較明顯的顯示 ,

另外一開始進入頁面的時候,不載入任何的資料,只有送出查詢表單的欄位資料後才顯示查詢後的查詢結果資料。

 

首先來看看一開始的頁面長得是什麼樣子…

image

 

Controller > Test3

public ActionResult Test3()
{
    this.PrepareDropDownLists("all", "CustomerID", "asc");
    this.SearchSolumnDropDownList("");
    ViewData.Model = new List<Customer>();
    ViewBag.Keyword = string.Empty;
    ViewBag.SortColumnName = "CustomerID";
    ViewBag.SortType = "asc";
    ViewBag.DisplaySortColumnStyle = new Func<string, string, string, string>(DisplaySortColumnStyle);
    return View();
}

一開始的顯示頁面並不顯任何的資料,但是頁面上的欄位所需要的資料還是要做處理,

以下的 Method 內容與上一篇「Dynamic LINQ + Entity Framework - Part.3:ASP.NET MVC 應用」的內容有些不同,

PrepareDropDownLists() 這個 Method 就是用來準備頁面裡面的下拉選單資料,

#region -- PrepareDropDownLists --
/// <summary>
/// Prepares the drop dwon lists.
/// </summary>
/// <param name="city">The city.</param>
/// <param name="sortColumnName">Name of the column.</param>
/// <param name="sort">The sort.</param>
private void PrepareDropDownLists(string city, string sortColumnName, string sort)
{
    using (NorthwindEntities db = new NorthwindEntities())
    {
        //City DropDownList
        var cities = db.Customers.Select(x => x.City).OrderBy(x => x).Distinct();
 
        Dictionary<string, string> cityDict = new Dictionary<string, string>();
        cityDict.Add("ALL Cities", "all");
        foreach (var item in cities)
        {
            cityDict.Add(item, item);
        }
        ViewData["CityDDL"] = DropDownListHelper.GetDropdownList
        (
            "city",
            "city",
            cityDict,
            new { id = "city" },
            string.IsNullOrWhiteSpace(city) ? "all" : city,
            false,
            ""
        );
 
        PrepareSortColumn(sortColumnName, db);
        PrepareSortType(sort);
    }
}
 
#endregion

另外把「排序欄位下拉選單」「排序類型下拉選單」給獨立出來,

#region -- PrepareSortColumn --
/// <summary>
/// Prepares the sort column.
/// </summary>
/// <param name="sortColumnName">Name of the column.</param>
/// <param name="db">The db.</param>
private void PrepareSortColumn(string sortColumnName, NorthwindEntities db)
{
    Dictionary<string, string> columnDict = new Dictionary<string, string>();
 
    var columns = this.GetPrimitiveEdmMemberNames(db.Customers.EntitySet.ElementType);
    foreach (var item in columns)
    {
        columnDict.Add(item, item);
    }
 
    ViewData["ColumnDDL"] = DropDownListHelper.GetDropdownList
    (
        "sortColumnName",
        "sortColumnName",
        columnDict,
        new { id = "sortColumnName" },
        string.IsNullOrWhiteSpace(sortColumnName) ? "CustomerID" : sortColumnName,
        false,
        ""
    );
}
 
private IEnumerable<EdmMember> GetEntityProperties(EntityTypeBase entityType)
{
    return entityType.Members.Select(x => x);
}
 
private IEnumerable<string> GetEntityPropertyNames(EntityTypeBase entityType)
{
    return GetEntityProperties(entityType).Select(x => x.Name);
}
 
private IEnumerable<EdmMember> GetPrimitiveEdmMembers(EntityTypeBase entityType)
{
    var properties = GetEntityProperties(entityType);
 
    return properties
        .Where(x => x.TypeUsage.EdmType.BuiltInTypeKind == BuiltInTypeKind.PrimitiveType)
        .Select(x => x);
}
 
private IEnumerable<string> GetPrimitiveEdmMemberNames(EntityTypeBase entityType)
{
    return GetPrimitiveEdmMembers(entityType).Select(x => x.Name);
}
 
#endregion
 
#region -- PrepareSortType --
/// <summary>
/// Prepares the type of the sort.
/// </summary>
/// <param name="sort">The sort.</param>
private void PrepareSortType(string sort)
{
    Dictionary<string, string> sortDict = new Dictionary<string, string>();
    sortDict.Add("ASC", "asc");
    sortDict.Add("DESC", "desc");
    ViewData["SortDDL"] = DropDownListHelper.GetDropdownList
    (
        "sort",
        "sort",
        sortDict,
        new { id = "sort" },
        string.IsNullOrWhiteSpace(sort) ? "asc" : sort,
        false,
        ""
    );
}
#endregion

 

因為這一次的需求還包括了可以讓使用者可以去選擇要對哪一個欄位做關鍵字查詢,

所以多了一個「查詢欄位下拉選單」,我們只讓使用者選擇我們所指定的欄位,

private void SearchSolumnDropDownList(string searchColumn)
{
    Dictionary<string, string> searchColumns = new Dictionary<string, string>();
    searchColumns.Add("CompanyName", "CompanyName");
    searchColumns.Add("ContactName", "ContactName");
    searchColumns.Add("ContactTitle", "ContactTitle");
    searchColumns.Add("Address", "Address");
 
    ViewData["SearchColumnDDL"] = DropDownListHelper.GetDropdownList
    (
        "searchColumn",
        "searchColumn",
        searchColumns,
        new { id = "searchColumn" },
        searchColumn ?? "CompanyName",
        false,
        ""
    );
}

 

另外大家一定會注意到 Test3() 這個 Action Method 中有一個 ViewBag 的資料居然是放 Func<>…

ViewBag.DisplaySortColumnStyle = new Func<string, string, string, string>(DisplaySortColumnStyle);

如果大家還有印象的話,這也是我之曾經介紹過的一個 ViewBag 的使用方法:

ASP.NET MVC 3 - ViewBag 裡使用方法(Method)

大家可以複習一下,如果沒有看過或是不知道可以這樣使用的朋友,可以藉此機會來認識一下,

為什麼要這樣用呢?這是因為我們的需求中有一項是說:表格標題列將做為排序的欄位做比較明顯的顯示

先看看 Method 內容:

/// <summary>
/// Displays the sort column style.
/// </summary>
/// <param name="columnName">Name of the column.</param>
/// <param name="sortColumn">The sort column.</param>
/// <param name="sortType">Type of the sort.</param>
/// <returns></returns>
private string DisplaySortColumnStyle(string columnName, string sortColumn, string sortType)
{
    if (!columnName.Equals(sortColumn, StringComparison.OrdinalIgnoreCase))
    {
        return columnName;
    }
    else
    {
        string displayColumnName = string.Format("<span style=\"color: red;\">{0} {1}</span>", 
            columnName,
            sortType.Equals("asc", StringComparison.OrdinalIgnoreCase) ? "" : "");
        return displayColumnName;
    }
}

看了 Method 的程式內容就應該可以了解,那一定會有人說,這個 Method 就放在 ViewBag 裡面就會有作用嗎?

當然不是,放在 ViewBag 裡面是會了要給前端來使用,所以前端的用法如下:

<table>
    <tr>
        <th>
            @Html.Raw(ViewBag.DisplaySortColumnStyle("CompanyName", ViewBag.SortColumnName, ViewBag.SortType))
        </th>
        <th>
            @Html.Raw(ViewBag.DisplaySortColumnStyle("ContactName", ViewBag.SortColumnName, ViewBag.SortType))
        </th>
        <th>
            @Html.Raw(ViewBag.DisplaySortColumnStyle("ContactTitle", ViewBag.SortColumnName, ViewBag.SortType))
        </th>
        <th>
            @Html.Raw(ViewBag.DisplaySortColumnStyle("Address", ViewBag.SortColumnName, ViewBag.SortType))
        </th>
        <th>
            @Html.Raw(ViewBag.DisplaySortColumnStyle("City", ViewBag.SortColumnName, ViewBag.SortType))
        </th>
        <th>
            @Html.Raw(ViewBag.DisplaySortColumnStyle("Region", ViewBag.SortColumnName, ViewBag.SortType))
        </th>
        <th>
            @Html.Raw(ViewBag.DisplaySortColumnStyle("PostalCode", ViewBag.SortColumnName, ViewBag.SortType))
        </th>
        <th>
            @Html.Raw(ViewBag.DisplaySortColumnStyle("Country", ViewBag.SortColumnName, ViewBag.SortType))
        </th>
        <th>
            @Html.Raw(ViewBag.DisplaySortColumnStyle("Phone", ViewBag.SortColumnName, ViewBag.SortType))
        </th>
        <th>
            @Html.Raw(ViewBag.DisplaySortColumnStyle("Fax", ViewBag.SortColumnName, ViewBag.SortType))
        </th>
    </tr>

就是這樣,這樣的做法是比在前端的頁面中使用一堆的 Server 端程式判斷要來得好多了。

 

看完一開始進入的空白頁面 Action 方法後,接著就來看前端頁面 POST 查詢資料到後端,後端 Action 方法要怎麼處理,

  1: [HttpPost]
  2: public ActionResult Test3(
  3:     string keyword, 
  4:     string searchColumn, 
  5:     string city = "all", 
  6:     string sortColumnName = "CustomerID", 
  7:     string sort = "asc")
  8: {
  9:     using (NorthwindEntities db = new NorthwindEntities())
 10:     {
 11:         this.PrepareDropDownLists(city, sortColumnName, sort);
 12:         this.SearchSolumnDropDownList(searchColumn);
 13: 
 14:         var query = db.Customers.Select(x => x);
 15: 
 16:         if (!string.IsNullOrWhiteSpace(keyword))
 17:         {
 18:             string queryExpression = string.Format("{0}.Contains(@0)", searchColumn);
 19:             query = query.Where(queryExpression, keyword);
 20:         }
 21:         if (!city.Equals("all"))
 22:         {
 23:             query = query.Where("City == @0", city ?? "London");
 24:         }
 25: 
 26:         query = query.OrderBy(string.Format("{0} {1}", sortColumnName, sort));
 27: 
 28:         ViewData.Model = query.ToList();
 29: 
 30:         ViewBag.Keyword = keyword;
 31:         ViewBag.SortColumnName = sortColumnName;
 32:         ViewBag.SortType = sort;
 33:         ViewBag.DisplaySortColumnStyle = new Func<string, string, string, string>(DisplaySortColumnStyle);
 34: 
 35:         return View();
 36:     }
 37: }
 38: 

這邊的程式內容與「Dynamic LINQ + Entity Framework - Part.3:ASP.NET MVC 應用」裡面的程式相差不多,

而在 Line 16 ~ Line 20 這幾行程式就是這次的重點,

需求:讓使用者可以選擇欄位並輸入關鍵字來做過濾查詢

當使用者有選擇查詢欄位並且輸入查詢關鍵字之後,我們就要去做查詢關鍵字的處理,

如果不輸入查詢關鍵字的話,就不會多去做查詢關鍵字的處理,

因為是查詢指定的欄位中是否有無「包含」查詢關鍵字,所以我們使用「Contains」,對應到 SQL Statement 就是「like」

 

在程式中,為什麼查詢式的字串處理要使用這樣的方式,

string queryExpression = string.Format("{0}.Contains(@0)", searchColumn);
query = query.Where(queryExpression, keyword);

而不是用這樣的方式就好了呢?

query = query.Where("@0.Contains(@1)", searchColumn, keyword);

Dynamic Linq 對於 @0, @1 這樣的 parameter 使用,只能限定用於「值」,

雖然說我們是動態決定前面的查詢欄位,就我們開發者的認知,「查詢欄位」是頁面上的一個會變動的值,

POST 到後端時,也是把「查詢欄位」當做一個「值」來處理,

但如果要使用在 Dynamic LINQ 裡面還是一樣當成「值」來使用的話,所轉換出來的 SQL Statement 就真的會是「值」,

簡單的說,就是會無法轉換成正確的 SQL Statement 而查詢不到資料,

 

直接來看使用第二種方式的執行結果,

下面的這張圖,我們選擇「ContactName」這個欄位,並且輸入「ana」,

要找出 Customers 資料裡,欄位「ContactName」含有「ana」的資料出來,

下圖是已經執行過的查詢結果,但所查詢出來的卻是找不到任何資料,

image

我們直接在 SQL GUI 工具中去執行,是有符合查詢條件的資料,

image

我們從 MiniProfiloer 中查看實際執行的 SQL Statement 內容,我們就可以知道為什麼會找不到資料了,

image

上圖的 SQL Statement 可以看到,原本應該是 ColumnName 的內容,因為我們是使用以下的方式,

query = query.Where("@0.Contains(@1)", searchColumn, keyword);

Dynamic LINQ 在轉換的過程中就會把 @0, @1 這些 parameter 視為一般的「值」來處理,而不是當成 Property 來處理,

最後轉換 SQL Statement 就會是我們看到的結果,所以就會查詢不到符合查詢關鍵字的資料了。

 

所以使用 Dynamic LINQ 時,如果連欄位都要動態處理的話,就必須注意不要把這些欄位給當成一般的「值」而用 parameter 的方式來處理,

要先做好 queryExpression 的字串處理,最後再做 parameter 的處理,如下:

string queryExpression = string.Format("{0}.Contains(@0)", searchColumn);
query = query.Where(queryExpression, keyword);

執行的查詢結果:

image

由 MiniProfiler 觀察轉換的 SQL Statement 內容:

image

所以有關於動態欄位的處理就要千萬小心並且注意程式中的處理方式!

 

 

現在就來看看完成需求的處理結果,

一開始進入的頁面內容

image

 

變更城市下拉選單、排序欄位下拉選單、排序類型下拉選單的內容,

image

執行結果,

表格標題列中的欄位名稱內容,會因為選擇排序欄位下拉選單、排序類型下拉選單的內容而做變化與特別顯示,

image

再變更排序欄位下拉選單、排序類型下拉選單的內容

image

 

選擇要查詢的欄位並輸入查詢關鍵字,排序欄位下拉選單、排序類型下拉選單的內容也做了變更,

執行結果

image

觀察 SQL Statement 內容

image

 

 

最後來個實際操作的影片來讓大家印象深刻一些…


(沒有聲音… 如果覺得影像模糊的話,可以調整解析度為 720P 然後切換到全螢幕觀看)

 

以上

5 則留言:

  1. 不好意思想請問一下,似乎在您的文章裡面沒有對報表有著墨,請問一下如果使用MVC來開發的話,您是用什麼來做報表呢??jquery套件嗎??還是?????????????

    回覆刪除
    回覆
    1. Hello, 你好
      的確,我的這些 ASP.NET MVC 文章裡並沒有提到過任何有關報表的內容,不管是簡單的匯入匯出 Excel,還是許多人常用的 Crystal Report,或是與 SSIS 的整合,這些都沒有寫過。
      不過這些主題也都是在寫作計畫裡,只是短期內應該是沒有時間將這些內容寫出來,但是這些有關報表的相關資料與文章其實在國外也蠻多的,中國的博客園或是 CSDN.NET 博客頻道也有蠻多參考資料,可以逕行至 Google 作查詢。
      另外在台灣點部落也有一些有關 ASP.NET MVC 與報表的相關文章,例如:
      http://www.dotblogs.com.tw/lastsecret/archive/2010/06/25/16127.aspx

      其他的相關參考資料:
      http://weblogs.asp.net/rajbk/archive/2006/03/02/How-to-render-client-report-definition-files-_28002E00_rdlc_2900_-directly-to-the-Response-stream-without-preview.aspx
      http://kbochevski.blogspot.tw/2010/01/aspnet-mvc-and-crystal-reports.html
      https://www.youtube.com/playlist?list=PLdbGeedlvV03-JJOuX52qNe3gdCVRmHo8
      以上

      刪除
  2. 請問是否能動態組成要抓取的欄位呢?

    回覆刪除
    回覆
    1. 使用 Dynamic Query (Dynamic Expression API) 也是可以動態去組成要取得的欄位給 Select
      你看我在這一篇「練習題 - LINQ Single Column Dynamic Group」裡面的操作五,就可以看到怎麼使用了
      http://kevintsengtw.blogspot.tw/2014/04/linq-single-culumn-dynamic-group.html

      刪除
    2. Dynamic Expressions and Queries in LINQ
      https://dl.dropboxusercontent.com/u/26764200/Dynamic%20Expression%20API.html

      刪除

提醒

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