Pages

2015-11-25

MVC聯絡表單Design Pattern

以下是一個MVC前端表單的設計模式,雖是我是第1次寫MVC,但有隱約把握到MVC的分層精神,自己的心得體會給大家參考看看。

Form Submit

ASP.NET MVC5裏前端View裏,那些@Html.XXXFor(),ValidateFor()這類的標籤輸助函式我都儘量不用,因為它不是標準的HTML,還得依賴Server端的解讀。雖然它能簡化產生html碼,但其轉譯代價明顯不合算。View提供的功能應該是協助前端HTML,JS而存在,而不是去取代或扭曲它,不足之處就靠前端JS框架來補足即可。事實上證明,下一版的MVC6提出Tag Helper,它的方式比較適合一些。上面表單驗證我使用jQuery Validation來取代MVC提供的Model Validation的前端作法,但保持其後端的ModelState驗證檢查。

Controller Action的表單接受方式為下圖,其精神就是儘量把商業邏輯寫入Model裏, Controller只寫「很簡單」的頁面請求服務判斷。這也是為何MVVM(Model-View-ViewModel)模式會被提出的理由,因為HTML的JS AJAX已經明顯選擇好了服務URL,Controller已不像字面上的「控制者」角色,精確來說它只是一個請求轉貼者(Forwarder),重點應在Service Model如何處理請求及讀寫DB (with Repository Model),最後產生ViewModel帶入View中顯示而己。

Action Code

既然MVC的Controller是虛職角色,真正處理請求邏輯的Model目錄,就得依其功能分類成主要三類:「Repository, Service, ViewModel」。你可以理解各種水果(DbTable)有它們削皮處理的方式,那麼Respository就是處理單一水果讀取的角色,而把它們拼成不同的水果盤,就是Service角色,包括各種水果再切片搭配或舖盤美化。最後完整的水果盤被端出,就是所謂的ViewModel。

Model Category

很多人會關心MVC裏,SQL命令該怎麼執行?是繼續寫串接式的SQL字串,還是該用Entity化的EF LINQ查詢?若有過ORM的開發經驗,你會很清楚EF的定位是啥,我很認同某人說過「別糾結EF查詢最終轉成什麼SQL法,EF本來對於複雜查詢本來就不是它的目的」。儘管如此,很多人還是會在EF技術群裏,糾結詢問複雜的DB查詢,LINQ該怎麼寫?產生的SQL差異效能如何?我提供一個比較簡單的比喻,傳統SQL寫法就像刀子,而EF是槍,我們要完成的DB動作目的就是要殺生。槍雖然威力強,但子彈成本貴(LINQ轉成SQL),假如要殺一隻老鼠,應該用刀子簡單省錢,因為開槍也是讓子彈像刀子一樣快速射入目標體內,剝奪生命而己。然而,槍畢竟不是刀,刀子除了殺生還能削片處理細節,槍就很難辦到了(LINQ複雜JOIN語法)。

簡言之,當複雜SQL報表查詢,你應該用SQL來寫,若覺得字串相加串條件的寫法很討厭,我這兒提供一個SQL範本式Pattern,能解決SQL動態條件字串問題。以我的作法而言,會先考量查詢效能及管理維護成本,再建立一個Service Model類別來管理DB操作函式,依目的選擇EF或SQL,或兩者併用,只要有統一類別管理它們,它們就能和平共處,共造雙贏局面。

 Model Code

至於,EF Data Model的POCO Class產生方式,我建議使用「CodeFirst from existing database」的工具方式來產生POCO,雖然它缺乏像*.edmx的UI管理工具,但其在連線字串、欄位微調會比EDMX精簡很多。雖然至目前我仍以DB First為主的開發方式,但Code First的優點卻是立即採用(聽說EF7也沒有EDMX了)!

2015-11-19

MVC5 ResolveUrl in static code

ASP.NET MVC5常用Url.Content(virtualPath)來取得實際的URL,若是在static程式區塊裏,要取得頁面實際路徑的簡易方式為System.Web.VirtualPathUtility.ToAbsolute(virtualPath),然而它無法處理http://路徑(會丟出例外),因此要額外處理它:

public static string ResolveUrl(string virtualPath)
{
if (virtualPath == null)
return null;

// VirtualPathUtility can't handle the http://xxx path.
if (virtualPath.IndexOf("://") != -1)
return virtualPath;

// Resolve the ~/ tilde.
if (virtualPath.StartsWith("~"))
return System.Web.VirtualPathUtility.ToAbsolute(virtualPath);

return virtualPath;
}