自定義的ControllerFactory:接口實(shí)現(xiàn),支持Area
幾個(gè)星期之前,有個(gè)朋友對(duì)我說(shuō),他的項(xiàng)目中需要將前后臺(tái)區(qū)分開(kāi)來(lái),也就是類似分Area的功能。不過(guò)Area只在MVC 2中出現(xiàn),因此現(xiàn)在想在1.0版本中先實(shí)現(xiàn)類似的功能了。他打算,根據(jù)Route中捕獲的內(nèi)容(如“area”),然后去找對(duì)應(yīng)命名空間下的Controller。這樣看來(lái)不難,似乎只要在Route上做點(diǎn)配置,而默認(rèn)的DefaultControllerFactory已經(jīng)對(duì)命名空間的查詢提供支持了(可惜有線程安全的問(wèn)題)。
不過(guò)他說(shuō),***發(fā)現(xiàn)似乎這塊功能不是他想象的那樣,因此希望我可以看看到底是什么問(wèn)題。由于當(dāng)時(shí)沒(méi)有擴(kuò)展ASP.NET MVC的需求,后來(lái)我事情一多就忘了,現(xiàn)在先說(shuō)聲抱歉。最近開(kāi)始對(duì)ASP.NET MVC動(dòng)手動(dòng)腳了,發(fā)現(xiàn)這樣一個(gè)Area的功能非常有用,而且巧合的是,我也打算把Area和命名空間對(duì)應(yīng)起來(lái)。
只是我選擇的路和那位兄弟不一樣,我打算自己寫(xiě)一個(gè)簡(jiǎn)單的ControllerFactory來(lái)替換掉默認(rèn)的DefaultControllerFactory。這么做的主要原因是:我不知道DefaultControllerFactory已經(jīng)提供對(duì)命名空間的支持了,微軟默默地實(shí)現(xiàn)了卻沒(méi)有對(duì)外公開(kāi)過(guò),我也是后來(lái)閱讀代碼時(shí)才發(fā)現(xiàn)的。同時(shí)又意識(shí)到線程安全的問(wèn)題,于是就還是打算自己寫(xiě)了。
好在ASP.NET MVC從設(shè)計(jì)之初就提供了擴(kuò)展的能力,每個(gè)組件粒度都很小,大部分組件都是可以獨(dú)立拔插的(Controller類除外,如果你使用自己的IController實(shí)現(xiàn),就會(huì)發(fā)現(xiàn)大部分功能,如各Filter都失效了)。而要實(shí)現(xiàn)一個(gè)Controller Factory,只要實(shí)現(xiàn)一個(gè)簡(jiǎn)單的IControllerFactory就可以了(我喜歡接口):
- public interface IControllerFactory
- {
- IController CreateController(RequestContext requestContext, string controllerName);
- void ReleaseController(IController controller);
- }
于是構(gòu)建一個(gè)AreaControllerFactory也大致只需要以下一些代碼:
- public class AreaControllerFactory : IControllerFactory
- {
- public IController CreateController(RequestContext requestContext, string controllerName)
- {
- ...
- }
- public void ReleaseController(IController controller)
- {
- IDisposable disposable = controller as IDisposable;
- if (disposable != null)
- {
- disposable.Dispose();
- }
- }
- }
然后按照慣例,還是一步步談起。首先是構(gòu)造函數(shù),我們的策略是根據(jù)不同的Area加載不同命名空間下的Controller類型。方便起見(jiàn),我選擇“基礎(chǔ)命名空間”和“擴(kuò)展部分”兩塊,它們從構(gòu)造函數(shù)中傳入:
- private Dictionary<string, string> m_areaPartMapping = new Dictionary<string, string>();
- public string NamespaceBase { get; private set; }
- public AreaControllerFactory(string namespaceBase)
- : this(namespaceBase, null)
- { }
- public AreaControllerFactory(string namespaceBase, IDictionary<string, string> areaPartMapping)
- {
- this.NamespaceBase = namespaceBase.EndsWith(".") ? namespaceBase : namespaceBase + ".";
- if (areaPartMapping != null)
- {
- foreach (var pair in areaPartMapping)
- {
- this.m_areaPartMapping.Add(pair.Key.ToLowerInvariant(), pair.Value);
- }
- }
- }
于是我們就可以這樣使用:
- var controllerFactory = new AreaControllerFactory("MyApp.Controllers");
- ControllerBuilder.Current.SetControllerFactory(controllerFactory);
如果在需要的時(shí)候,還可以指定Area與特定命名空間“部分”的映射關(guān)系。因此,我們需要從Area來(lái)獲取這個(gè)“Part”:
- private string GetNamespacePart(string area)
- {
- if (String.IsNullOrEmpty(area)) return "";
- string part;
- if (this.m_areaPartMapping.TryGetValue(area.ToLowerInvariant(), out part))
- {
- return part;
- }
- return area;
- }
這里我選擇“配置”和“約定”相結(jié)合的方式。得到一個(gè)Area之后,我們會(huì)在映射表里進(jìn)行查找Part,如果沒(méi)有,則Area本身便是Part。根據(jù)Part和Controller名稱,我們便可以獲得Controller的類型:
- private ReaderWriterLockSlim m_rwLock = new ReaderWriterLockSlim();
- private Dictionary<string, Type> m_controllerTypes = new Dictionary<string, Type>();
- private Type GetControllerType(string area, string controllerName)
- {
- string part = this.GetNamespacePart(area);
- string typeName = String.IsNullOrEmpty(part) ?
- this.NamespaceBase + controllerName.ToLowerInvariant() + "Controller" :
- this.NamespaceBase + part + "." + controllerName.ToLowerInvariant() + "Controller";
- Type type;
- this.m_rwLock.EnterReadLock();
- try
- {
- if (this.m_controllerTypes.TryGetValue(typeName, out type))
- {
- return type;
- }
- }
- finally
- {
- this.m_rwLock.ExitReadLock();
- }
- type = Type.GetType(typeName, false, true);
- if (type != null)
- {
- this.m_rwLock.EnterWriteLock();
- try
- {
- this.m_controllerTypes[typeName] = type;
- }
- finally
- {
- this.m_rwLock.ExitWriteLock();
- }
- }
- return type;
- }
由于我選擇在應(yīng)用程序中使用同一個(gè)AreaControllerFactory對(duì)象,因此線程安全是一定要有保證的。這里我們用到了讀寫(xiě)鎖,不過(guò)請(qǐng)注意,紅色那句話并不保證對(duì)于每個(gè)相同的typeName只執(zhí)行一次,也不保證相同的typeName對(duì)于m_controllerTypes字典只會(huì)進(jìn)行一次寫(xiě)操作(所以我沒(méi)有Add,而是使用了下標(biāo)操作)。不過(guò),由于這些“重復(fù)”不會(huì)造成問(wèn)題,因此就沒(méi)有去涉及太多這方面的考慮。
***,便是那CreateControlle方法:
- public IController CreateController(RequestContext requestContext, string controllerName)
- {
- Type controllerType;
- object area;
- if (requestContext.RouteData.Values.TryGetValue("area", out area))
- {
- controllerType = this.GetControllerType(area.ToString(), controllerName);
- }
- else
- {
- controllerType = this.GetControllerType(null, controllerName);
- }
- if (controllerType == null)
- {
- throw new HttpException(404,
- String.Format(
- "Controller of path {0} not found.",
- requestContext.HttpContext.Request.Path));
- }
- try
- {
- return (IController)Activator.CreateInstance(controllerType);
- }
- catch (Exception ex)
- {
- string message = String.Format("Error creating controller {0}" + controllerType);
- throw new InvalidOperationException(message, ex);
- }
- }
似乎沒(méi)有什么可談的:我們從RouteData中獲取出area對(duì)應(yīng)的值,并且調(diào)用GetControllerType方法獲得Controller的類型,并使用Activator.CreateInstance創(chuàng)建對(duì)象。在不合法的情況下,拋出合適的異常即可。
至此,AreaControllerFactory就完成了,很容易,不是嗎?很顯然,這個(gè)組件的功能非常有限,例如為什么所有的Controller一定要在同一個(gè)命名空間下?沒(méi)錯(cuò),它其實(shí)只是符合“我要求”的一個(gè)東西。但是,在項(xiàng)目中很多東西都是如此,我只實(shí)現(xiàn)我夠用的功能。例如,我可能不會(huì)向?qū)ν夤_(kāi)的API那樣,嚴(yán)格檢查每個(gè)問(wèn)題,拋出嚴(yán)謹(jǐn)?shù)漠惓?。我可能傾向于在項(xiàng)目中使用接口,而不是使用抽象類。因?yàn)槭俏业捻?xiàng)目,我可以快速反饋,需要修改的時(shí)候就修改吧。
同樣的,如果DefaultControllerFactory真在某些特別情況下有問(wèn)題,或者支持的有些復(fù)雜。那么不如我們就自己動(dòng)手吧。一次性投入,而且這樣的小組件也花不了多少時(shí)間。
本文來(lái)自老趙點(diǎn)滴:《支持Area的ControllerFactory》
【編輯推薦】