代码改变世界

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版本中。