Hello World
Spiga

应该算是WebFormView的一个Bug

2009-09-14 15:33 by 老赵, 12908 visits

最近需要搞一些重要的功能,结果又遇到了意料外的障碍。于是又仔细地看了看ASP.NET和ASP.NET MVC的源代码,又发现了以前不曾知道的一些细节。您最多说ASP.NET WebForms模型不一定适合某些Web应用程序的开发,但是我想没有人可以否认ASP.NET中设计的巧妙——以及复杂程度。其实ASP.NET为我们留下了不少切入点,但几乎没什么书会提到这些切入点,我们只能从微软自己的框架中一探究竟。

不过这次我想谈的是ASP.NET MVC框架中的一个Bug,这个Bug在一般情况下不会出现问题,但是这的确违反了ASP.NET MVC自身的设计。这个问题就出在WebFormView对象的实现上。

WebFormView是一个视图对象的实现。而在ASP.NET MVC中,任何视图都需要实现一个IView接口:

public interface IView {
    void Render(ViewContext viewContext, TextWriter writer);
}

Render方法的目的自然是根据ViewContext对象中的数据,将视图内容输出至TextWriter中。例如在HtmlHelper的RenderPartial方法,便是将一个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);
    }
}

虽然我认为这里的做法是不太妥当的(这点下次再谈),但是这的的确确地表现了Render方法的设计意图。只可惜在WebFormView中,Render方法却违背了这一设计:

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);
    }
}

对于Partial View,WebFormView会加载合适的ViewUserControl实例,并调用其RenderView方法生成内容……但是,我们的writer参数到哪里去了?没错,对writer参数Find All Reference就会发现,这个参数根本没有用到。既然在这里就已经抛弃了我们指定writer,那么接下来的逻辑再怎么搞也就“那么一回事儿”了。

如果您感兴趣阅读代码的话,会发现事实上最终这个对象被放入了一个新建的ViewPage对象中,然后调用ViewPage的RenderView方法生成视图内容:

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);
    }
}

瞧到这个HttpContext.Current了吗?也就是说,无论RenderView方法何时调用,永远是向HttpContext.Current输出内容。这个设计很不合理,但是修改起来还是非常简单的,例如以下几行代码就可以得到差不多的效果:

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);
    }
}

但是我不喜欢这种做法,因为它没有遵循ASP.NET MVC既定的模型。ASP.NET MVC的确可以扩展,但如果需要按照标准扩展的话,我们作的事情就多了:

  1. 继承WebFormView,覆盖RenderView方法。
  2. 继承WebFormViewEngine,覆盖CreatePartialView方法,返回刚创建的新类。
  3. 在Application Start时,使用新的ViewEngine类替换ASP.NET MVC原有的视图引擎。

但是在实际情况中,我会选择使用使用第三种方法:下载ASP.NET MVC的源代码,改写,编译。既然它是MS-PL的授权协议,为什么不自己动手打一些Patch呢?事实上,我也打算使用这种方法来修补ASP.NET MVC的Bug或Design Issue,并发布一个临时的新项目,就叫作……MvcPatch如何?

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

25 条回复

  1. alonesail[未注册用户]
    *.*.*.*
    链接

    alonesail[未注册用户] 2009-09-14 15:43:00

    留意下!

  2. AlexLiu
    *.*.*.*
    链接

    AlexLiu 2009-09-14 15:48:00

    EventHandle里面的object sender 都做什么啊?赵老师?能不能和这个有点类似呢?

  3. 老赵
    admin
    链接

    老赵 2009-09-14 15:49:00

    @AlexLiu
    没看出任何关系来……

  4. 温景良(Jason)
    *.*.*.*
    链接

    温景良(Jason) 2009-09-14 16:06:00

    ASP.NET有开源?

  5. 老赵
    admin
    链接

    老赵 2009-09-14 16:08:00

    @温景良(Jason)
    如果你说是“公开源代码”,那么许多框架的确公开了源代码,就算没公开,用.NET Reflector也差不多。
    如果你是说Open Source,那么ASP.NET MVC是基于MS-PL开源的。

  6. 潜水冠军[未注册用户]
    *.*.*.*
    链接

    潜水冠军[未注册用户] 2009-09-14 16:38:00

    支持,到时候上链接。
    另,到底什么样的代码偶能用于商业目的捏?有参考没?
    Relfect的行吗?

  7. 老赵
    admin
    链接

    老赵 2009-09-14 16:41:00

    @潜水冠军
    要看license,有的是不允许你修改源代码的。

  8. 麒麟.NET
    *.*.*.*
    链接

    麒麟.NET 2009-09-14 17:14:00

    怎么会有这样的设计呢?接口已经不是契约而成为“累赘”了。Bug算不上,顶多算Bad Smell吧

  9. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-09-14 17:36:00

    麒麟.NET:怎么会有这样的设计呢?接口已经不是契约而成为“累赘”了。Bug算不上,顶多算Bad Smell吧




    很显然这玩意儿应该是为了兼容而存在的。

    老赵的代码不失为一种更好的解决方案。

  10. 老赵
    admin
    链接

    老赵 2009-09-14 17:40:00

    麒麟.NET:怎么会有这样的设计呢?接口已经不是契约而成为“累赘”了。Bug算不上,顶多算Bad Smell吧


    它的接口是合理的,这意味着设计是合理的。
    现在的问题是实现是有问题的。

  11. 老赵
    admin
    链接

    老赵 2009-09-14 17:41:00

    @Ivony...
    这里的代码只能算是临时的解决方案,我已经创建好MvcPatch项目了,会有更“标准”的做法的……

  12. aohan
    *.*.*.*
    链接

    aohan 2009-09-14 17:54:00

    需要新建一个项目,还发现了哪些有问题的地方呢?

  13. 老赵
    admin
    链接

    老赵 2009-09-14 18:03:00

    @aohan
    赫赫,慢慢来吧。
    这一块主要是在做性能优化时发现的问题。
    ASP.NET这一块比较混乱,我希望可以订一个准则出来。

  14. 小城故事
    *.*.*.*
    链接

    小城故事 2009-09-14 21:26:00

    今天终于有时间看下老赵新文章了,虽然不太懂,尽量理解吧。

  15. Arthraim
    *.*.*.*
    链接

    Arthraim 2009-09-15 00:55:00

    貌似最近非常高产……

  16. 卡通一下
    *.*.*.*
    链接

    卡通一下 2009-09-15 08:11:00

    Arthraim:貌似最近非常高产……


    同感!

  17. winter-cn
    *.*.*.*
    链接

    winter-cn 2009-09-15 09:06:00

    实现者并不总能正确理解设计者的意图 开发的人多了就这样 没办法

  18. 韦恩卑鄙
    *.*.*.*
    链接

    韦恩卑鄙 2009-09-15 10:34:00

    直接叫 asp.net mvc 1.0 sp1 吧

  19. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-09-15 11:16:00

    winter-cn:实现者并不总能正确理解设计者的意图 开发的人多了就这样 没办法



    应该不是这个问题,这个接口简单易懂。。。。当然要完美的实现并不容易,这种Wrapper很难做,要兼顾到两边的逻辑。。。。


    其实这也能看出ASP.NET原有架构的一些缺陷。。。。

  20. winter-cn
    *.*.*.*
    链接

    winter-cn 2009-09-15 13:45:00

    Ivony...:

    winter-cn:实现者并不总能正确理解设计者的意图 开发的人多了就这样 没办法



    应该不是这个问题,这个接口简单易懂。。。。当然要完美的实现并不容易,这种Wrapper很难做,要兼顾到两边的逻辑。。。。


    其实这也能看出ASP.NET原有架构的一些缺陷。。。。


    大方向上 你可以说完美的实现并不容易 但这却是个低级错误
    其实.net的架构做的很好但是实现的很烂的地方有不少 有兴趣的同学去看看Silverlight的ItemContainerGenerator和IItemContainerGenerator

  21. 老赵
    admin
    链接

    老赵 2009-09-15 13:54:00

    @winter-cn
    呵呵,架子搭得好,实现就偷懒了?

  22. 老赵
    admin
    链接

    老赵 2009-09-15 13:55:00

    @Ivony...
    我认为这里倒看不出ASP.NET架构上的缺陷。
    但是我体会到了,ASP.NET有些地方给人的“选择”太多,导致从表面看来,要做成一件事情的方法有不少。
    但是如果你一旦选错方向了,接下去就容易发现这里碰壁,那里碰壁。
    和virtual东virtual西差不多的道理。

  23. winter-cn
    *.*.*.*
    链接

    winter-cn 2009-09-15 14:15:00

    Jeffrey Zhao:
    @winter-cn
    呵呵,架子搭得好,实现就偷懒了?


    一般架子都是比较senior的人搭的 但是实现就不好说了

  24. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-09-15 15:38:00

    @Jeffrey Zhao

    大体上,我觉得产生这个问题的原因是IHttpHandler与HttpContext的紧耦合,而HttpContext又与ASP.NET的核心处理机制(现在看来应该是HttpApplication)紧耦合而造成的问题,使得在做Wrapper的时候,没有一个合适的切入点。例如如果IHttpHandler与HttpContext不是紧耦,我们可以自己做MvcContext传进去,而HttpContext与核心处理机制不是紧耦,我们就可以创建假的HttpContext。

  25. 老赵
    admin
    链接

    老赵 2009-09-15 15:46:00

    @Ivony...
    我不觉得啊,就像现在我修改的做法,就是正当且合适的,并不是一个trick或workaround。之前的问题是因为实现上的低级错误……

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我