Alan Tsai 的學習筆記


學而不思則罔,思而不學則殆,不思不學則“網貸” 為現任微軟最有價值專家 (MVP)、微軟認證講師 (MCT) 、Blogger、Youtuber:記錄軟體開發的點點滴滴 著重於微軟技術、C#、ASP .NET、Azure、DevOps、Docker、AI、Chatbot、Data Science

[iThome 第七屆鐵人賽 23] 搜索頁面 - Service層的工作 - 自動套用一般搜索條件

2014-10-15 Wednesday

在上一篇介紹完了如何動態產生Linq條件之後,在這一篇,將會透過Reflection和Dynamic Linq Query來讓Service層,能夠在不做任何事情的情況下,自動對資料做過濾,並且轉成對應的ViewModel配上分頁。

Service層的處理

在處理搜索的部份,Service層將會需要:

  1. 透過Reflection取得要搜索的欄位 - 這邊要記得是不要base的欄位(不要那些例如目前在第幾頁,和一頁幾筆的那種)
  2. 依照Reflection的欄位和Dynamic LInq Query組成搜索條件
  3. 做搜索並且用Automapper把Entity 對應到ViewModel搭配分頁

Service處理搜索的方法

首先,之前的那個GenericService將會多一個方法叫做ProcessIndexViewModel

/// <summary>
/// 通用行的Service layer實作
/// </summary>
/// <typeparam name="T">主要的Entity形態</typeparam>
public class GenericService<T> : IService<T>
    where T : class
{
....
	/// <summary>
    /// 處理在Index ViewModel所需要做的事情
    /// </summary>
    /// <typeparam name="TSearchForm">搜索form的形態</typeparam>
    /// <typeparam name="TPageResult">搜索結果的形態</typeparam>
    /// <param name="searchViewModel">搜索相關的viewmodel</param>
    /// <param name="includes">需要Include進來的其他Entity</param>
    public virtual void ProcessIndexViewModel<TSearchForm, TPageResult>(ISearchViewModelBase<TSearchForm, TPageResult> searchViewModel,  
        params System.Linq.Expressions.Expression<Func<T, object>>[] includes) 
        where TSearchForm : ISearchFormViewModelBase, new()
    {
        var data = db.Repository<T>().Reads();

        foreach (var item in includes)
        {
            data.Include(item);
        }
		
		SearchViewModelProcess.ApplySearchForm<T, TSearchForm, TPageResult>(data,
                    searchViewModel);
    }
....
}

基本上ProcessIndexViewModel會接受一個之前定義過的SearchViewModelBase的形態,同時假設搜索的內容需要做到Include的話,可以設定。


再來就詳細看一下實際做搜索的邏輯。

/// <summary>
/// 把Search Form Viewmodel的OrderBy和Where條件apply上去。
/// 會把最終內容儲存到SearchViewModelBase.Result裡面。
/// </summary>
/// <typeparam name="T">EF 的Entity</typeparam>
/// <typeparam name="TSearchForm">Search Form ViewModel的Type</typeparam>
/// <typeparam name="TPageResult">Search結果的VieModel type</typeparam>
/// <param name="data">原始的IQueryable</param>
/// <param name="searchForm">Search Form ViewModel</param>
public static void ApplySearchForm<T, TSearchForm, TPageResult>(IQueryable<T> data, ISearchViewModelBase<TSearchForm, TPageResult> searchForm)
     where TSearchForm : ISearchFormViewModelBase, new()
{
    data = data.DynamicWhere(searchForm.SearchForm);

    ApplyOrderByAndToPageResult<T, TSearchForm, TPageResult>(data, searchForm);
}
ApplySearchForm會在呼叫兩個方法,一個是下搜索條件,一個是做實際的搜索的呼叫AutoMapper,先來看一下實際的搜索。

Reflection取得欄位值和組成搜索條件

/// <summary>
/// 依照Search Form ViewModel的值來設定Where的內容。
/// </summary>
/// <typeparam name="T">通常是EF的Entity</typeparam>
/// <param name="data">要被處理的資料</param>
/// <param name="searchForm">Search Form Viewmodel的值</param>
/// <returns>有增加OrderBy的IQueryable</returns>
public static IQueryable<T> DynamicWhere<T>(this IQueryable<T> data, ISearchFormViewModelBase searchForm)
{
    var properties = searchForm.GetType().GetProperties
						(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public)
                        .Where(x => (x.GetGetMethod().GetBaseDefinition() == x.GetGetMethod())).ToArray(); // where 條件是用來避免override的property (例如:OrderByColumnName)也被算進去。

    string whereCalus = string.Empty;
    string andOperator = string.Empty;
    List<object> propertiesValue = new List<object>();

    for (int i = 0; i < properties.Length; i++)
    {
        var value = properties[i].GetValue(searchForm);

        if (string.IsNullOrEmpty(value.NonNullString()) == false)
        {
            whereCalus = string.Format("{0}{1} {2} = @{3}", whereCalus, andOperator,
                    properties[i].Name, propertiesValue.Count);

            andOperator = " and";

            propertiesValue.Add(value);
        }
    }

    if (string.IsNullOrEmpty(whereCalus) == false)
    {
        data = data.Where(whereCalus, propertiesValue.ToArray());
    }

    return data;
}

這邊注意到核心是取得properties的部分,只需要後來繼承的欄位,和注意不要取得複寫的欄位,例如OrderByColumnName。之後,就是用Dynamic Linq Query來組裝搜索條件。


執行完了DynamicWhere,就有了搜索的條件,接下來就是執行搜索條件並且轉成對應的ViewModel和分頁。


做搜索和用AutoMapper對應

/// <summary>
/// Apply Orderby並且把最後結果塞到SearchViewModelBase.Result裡面。
/// </summary>
/// <typeparam name="T">EF 的Entity</typeparam>
/// <typeparam name="TSearchForm">Search Form ViewModel的Type</typeparam>
/// <typeparam name="TPageResult">Search結果的VieModel type</typeparam>
/// <param name="data">原始的IQueryable</param>
/// <param name="searchForm">Search Form ViewModel</param>
private static void ApplyOrderByAndToPageResult<T, TSearchForm, TPageResult>(IQueryable<T> data, 
    ISearchViewModelBase<TSearchForm, TPageResult> searchForm) where TSearchForm : ISearchFormViewModelBase, new()
{
    if (searchForm.SearchForm.IsAscending)
    {
        data = data.OrderBy(searchForm.SearchForm.OrderByColumnName);
    }
    else
    {
        data = data.OrderBy(searchForm.SearchForm.OrderByColumnName + " descending");
    }

    searchForm.Result = data.Project().To<TPageResult>().ToPagedList(searchForm.SearchForm.Page, searchForm.SearchForm.PageSize);
}

可以看到,先依照欄位做排序(這邊需要注意到OrderBy也是Dynamic Linq Query的方法),然後在把資料轉型把結果放到Result裡面。


Controller呼叫搜索


在前面呼叫就變得比較簡單:

public ActionResult Index(Index searchViewModel)
{
    service.ProcessIndexViewModel(searchViewModel);

    return View(searchViewModel);
}

Service是和之前一樣注入進來的。這邊把Index ViewModel作為方法參數是有兩個用意:



  1. 如果要做搜索或者按下下一頁的時候,會直接post back到這一邊,因此要接住才可以。
  2. 當第一次get的時候,為了邏輯一致,因此還是要呼叫ProcessIndexViewModel,但是如果Index ViewModel沒有被實例化,會造成裡面Property是null 導致出錯。


結語


透過這一篇,可以讓框架幫我們針對一般的欄位和條件自動做搜索和分頁,不過這一篇沒有介紹在View上面如何使用。


在下一篇,將會介紹如何透過template讓產生的搜索欄位在不同功能看起來一致,並且一些helper來幫助產生url。


如果文章對您有幫助,就請我喝杯飲料吧
街口支付QR Code
街口支付QR Code
台灣 Pay QR Code
台灣 Pay QR Code
Line Pay 一卡通 QR Code
Line Pay 一卡通 QR Code
街口支付QR Code
支付寶QR Code
街口支付QR Code
微信支付QR Code
2014-10-15 Wednesday
comments powered by Disqus