淺析WebFormView中的一個(gè)Bug
老趙的文章主要是針對(duì)WebFormView中的一個(gè)Bug,這個(gè)Bug在一般情況下不會(huì)出現(xiàn)問(wèn)題,它沒(méi)有遵循ASP.NET MVC既定的模型。
最近需要搞一些重要的功能,結(jié)果又遇到了意料外的障礙。于是又仔細(xì)地看了看ASP.NET和ASP.NET MVC的源代碼,又發(fā)現(xiàn)了以前不曾知道的一些細(xì)節(jié)。您最多說(shuō)ASP.NET WebForms模型不一定適合某些Web應(yīng)用程序的開(kāi)發(fā),但是我想沒(méi)有人可以否認(rèn)ASP.NET中設(shè)計(jì)的巧妙——以及復(fù)雜程度。其實(shí)ASP.NET為我們留下了不少切入點(diǎn),但幾乎沒(méi)什么書(shū)會(huì)提到這些切入點(diǎn),我們只能從微軟自己的框架中一探究竟。
不過(guò)這次我想談的是ASP.NET MVC框架中的一個(gè)Bug,這個(gè)Bug在一般情況下不會(huì)出現(xiàn)問(wèn)題,但是這的確違反了ASP.NET MVC自身的設(shè)計(jì)。這個(gè)問(wèn)題就出在WebFormView對(duì)象的實(shí)現(xiàn)上。
WebFormView是一個(gè)視圖對(duì)象的實(shí)現(xiàn)。而在ASP.NET MVC中,任何視圖都需要實(shí)現(xiàn)一個(gè)IView接口:
- public interface IView {
- void Render(ViewContext viewContext, TextWriter writer);
- }
Render方法的目的自然是根據(jù)ViewContext對(duì)象中的數(shù)據(jù),將視圖內(nèi)容輸出至TextWriter中。例如在HtmlHelper的RenderPartial方法,便是將一個(gè)Partial View輸出至Response中.
- public class HtmlHelper {
- ...
- internal virtual void RenderPartialInternal(
- string partialViewName,
- ViewDataDictionary viewData,
- object model,
- ViewEngineCollection viewEngineCollection) {
- ...
- ViewContext newViewContext = new ViewContext(...);
- IView view = FindPartialView(newViewContext, partialViewName, viewEngineCollection);
- view.Render(newViewContext, ViewContext.HttpContext.Response.Output);
- }
- }
雖然我認(rèn)為這里的做法是不太妥當(dāng)?shù)模ㄟ@點(diǎn)下次再談),但是這的的確確地表現(xiàn)了Render方法的設(shè)計(jì)意圖。只可惜在WebFormView中,Render方法卻違背了這一設(shè)計(jì):
- public class WebFormView : IView {
- ...
- public virtual void Render(ViewContext viewContext, TextWriter writer) {
- ...
- object viewInstance = ...;
- ...
- ViewUserControl viewUserControl = viewInstance as ViewUserControl;
- if (viewUserControl != null) {
- RenderViewUserControl(viewContext, viewUserControl);
- return;
- }
- ...
- }
- private void RenderViewUserControl(ViewContext context, ViewUserControl control) {
- ...
- control.ViewData = context.ViewData;
- control.RenderView(context);
- }
- }
對(duì)于Partial View,WebFormView會(huì)加載合適的ViewUserControl實(shí)例,并調(diào)用其RenderView方法生成內(nèi)容……但是,我們的writer參數(shù)到哪里去了?沒(méi)錯(cuò),對(duì)writer參數(shù)Find All Reference就會(huì)發(fā)現(xiàn),這個(gè)參數(shù)根本沒(méi)有用到。既然在這里就已經(jīng)拋棄了我們指定writer,那么接下來(lái)的邏輯再怎么搞也就“那么一回事兒”了。
如果您感興趣閱讀代碼的話(huà),會(huì)發(fā)現(xiàn)事實(shí)上最終這個(gè)對(duì)象被放入了一個(gè)新建的ViewPage對(duì)象中,然后調(diào)用ViewPage的RenderView方法生成視圖內(nèi)容:
- public class ViewPage : Page, IViewDataContainer {
- ...
- public virtual void RenderView(ViewContext viewContext) {
- ViewContext = viewContext;
- InitHelpers();
- // Tracing requires Page IDs to be unique.
- ID = Guid.NewGuid().ToString();
- ProcessRequest(HttpContext.Current);
- }
- }
瞧到這個(gè)HttpContext.Current了嗎?也就是說(shuō),無(wú)論RenderView方法何時(shí)調(diào)用,永遠(yuǎn)是向HttpContext.Current輸出內(nèi)容。這個(gè)設(shè)計(jì)很不合理,但是修改起來(lái)還是非常簡(jiǎn)單的,例如以下幾行代碼就可以得到差不多的效果:
- public static class HtmlExtensions
- {
- public static void Partial(this HtmlHelper htmlHelper, string partial)
- {
- var viewInstance = BuildManager.CreateInstanceFromVirtualPath(partial, typeof(object));
- var control = viewInstance as ViewUserControl;
- control.ViewContext = htmlHelper.ViewContext;
- control.ViewData = htmlHelper.ViewData;
- Page page = new ViewPage();
- page.Controls.Add(control);
- htmlHelper.ViewContext.HttpContext.Server.Execute(
- page,
- htmlHelper.ViewContext.HttpContext.Response.Output,
- false);
- }
- }
但是我不喜歡這種做法,因?yàn)樗鼪](méi)有遵循ASP.NET MVC既定的模型。ASP.NET MVC的確可以擴(kuò)展,但如果需要按照標(biāo)準(zhǔn)擴(kuò)展的話(huà),我們作的事情就多了:
繼承WebFormView,覆蓋RenderView方法。
繼承WebFormViewEngine,覆蓋CreatePartialView方法,返回剛創(chuàng)建的新類(lèi)。
在Application Start時(shí),使用新的ViewEngine類(lèi)替換ASP.NET MVC原有的視圖引擎。
但是在實(shí)際情況中,我會(huì)選擇使用使用第三種方法:下載ASP.NET MVC的源代碼,改寫(xiě),編譯。既然它是MS-PL的授權(quán)協(xié)議,為什么不自己動(dòng)手打一些Patch呢?事實(shí)上,我也打算使用這種方法來(lái)修補(bǔ)ASP.NET MVC的Bug或Design Issue,并發(fā)布一個(gè)臨時(shí)的新項(xiàng)目,就叫作……MvcPatch如何?
原文標(biāo)題:應(yīng)該算是WebFormView的一個(gè)Bug
鏈接:http://www.cnblogs.com/JeffreyZhao/archive/2009/09/14/webviewengine-bug-always-render-to-current-context.html
【編輯推薦】