文章名字好難起哦,既想能清楚的表達本文的主旨,又想短小精悍,真難。
為啥要“親自”呢?我想表達的意思是,在自已寫的程式中自已控制一切,這就叫親自。說起這個詞,還有一個典故(真人真事,如果雷同,實屬巧合):
在我上高三時,四班有一個位同學姓黃,名**,他以前在三中,後來轉學到一中的。該黃姓同學一直在追求一種境界,到底是一種什麼樣的境界,很難描述...他可以在課堂上肆無忌憚的排放腹中廢氣,也可以在毫無徵兆的情況下打個方圓30m之內其他房間可以聽到的噴嚏,甚至可以和校長開玩笑,一次,他課間去噓噓,本來是不允許學生去教師的WC的,但他是無視這種規定的,他先到的,正在放水時校長進來了,他就和校長打了個招呼:“高校長,您親自來上廁所了?”,校長被憋的竟只得"唔,唔"搪塞過去...
很是懷念校園的時光啊
進入正題:
為啥需要“親自”呢,這得說明來龍去脈:我的本意是做一個RESTful服務,自已寫了一個Atom的Client,使用WebRequest向伺服器提交資料,當然,格式是Atom10的,在伺服器端使用Request把Client傳上來的資料拿出來,如下虛擬碼所示:
public ActionResult Create(){ var entry = GetFromRequest(); //...}
這樣好嗎?剛寫好時覺得不錯,但第二眼就覺得不好了,為啥,如果這個entry是從Create方法的參數中傳來的該多好啊,想起以前在學習WCF時遇到的一個問題,使用Atom10FeedFormatter類就可以在參數中獲得這個entry的執行個體了,何樂不為呢,於是代碼變成這樣:
public ActionResult Create(Atom10FeedFormatter<LogEntry> log){ //...}
不幸的是,按照之前經驗,在該方法內部可以通過log.Item就獲得從Client傳來的實體了,但是在這裡發現log.Item居然為null。
這才發現原來MVC和WCF Syndication的機制是不同的啊。
通過閱讀MVC的原始碼發現,ControllerActionInvoker中一個方法GetParameterValues,這個方法擷取Action的參數列表,然後把根據一些策略產生對應的參數值,但是這個GetParameterValues似乎只能通過重載才能達到我想要的目的,達到我的目的又如何保證不影響MVC本來的意圖呢?還是先試試不重載,看有沒有輕量級的解決方案吧。
又想起來MVC中Filter不是幾乎無所不能麼,要麼來個ActionFitler好了,於是定義了一個類型:
[AttributeUsage(AttributeTargets.Method)]public class AtomEntryConvertAttribute : Attribute, IActionFilter{ public void OnActionExecuting(ActionExecutingContext filterContext) { //... } public void OnActionExecuted(ActionExecutedContext filterContext) { }}
試圖在OnActionExecuting中從Request中讀取資料,結果很快發現了問題,Action都執行了,該屬性類別的方法還沒有執行,仔細查看MVC的原始碼,發現了問題所在:
protected virtual ActionExecutedContext InvokeActionMethodWithFilters(ControllerContext controllerContext, IList<IActionFilter> filters, ActionDescriptor actionDescriptor, IDictionary<string, object> parameters) { ActionExecutingContext preContext = new ActionExecutingContext(controllerContext, actionDescriptor, parameters); Func<ActionExecutedContext> continuation = () => new ActionExecutedContext(controllerContext, actionDescriptor, false /* canceled */, null /* exception */) { Result = InvokeActionMethod(controllerContext, actionDescriptor, parameters) }; // need to reverse the filter list because the continuations are built up backward Func<ActionExecutedContext> thunk = filters.Reverse().Aggregate(continuation, (next, filter) => () => InvokeActionMethodFilter(filter, preContext, next)); return thunk(); }
Action的調用在ActionMethodFitler之前...汗啊,真是不長記性,上次就在這裡被“騙”一次,時間不長居然又忘了這茬了。
又回到GetParameterValues方法...
(此處省去2小時的思考,嘗試過程)
有一點結論了,如果要實現輕量級“非侵入”式的操作,以M$的習慣,一般是使用Attribute或者反射的,這裡用反射似乎不妥,那麼就應該在Entry類型或是參數本身上應用Attribute比較合適,根據M$的命名習慣,這種事情應該使用諸如Custom、Convert、Parameter之類的名稱,帶著這個思路一找,果然發現了一個類型CustomModelBinderAttribute。
光是這個類型名,就感覺是幹這個事情的,自訂 模型 綁定 特性 ,看起來像了,該類型中有一個公用方法:
public abstract IModelBinder GetBinder();
剛才在讀MVC原始碼時就發現這個IModelBinder介面大量使用在GetParameterValues方法中,差不多了,再看IModelBinder的定義:
object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext);
看定義就覺得像是從controllerContext和bindingContext中產生一個參數值的。
直接建立一個CustomModelBinderAttribute的子類:
[AttributeUsage(AttributeTargets.Parameter)] public class AtomEntryParameterConvertAttribute : CustomModelBinderAttribute { public Type EntryType { get; private set; } public AtomEntryParameterConvertAttribute(Type entryType) :base() { this.EntryType = entryType; } public override IModelBinder GetBinder() { return new AtomEntryConvertModelBinder(this.EntryType); } }
再建立一個IModelBinder的實現:
internal class AtomEntryConvertModelBinder : IModelBinder { public Type EntryType { get; private set; } internal AtomEntryConvertModelBinder(Type entryType) { this.EntryType = entryType; } public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) { var result = AtomServiceHelper.GetDataFromEntry(controllerContext.HttpContext.Request, this.EntryType, bindingContext.ModelType); return result; } }
這裡我要稍說明下,其實我在Action的參數中收到的不是直接從用戶端上傳來的SyndicationItem類型的子類AtomEntry,AtomEntry只是Client和Server互動時的基於Atom協議的資料結構,它派生自SyndicationItem類,真正在Server上使用的是實體類,在我的例子中:
public class Log {}
Log類型是真正服務端業務層、資料層使用的實體類型
public class LogEntry : AtomEntry {}public class AtomEntry : SyndicationItem {}
LogEntry類型是Client與Server互動的資料格式
因此,我不僅需要從Client上把LogEntry的執行個體傳到Server,而且還要在Server上的Action中使用參數直接獲得Log類型的執行個體,當然Log和LogEntry的定義是一個策略,或者說它們之間是有約定的,沒有約定,它倆也完不成轉換,我喜歡約定甚於配置。
AtomServiceHelper.GetDataFromEntry(controllerContext.HttpContext.Request, this.EntryType, bindingContext.ModelType)
正是AtomServiceHelper類讀取Request中的資料,然後將EntryType類型的執行個體轉換為參數類型的執行個體。
其實對於IModuleBinder我也沒有查看MSDN,只是感覺就是這種用法,對不對還沒有做實驗,於是寫個例子:
[HttpPost] [ServiceError] public ActionResult Create([AtomEntryParameterConvert(typeof(LogEntry))]LogRecord log) { var logMng = new LogManager(); logMng.CreateLog(log); return new EmptyResult(); }
運行,yeah!果然如願得到了來自Client的資料