WebFormView的标准修改办法及MvcPatch项目
2009-09-15 12:11 Jeffrey Zhao 阅读(12533) 评论(8) 编辑 收藏 举报上一篇文章中我提到WebFormView的实现破坏了IView对象设计思路,它会把视图内容直接生成至HttpContext.Current而不是Render方法指定的TextWriter中。目前,WebFormView.Render的调用方只有两个:ViewResult.ExecuteResult方法还有HtmlHelper.RenderPartial方法,但是这两者原本的目的地就是当前的HttpContext,因此在平时使用时WebFormView的错误实现并不会造成问题。
但是,如果我们在构建一个面向AJAX请求的Action,此时View的内容可能只是输出的一部分,甚至我们要对内容进行过滤/编码等额外操作。此时,我们就希望指定一个TextWriter用于收集内容——但是WebFormView自然无法做到。之前我提出了一种非常临时,非常山寨,非常简陋,绕弯,但是可行,或者说是可以“表现出解决问题的方法”的代码,修改一下便能说明问题:
public static class HtmlExtensions { public static string 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); TextWriter writer = new StringWriter(); htmlHelper.ViewContext.HttpContext.Server.Execute(page, writer, false); return writer.ToString(); } }
这个HtmlHelper的扩展方法Partial,和HtmlHelper自带的RenderPartial功能比较接近,不过Partial是将视图内容直接生成一个字符串并返回,RenderPartial方法是直接输出至当前HttpContext。因此它们在视图中的使用方式是不同的:
<% Html.RenderPartial("MyPartialView"); %> <%= Html.Partial("MyPartialView") %>
RenderPartial以<%开头,末尾有分号。而Partial以<%=开头,末尾没有分号。关于视图中的各种输出方式,我最近在阅读ASP.NET源代码时有更深的了解,下次我们再详谈。不过目前,我们还是专注于WebFormView的修改。
WebFormView目前问题的主要原因,是由于ViewPage和ViewUserControl两个类中缺乏合适的接口:
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); } } public class ViewUserControl : UserControl, IViewDataContainer { ... public virtual void RenderView(ViewContext viewContext) { viewContext.HttpContext.Response.Cache.SetExpires(DateTime.Now); // 这是ViewPage的子类,专用于生成独立的ViewUserControl内容 var containerPage = new ViewUserControlContainerPage(this); // Tracing requires Page IDs to be unique. ID = Guid.NewGuid().ToString(); // 其中会执行ViewUserControlContrainerPage的RenderView方法 RenderViewAndRestoreContentType(containerPage, viewContext); } }
可见,在ViewPage和ViewUserControl中各有一个RenderView方法,它们只包含一个ViewContext参数,但是却没有输出目的地。因此,最终它们使用HttpContext.Current这个邪恶的、臭名昭著的静态属性来生成内容。现在想起来,我当时在搞异步Action时,遭遇异常而不得不手动保持HttpContext就是这个原因造成的。于是我们目前修改的方式,便是为ViewPage和ViewUserControl增加一个额外的TextWriter参数:
public class ViewPage : Page, IViewDataContainer { ... public virtual void RenderView(ViewContext viewContext, ViewContext writer) { ViewContext = viewContext; InitHelpers(); // Tracing requires Page IDs to be unique. ID = Guid.NewGuid().ToString();ProcessRequest(HttpContext.Current);viewContext.HttpContext.Server.Execute(this, writer, false); } } public class ViewUserControl : UserControl, IViewDataContainer { ... public virtual void RenderView(ViewContext viewContext, ViewContext writer) { viewContext.HttpContext.Response.Cache.SetExpires(DateTime.Now); var containerPage = new ViewUserControlContainerPage(this); // Tracing requires Page IDs to be unique. ID = Guid.NewGuid().ToString(); RenderViewAndRestoreContentType(containerPage, viewContext, writer); } }
至于其他“顺其自然”的修改就不值一提了。
在我看来,这种问题可能不是ASP.NET MVC的设计问题(Design Issue),但是这也是它的内部实现的低级错误。对于此类问题,如果使用扩展的方式进行修改会显得沉重而麻烦,需要各种扩展和配置才能使用。之前项目中使用的便是基于“外部扩展”来回避“内部错误”的办法,而目前已经换成自行修改编译过的System.Web.Mvc.dll了。这个修改版本目前已经发布在CodePlex中的MvcPatch项目中,如果您感兴趣可以获取它的源代码并编译使用。
目前,MvcPatch包含两个修改,一个自然就是目前WebViewEngine的问题,而另一个便是之前提过的DefaultControllerFactory线程安全问题,以后我会补充更多设计方面的修改和扩展。在使用MvcPatch的时候,除了让您的项目引用正确的程序集之外,还必须将web.config文件中各类型的名称指向修改正确。因为使用ASP.NET MVC的模板创建项目时,它的web.config会使用GAC中注册的强类型的ASP.NET MVC 1.0程序集。如果修改不正确,在使用MvcPatch的程序集时便会遇到错误。
因此我们也可以发现,使用MvcPatch的好处在于,我们不需要使用外部扩展的方式来构建workaround,但是它也有缺点,那就是一些依赖于ASP.NET MVC 1.0程序集的项目无法和我们一起使用了。好在目前看起来这些项目都是些开源产品,如Telerik Extensions for ASP.NET MVC,我们可以下载它们的源代码,基于MvcPatch的程序集编译后再使用。
您别嫌麻烦,这就是享受开源的优势时需要付出的小小代价。
最后再谈一件事情。昨天晚上写完文章之后,我想到这种“补丁版本”并不是长久之计,因此在CodePlex上给ASP.NET项目提了一个Issue:WebFormView总是输出至HttpContext.Current而不是指定的TextWriter。今天早上发现已经有了ASP.NET团队成员回复,他们表示内部的代码库中已经修改了这个问题,将会体现在ASP.NET MVC 2的Preview 2版本中。