2014年10月15日 星期三

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

在上一篇介紹完了如何動態產生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。


沒有留言 :

張貼留言