在上一篇介紹完了如何動態產生Linq條件之後,在這一篇,將會透過Reflection和Dynamic Linq Query來讓Service層,能夠在不做任何事情的情況下,自動對資料做過濾,並且轉成對應的ViewModel配上分頁。
Service層的處理
在處理搜索的部份,Service層將會需要:
- 透過Reflection取得要搜索的欄位 - 這邊要記得是不要base的欄位(不要那些例如目前在第幾頁,和一頁幾筆的那種)
- 依照Reflection的欄位和Dynamic LInq Query組成搜索條件
- 做搜索並且用Automapper把Entity 對應到ViewModel搭配分頁
Service處理搜索的方法
首先,之前的那個GenericService
將會多一個方法叫做ProcessIndexViewModel
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | /// <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的話,可以設定。
再來就詳細看一下實際做搜索的邏輯。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | /// <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取得欄位值和組成搜索條件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | /// <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對應
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | /// <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呼叫搜索
在前面呼叫就變得比較簡單:
1 2 3 4 5 6 7 | public ActionResult Index(Index searchViewModel) { service.ProcessIndexViewModel(searchViewModel); return View(searchViewModel); } |
Service是和之前一樣注入進來的。這邊把Index ViewModel作為方法參數是有兩個用意:
- 如果要做搜索或者按下下一頁的時候,會直接post back到這一邊,因此要接住才可以。
- 當第一次get的時候,為了邏輯一致,因此還是要呼叫
ProcessIndexViewModel
,但是如果Index ViewModel沒有被實例化,會造成裡面Property是null 導致出錯。
結語
透過這一篇,可以讓框架幫我們針對一般的欄位和條件自動做搜索和分頁,不過這一篇沒有介紹在View上面如何使用。
在下一篇,將會介紹如何透過template讓產生的搜索欄位在不同功能看起來一致,並且一些helper來幫助產生url。