架構之路(七)MVC點滴

自由飛發表於2015-12-02

我們目前正在開發中的是任務管理系統,一個前端複雜的專案,所以我們先從MVC講起吧。

 

WebForm

 

隨著ASP.NET MVC的興起,WebForm已成昨日黃花,但我其實還很想為WebForm說幾句。

 

沒有經歷過從ASP向ASP.NET轉變的同學,是很難理解當WebForm出現時,程式猿世界的歡呼雀躍的。事實上,我也是在Razor出現之後,才勉勉強強的轉向MVC,因為看見<% %>這個東西就怕。我曾經參加過一個升級ASP到ASP.NET的專案,ASP裡面亂七八糟的程式碼看得我眼睛又酸又脹紅通通的流淚,一輩子都記得!

WebForm最後生成的html可能會臃腫難看,但其內碼表面(.aspx)是相當清爽漂亮的。

 

既然我們都已經決定採用MVC了,WebForm的不足就不用再多說了。但我們應該努力的學習和借鑑它優秀的地方,這些也是在MVC的開發中會用到的:

  • 呈現和頁面邏輯相分離。WebForm裡由於它的框架本來就顯式的區分了aspx和aspx.cs,所以大多數時候我們不會擔心這個事情。但MVC裡面,我們很容易就在view裡面利用ViewModel資料進行運算,模糊Controller和View之間的邏輯界限。這個問題我們將在CurrentUser的時候詳細講解。
  • 良好的頁面封裝和重用。當我們發現頁面又反覆出現的、大同小異的“部件”時,我們肯定就會想到重用。這就是考驗我們功力的時候。我先提一點我想到的:有時候我們寧願重複不願重用!這是我得出來的血淚教訓。應該是在創業家園專案的評論頁面部分,我曾經試圖重用所有評論的PartialView,結果慘不忍睹,最後放棄重用,反而海闊天空。其實有一個更好的例子就是WebForm中的GridView和Repeater,從實踐上看,反而是簡單封裝的Repeater更受歡迎,“大而全”的GridView卻少有人用。所以封裝和重用有一個度的問題。

 

RouteTest

 

Route功能是MVC的一個重大突破,也是一個重要缺陷。由於沒有良好的自動檢查機制,在實際的開發過程中,非常容易出錯!相信有過開發經驗的同學都有體會,有時候老半天都報錯:找不到View找不到Action,查來查去就一個拼寫錯誤;有時候新增一條RouteConfig,一會兒其他同事叫起來了,“考!原來是你的設定把我的覆蓋了。查了我一下午!”

把時間浪費在這些地方實在是可惜,所以我們解決這個問題的辦法是使用單元測試,在PCTest的project中引入了RouteTest。每一次新增RouteConfig,跑一遍單元測試:自己的能過,也不影響別人的,就OK了。

這是單元測試我們專案的UI層最成功的例子。照理說,MVC的最大的一個好處就是“可測試”,其他地方也應該廣泛引入單元測試的,但本人偷懶,另外HttpContext的sealed限制也限制了單元測試的實施(MVC 5應該解決了這個問題),所以目前UI層的單元測試還沒有展開。但估計這個工作遲早都得做,現在已經出現了一些手工測試繁瑣費事易遺漏的問題了。

 

URL/View層級

 

MVC現目前的另一個問題是,View很難按多層級組織。比如,我可能需要的View是這樣組織的:

注意Controller也有層級關係設定。我始終覺得這樣會更清晰整潔,但如果MVC的框架不能這樣進行“層級對應”。如果一定要這樣把View分層組織起來,在Action中就必須寫出View的全部路徑,比如:

    public class LogController : Controller
    {
        //
        // GET: /Account/Log/On
        public ActionResult On()
        {
            return View("~/Views/Account/Log/On.cshtml");
        }
    }

還得專門配置RouteConfig,這也太麻煩了一點。所以,我們就還是儘量按MVC的框架,從URL的設計開始,就儘量是/{Controller}/{action}/{route-parameter}的樣式,View也同樣,放在Contoller對應的資料夾下即可。

 

Partial/ChildAction/EditorTemplate

 

當我們需要重用某些“頁面片段”時,我們就面臨了以上這幾種選擇。切入的點有很多,我們就只結合我們專案,抽取其最鮮明最容易辨認的特點,直接講述他們的使用場景:

 

首先是EditorTemplate。它的特點最明顯,是和Post相關的。也就是,當一個“頁面片段”的資料,還需要再Post回伺服器的時候,我們就必須使用EditorTemplate;如果不使用EditorTemplate,就ViewModel的資料就無法傳回(參考:任務管理系統程式碼中/Views/Task/EditorTemplates)。為什麼呢?和MVC的ViewModel繫結機制有關,EditorTemplate中的html控制元件呈現時,會在其name上加上所屬父Model的字首,以便於MVC自動解析post資料並繫結到ViewModel。

 

如果“頁面片段”不需要POST,只負責呈現即可,又該如何選擇呢?我們的原則是:

  • 如果“頁面片段”不需要和伺服器端互動,所需要的資料都能從父Model中獲得,使用Partial;
  • 否則,如果“頁面片段”說需要的資料還需要從伺服器獲得,那就只能使用ChildAction了。

 

HtmlHelper

 

除了上述幾種頁面片段的重用,還有通過建立HtmlHelper的擴充套件方法,自定義一種“頁面片段”的呈現方式。這種方式一般是PartialView的一種替代方式,我們通常把“很小很小”(比如一個連結、一個下拉選單等),用處“很多很多”(甚至於跨專案)的可重用html片段用HtmlHelper封裝起來。可參考:

  • 任務管理系統專案中的DocumentLink:封裝一個總是使用doc.zyfei.net域名的html連結
  • Global專案(還未上傳原始碼)中的EnumDropDownListFor:封裝一個使用dropdownlist,該dropdownlist由enum填充,使用enum上的[Description]作為呈現文字

 

AJAX

 

觀察我們的Action就可以發現,我們為Ajax提供的Action始終是返回的ActionResult,而不是使用“更先進”的WebApi機制(直接返回int等簡單型別)。這主要是因為我們使用了SessionPerRequest機制(主要是為了提高效能),我們讓一個Request請求只使用一個session(可先簡單的理解為一個資料庫連線),亦即:

  1. 當MVC獲得一個Request,需要使用session時,Service生成一個session;
  2. 然後,在這個Request的整個請求過程中,使用的都將是這個已經生成的session(類似於“單例模式”);
  3. 當Request結束後,釋放這個session,將所有改動同步到資料庫

好了,這裡我們的關鍵點就是什麼時候算“Request結束”?我們更進一步的定義它為View呈現完畢的時候,所以利用了Filter機制,在OnResultExecuted()時同步資料庫,程式碼如下:

    public class SessionPerRequest : ActionFilterAttribute
    {
        public override void OnResultExecuted(ResultExecutedContext filterContext)
        {
            #if PROD
            FFLTask.SRV.ProdService.BaseService.EndSession();
            #endif

            base.OnResultExecuted(filterContext);
        }
    }

所以,即使Ajax呼叫,也必須經歷一個“View呈現完畢”的過程,才能完成資料同步。

 

UIDevService切換

 

進行前臺開發,不需要連線後臺資料庫的同學,只需要在MVC專案編譯時,輸入UIDEV即可(如果要真正的連線資料庫,使用PROD),如下所示:

 

那麼,這究竟是如何實現的呢?

 

總體上來說,我們借用了autofac這個類庫,實現了所謂的“依賴倒置”

 

所以,在MVC的Controller中,我們只使用ServiceInterface而不管其具體實現,如下所示:

        private IAuthroizationService _authService;
        public AuthController(IAuthroizationService authService)
        {
            _authService = authService;
        }

 

最後,在Global.asax.cs中我們通過條件編譯符if...else來確定究竟使用哪一種Service實現:ProdServiceModule,或者UIDevServicemodule

        void ResolveDependency()
        {
            var builder = new ContainerBuilder();

            builder.RegisterControllers(Assembly.GetExecutingAssembly());
            builder.RegisterFilterProvider();

#if PROD
            builder.RegisterModule(new ProdServiceModule());
#endif
#if UIDEV
            builder.RegisterModule(new UIDevServicemodule());   
#endif
            container = builder.Build();
            DependencyResolver.SetResolver(new AutofacDependencyResolver(container));
        }


最後,不要忘了,新引入一個Service時,在ProdServiceModule.cs或者UIDevServicemodule.cs中新增:

            builder.RegisterType<RegisterService>().As<IRegisterService>();

 

這一章就差不多了吧。下一章我們再講CurrentUser,並由此引出我們的原則:如何在View、Controller、Service和ViewModel之間劃分邏輯(或者責任)。

相關文章