Hello World
Spiga

为视图自定义辅助方法(下)

2009-04-29 22:35 by 老赵, 25280 visits

上一篇文章中,我们把繁冗的客户端脚本变成了可以由Visual Studio提示并轻易输出的服务器端辅助方法。但是,目前的做法还有不少可以改进的地方。我们编写辅助方法的目的便是为了简化开发,因此我们还可以在这条路上走的更远,让开发人员可以在使用我们的API时觉得更流畅,更有快感。

简化入口

目前,我们的辅助方法的使用方法大约如下:

<span>Name: </span> <!-- 必填 -->
<input type="text" name="user.Name" />
<% this.JQuery().Validate().Required("user.Name", "please provide your name!!!"); %>

而内置的辅助方法是这样使用的:

Html.ActionLink("Home", "Index")

比较两者就可以看出差别:

  • 由于只有“扩展方法”没有“扩展属性”,因此JQuery后面的括号不可少。
  • 由于利用了“扩展方法”,因此前面this关键字无法省略。

从某些角度看来,这么做其实并不会造成太大差别。不过在老赵看来,我们在前端定义视图时,我们并非在使用C#进行开发,而是在使用一种DSL——当然,是C#实现的Internal DSL。例如,在上面的代码中访问Html时,老赵并没有过分认为这是一个“属性”。在设计DSL,尤其是Internal DSL的时候,我们往往都需要考虑到实际需求,尽可能得到更加优美的语法。这样看来,this关键字和“作用不大”的括号也像是一种累赘。因此我们做的第一件事情,便是简化辅助方法的入口。

造成“累赘”的关键性因素便是“扩展方法”这个特性,但是如果我们要去除扩展方法的使用,就必须把“入口”直接定义在ViewPage中,这就要求我们的视图继承我们定义的基类,而不是直接继承ViewPage。这一点看上去是额外的工作,但是在实际项目中由于自然需要,我们一般都会定义这样的类型。而即使您的项目还没有自定义的基类,也建议您从一开始便采取这种做法。因为随着项目的发展,您很可能在某一时刻就发现您很渴望有一个统一的地方可以处理一些相同的逻辑,如果您这时再去修改项目中所有的视图,这会是一件劳民伤财的事情。相反,如果您一开始便定义了共同的基类,那么进行这种功能补充就会变得轻而易举。而且,即使您没有为基类添加任何功能,也不会造成任何损失。

目前,我们的基类只需要定义一个JQuery属性便可:

namespace MvcApp.Views
{
    public class ViewPageBase : ViewPage
    {
        private JQueryHelper m_jquery;
        public JQueryHelper JQuery
        {
            get
            {
                if (this.m_jquery == null)
                {
                    this.m_jquery = new JQueryHelper(this.ViewContext, this);
                }

                return this.m_jquery;
            }
        }
    }
}

由于不同的Page对象已经为我们做好了分离,我们已经不需要把JQueryHelper对象放在Page.Items集合中了。而现在,我们便可以使用这样的方式来改写之前的代码:

<span>Name: </span> <!-- 必填 -->
<input type="text" name="user.Name" />
<% JQuery.Validate().Required("user.Name", "please provide your name!!!"); %>

您是否觉得更清楚了一些呢?

链式API

经过改进之后,我们对于“年龄”文本框的判断便可以写成这样了:

<span>Age: </span> <!-- 必填,15到28之间的数字 -->
<input type="text" name="user.Age" />
<% JQuery.Validate().Required("user.Age", null); %>
<% JQuery.Validate().Number("user.Age", null); %>
<% JQuery.Validate().Range("user.Age", 15, 28, null); %>

这里的“坏味道”其实也非常浓郁:为什么user.Age重复了三遍呢?而JQuery.Validate()前缀也反复出现,这也违反了DRY原则,这并不是我们的目的,我们使用编写一套易于使用,流畅的API。说到“流畅”,“链式API”便是其中的典型之一。例如我们在使用StringBuilder的时候,其实便可以使用链式API来拼接字符串:

StringBuilder builder = new StringBuilder();
builder
    .Append("Hello ")
    .AppendLine("World")
    .AppendFormat("{0} {1}", "Hello", "World");

编写这种类型的API其实非常容易,只要每个方法都“返回当前对象”便可(或者可以进行后续操作的新对象)。再考虑到我们需要“定义一个元素”然后“多次使用”以避免反复,我们便可以构造一个新的类型辅助验证信息的收集:

public class JQueryValidation
{
    ...

    public ValidationElement Element(string name)
    {
        return new ValidationElement(name, this);
    }

    public class ValidationElement
    {
        internal ValidationElement(string name, JQueryValidation validation)
        {
            this.Name = name;
            this.Validation = validation;
        }

        public string Name { get; private set; }

        public JQueryValidation Validation { get; private set; }

        ...
    }
}

我们为JQueryValidation定义了一个内部类ValidationElement,并提供一个Element方法用于返回一个ValidationElement对象。在ValidationElement对象内部将保留元素的name以及用于收集信息的JQueryValidation对象。这样我们可以在ValidationElement内部重新定义一些验证方法,并且将调用直接委托给JQueryValidation对象上的方法。当然,每个方法最终还是会把当前ValidationElement对象返回,以便形成链式调用方式:

public class ValidationElement
{
    ...

    public ValidationElement Required(string message)
    {
        this.Validation.Required(this.Name, message);
        return this;
    }

    public ValidationElement Email(string message)
    {
        this.Validation.Email(this.Name, message);
        return this;
    }

    public ValidationElement Number(string message)
    {
        this.Validation.Number(this.Name, message);
        return this;
    }

    public ValidationElement Range(int min, int max, string message)
    {
        this.Validation.Range(this.Name, min, max, message);
        return this;
    }
}

看看结果:

<span>Age: </span> <!-- 必填,15到28之间的数字 -->
<input type="text" name="user.Age" />
<% JQuery.Validate().Element("user.Age")
       .Required(null)
       .Number(null)
       .Range(15, 28, null); %>

感觉如何?

多跨一步

不知您是否想到,其实这两篇文章中编写的辅助方法并非ASP.NET MVC转述,因为ASP.NET MVC使用WebForm来作为默认的视图,因此这些方法都可以轻而易举地用在WebForm模型中。我们唯一需要的就是……要不您自己思考一下?:)

总结

每个优秀的项目都会有一套完备的辅助方法类库来简化开发,这套类库随着项目的进行会不断完善,不断丰富。在编写辅助方法时,有时候会需要您尽可能地想象,不拘一格,唯一的目的便是优美易用的API。

Creative Commons License

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

Add your comment

30 条回复

  1. 老赵
    admin
    链接

    老赵 2009-04-29 23:20:00

    hmmmm……好像大家对这些内容都不太感兴趣的样子,还是吵架来的有意思,呵呵。

  2. yyliuliang
    *.*.*.*
    链接

    yyliuliang 2009-04-29 23:34:00

    每次看老赵的文章都有新的收获,可是沙发居然没有了!

  3. Vincent Liang[未注册用户]
    *.*.*.*
    链接

    Vincent Liang[未注册用户] 2009-04-29 23:46:00

    其实jQuery的validate控件本来使用自定义class已经足够优雅了,如果仅仅是这样搞多一个JQuery对象似乎有点画蛇添足(题外话,JQuery对象的“J”因为.Net规范必须大写,但又不太符合习惯)。我有点反对在代码或ViewHelpers过多地依赖或引用一些客户端的东西,有两个原因,一个就是有jQuery库改动的话要同时改动二进制代码的风险;第二就是好像有点走回服务器控件的端倪(当这种做法继续复杂下去的话)。

    目前正在努力向将团队向更纯粹的TDD进发,但发现很多程序员难以界定View的边界(这也是WebForm的遗毒),老赵说的“我们在前端定义视图时,我们并非在使用C#进行开发,而是在使用一种DSL——当然,是C#实现的Internal DSL”这句太有用了。

  4. DiryBoy
    *.*.*.*
    链接

    DiryBoy 2009-04-29 23:53:00

    今天脑袋有点不好使,先占前排。明天再来。

  5. 老赵
    admin
    链接

    老赵 2009-04-29 23:55:00

    @Vincent Liang
    其实我不介意JQuery的J小写,因为当DSL用其实没有太多限制,我个人认为。
    我不喜欢class的样子,因为class有其自己的样式含义,把样式和功能绑定在一起我不太喜欢。同样,加上特别的标签也是差不多的原因。
    jQuery改动,二进制改动是正常的,毕竟你用了新的库,我的辅助方法可以看作是一种“粘合剂”,肯定也要改的——不过如果jQuery改了,难道不用辅助方法的客户端代码就不需要改了吗?呵呵。
    至于说走回服务器端控件的端倪,我丝毫看不出来,我只是定义了一些服务器端的辅助方法,可以看作是在改进View Template的功能,一切都是在生成客户端的“文本”。
    还有就是,我是提供了一些示例方法和功能,可以用作后续的开发吧,比如通过Model自动生成验证脚本,这就比手动写验证规则要好多了。

  6. 新手村村长
    *.*.*.*
    链接

    新手村村长 2009-04-30 08:24:00

    顶一下老赵。
    有大师的风范---在写自定义辅助方法的同时,其实更重要的是:讲述了他在开发时的开发思路,这些重要的思想,对programmer来说,是最最重要的了。

  7. Nick Wang
    *.*.*.*
    链接

    Nick Wang 2009-04-30 08:31:00

    老赵直接做个open source library吧

  8. 隨風.NET
    *.*.*.*
    链接

    隨風.NET 2009-04-30 11:17:00

    确实便利不少 我也计划如此去做

  9. WCF技术联盟
    *.*.*.*
    链接

    WCF技术联盟 2009-04-30 12:02:00

    不知道以后ASP.NET MVC的流行程度能不能超过WEB FORM。我现在也在观望中

  10. flankerfc
    *.*.*.*
    链接

    flankerfc 2009-04-30 16:13:00

    菜鸟问一个问题:
    我在写View时 会有一些

    <% if(登录)
    {
    %>
    blablabla
    <% { %>

    当然不是很多, 因为逻辑是在Contorller中做的, View有时只是控制比如使用那个ViewUserControl 而已
    但是代码看起来很烦人
    有没有方法, 可以让我这样子写

    <if xxxx>
    blablabla
    </if>

    貌似用一些 Spark 等其他的View引擎可以实现
    但是 Spark那些 感觉变化太大了 不知道VS对其支持如何
    我只是在默认的View引擎上要多加这一个功能而已

    谢谢~~

  11. DiryBoy
    *.*.*.*
    链接

    DiryBoy 2009-04-30 19:14:00

    学习了,喜欢这种不断重构改进的思考过程。顺便问一个问题
    [QUOTE]--------------
    在ValidationElement对象内部将保留元素的name以及用于收集信息的JQueryValidation对象。这样我们可以在ValidationElement内部重新定义一些验证方法,并且将调用直接委托给JQueryValidation对象上的方法。
    -------------[/QUOTE]
    而在示例代码中,只是直接调用返回:
    [CODE]
    public ValidationElement Required(string message)
    {
    this.Validation.Required(this.Name, message);
    return this;
    }
    [/CODE]
    这样岂不是要同时维护两套相同的API,或者说两个地方吧……如果有一种方法,可以利用Refactor工具做一次就好了,当然,感觉自己在吹毛求疵而已。

    ps.楼上说的那种貌似在做 blogger 模板的时候碰到过……

  12. 老赵
    admin
    链接

    老赵 2009-04-30 20:36:00

    @flankerfc
    用aspx的话当然只能用它的语法了,不过我不觉得烦人阿。

  13. 老赵
    admin
    链接

    老赵 2009-04-30 20:36:00

    @DiryBoy
    可以设法自动生成。

  14. DiryBoy
    *.*.*.*
    链接

    DiryBoy 2009-04-30 20:58:00

    @Jeffrey Zhao
    不错,有启发了
    JQuery.Validate("user.Age")
    .Required(null)
    .Number(null)
    .Range(15, 28, null);
    会不会更好?

  15. 老赵
    admin
    链接

    老赵 2009-04-30 21:30:00

    @DiryBoy
    不能,作用其实是不一样的。

  16. 没注册[未注册用户]
    *.*.*.*
    链接

    没注册[未注册用户] 2009-04-30 22:12:00

    对于Web网站.我宁愿去多写Js也不去经过服务端的逻辑再进行呈现

  17. 老赵
    admin
    链接

    老赵 2009-04-30 22:17:00

    @没注册
    hmmm……那么您一定不喜欢rails,呵呵。

  18. Vincent Liang[未注册用户]
    *.*.*.*
    链接

    Vincent Liang[未注册用户] 2009-05-03 15:10:00

    @Jeffrey Zhao
    “jQuery改动,二进制改动是正常的,毕竟你用了新的库,我的辅助方法可以看作是一种“粘合剂”,肯定也要改的——不过如果jQuery改了,难道不用辅助方法的客户端代码就不需要改了吗?呵呵”

    这点有点商榷的余地,毕竟不用重新编译对于发布的风险还是比较小的。我个人认为二进制代码不应该依赖客户端脚本某个库。

    “至于说走回服务器端控件的端倪,我丝毫看不出来,我只是定义了一些服务器端的辅助方法,可以看作是在改进View Template的功能,一切都是在生成客户端的“文本”。 ”

    关于这个我愿意是继续复杂下去的情况,我相信之前ASP转换成ASP.NET WebForm的设计师也在这个模糊的边界上挣扎过。服务器端控件归根到底何尝又不是生成html呢?

  19. 老赵
    admin
    链接

    老赵 2009-05-03 15:23:00

    @Vincent Liang
    客户端脚本已经不是独立存在了,而是和辅助方法捆绑在一起的,例如如果你提供一个辅助方法类库,使用dll形式发布,那么客户端脚本可以作为dll的内嵌资源来实现。在客户端使用的DSL,不是JS脚本和二进制代码。如果要升级,也不用重新编译,只要升级二进制文件便可。这就是抽象的作用JQuery可以变,但是我的辅助方法“接口”可以不变。

    还有不能用“归根到底”来说明问题,否则所有东西一归根到底不都是变成“汇编”吗?一个东西主要还是看它带来的思维方式决定的,WebForm控件的抽象模型所带来的思维方式,与MVC中所使用的辅助方法可以说完全不同。
    我觉得你是不是不太喜欢rails框架,因为它被人津津乐道的优势之一,就是大量得为开发人员考虑的辅助方法,其实我们目前对asp.net mvc做再多补充还没有赶上rails自带的,呵呵。

  20. 孙孟
    *.*.*.*
    链接

    孙孟 2009-05-07 23:30:00

    有一点很不爽 就是必须把
    <script language="javascript" type="text/javascript">
    <%= this.JQuery().Validate().ToScripts("#form") %>
    </script>

    写到页面的下面 我一般都习惯写到head里 这个有什么办法解决吗?

  21. 老赵
    admin
    链接

    老赵 2009-05-07 23:35:00

    @孙孟
    这个没必要忌讳的,因为基本上是不可避免的。
    当然这里你可以用$(document).ready(...)这种写法。

  22. 孙孟
    *.*.*.*
    链接

    孙孟 2009-05-07 23:51:00

    @Jeffrey Zhao
    这里到无所谓

    不过突然想起来 怎么让c# 在页面加载完再执行呢? 类似WebForm的PreRender事件或者像jquery 的$(document).ready(...) 等页面结构生成完后再执行的代码 呵呵 纯属学习 !

  23. 老赵
    admin
    链接

    老赵 2009-05-07 23:54:00

    @孙孟
    服务器端无法知道客户端是否加载完毕的,不过有个Dispose还是什么的方法/事件可以知道服务器端的Page对象处理完毕了。
    如果你真要客户端加载完毕做些什么事情,就在客户端写代码回调一下吧。

  24. 孙孟
    *.*.*.*
    链接

    孙孟 2009-05-08 00:02:00

    @Jeffrey Zhao
    明白了 多谢

  25. 冰之玄岩,小小Programmer
    *.*.*.*
    链接

    冰之玄岩,小小Programmer 2009-05-12 15:25:00

    老赵,
    这些Jquery的方法和需要的参数都去哪里看啊??

  26. 言学
    *.*.*.*
    链接

    言学 2009-05-17 16:58:00

    请问老赵,是不是要分别定义ViewPage和ViewPage<TModel>各自的派生类作为自己的基类呢?
    觉得有点累赘啊。

  27. 老赵
    admin
    链接

    老赵 2009-05-17 17:58:00

    @言学
    always ViewPage<T>

  28. 言学
    *.*.*.*
    链接

    言学 2009-05-19 13:07:00

    @Jeffrey Zhao
    是不是在项目里面不使用ViewPage
    如果一个页面确实没有实体Model的话,就用ViewPage<object>
    老赵是这个意思吗?

  29. 老赵
    admin
    链接

    老赵 2009-05-19 13:23:00

    @言学
    是,就是这个意思。

  30. 言学
    *.*.*.*
    链接

    言学 2009-05-21 13:10:00

    @Jeffrey Zhao
    明白了,甚是感谢。

发表回复

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

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

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

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

使用Live Messenger联系我