Hello World
Spiga

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

2009-04-29 00:24 by 老赵, 21999 visits

在编写ASP.NET MVC应用程序时,只依赖内置的视图辅助方法很难达到很高的生产力,即使是定义在MvcFutures中的补充类库,也很难满足项目的具体需求。此外,不同的项目有不同的特点,在很多时候也需要定义较为特殊的辅助方法,使开发人员能够更快,更方便地写出更容易维护的代码。这也是自定义视图辅助方法最主要的目的(没有之一)。而这次的文章,便是给出一个这方面的示例,可作为此类问题的一个参考。

预备

在编写客户端HTML时,进行客户端验证是最常见的工作之一。既然ASP.NET MVC集成了jQuery类库,那么我们不妨基于jQuery的Validate插件编写这部分功能。假设有如下表单:

<form method="post" action="" id="form">
    <p>
        <span>Name: </span> <!-- 必填 -->
        <input type="text" name="user.Name" />
    </p>
    <p>
        <span>Age: </span> <!-- 必填,15到28之间的数字 -->
        <input type="text" name="user.Age" />
    </p>
    <p>
        <span>Email:</span> <!-- 必填,且为合法Email -->
        <input type="text" name="user.Email" />
    </p>
    <input type="submit" value="Submit" />
</form>

如果使用JQuery Validate插件进行客户端校验,那么我们可以编写如下代码:

<script language="javascript" type="text/javascript">
    $("#form").validate({
        "rules": {
            "user.Name": { "required": true },
            "user.Age": {
                "required": true,
                "number": true,
                "range": [15, 28]
            },
            "user.Email": {
                "required": true,
                "email": true
            }
        },
        "messages": {
            "user.Name": { "required": "please provide your name!!!" },
            "user.Email": {
                "required": "email please...",
                "email": "valid email please..."
            }
        }, "onkeyup": false
    });
</script>

以上这段脚本的作用便是告诉jQuery校验哪个表单,对表单中的各个元素分别采取哪种校验方式,以及在发生错误的时候该提示什么信息(详细内容可查阅文档)。这里可能需要补充一段题外话。实际上jQuery.Validate非常灵活,可以有各种方式提供校验信息,例如直接为html元素加特殊标记。不过老赵认为那些方法的trick意味太浓,且不容易编写辅助方法协助开发,因此刻意忽略那些方式。

这样的结果可能会招一些前台开发人员欢喜,不过老赵觉得,至少有三个地方需要提高:

  1. 数据分散,提高维护难度。例如HTML元素和校验规则分离,校验规则
  2. 对于开发人员来说,编写JSON较不方便,容易发生错误。
  3. 缺少对于“自动校验”的支持,需要完全手写。

而我们编写的辅助方法,主要就是面向前两点,而最后一点可以说是自然而然的事情。

入口

任何方法都需要一个入口,这个入口可能是某个静态类,甚至直接定义在global下最直接的“方法”。而ASP.NET MVC视图的辅助方法的入口往往定义在ViewPage中。例如与HTML相关的辅助方法定义在ViewPage.Html属性下,而与URL相关的辅助方法都定义在ViewPage.Url属性下(ViewUserControl和ViewMasterPage情况也一样)。这样的入口区分,其实也是为辅助方法进行了分类,也让用户可以在IDE的帮助下快速定位到所需的辅助方法。在编写一些简单的自定义辅助方法时,我们可以利用C# 3.0的扩展方法特性,将方法定义在HtmlHelper和UrlHelper上,这样便可直接在页面上使用。而对于一些特殊情况,例如目前的状况,我们便需要重新定义一个入口,以便我们的辅助方法可以以此作为基础进行扩展。为此,我们把这一重担交给JQueryHelper类:

public class JQueryHelper
{
    public JQueryHelper(ViewContext viewContext, ViewPage page)
        : this(viewContext, page, RouteTable.Routes)
    { }

    public JQueryHelper(
        ViewContext viewContext,
        ViewPage page,
        RouteCollection routeCollection)
    { 
        this.ViewContext = viewContext;
        this.Page = page;
        this.RouteCollection = routeCollection;
    }

    public RouteCollection RouteCollection { get; private set; }

    public ViewContext ViewContext { get; private set; }

    public ViewPage Page { get; private set; }
}

生成JQueryHelper对象时,我们会保留目前的上下文,让各种扩展方法自行选用。至于保留哪些成员,这并没有太多限制,一般来说够用便可,如有不足,我们按需补充便是。接下来便是要让页面可以访问到这个辅助对象了,与Html和Url这些现有的入口不同,我们使用扩展方法,而不是属性来提供入口。这样做的好处便是在毫无侵入的情况,提供了较为友好的语法,这也是扩展方法的美妙之处:

public static class JQueryExtensions
{
    public static JQueryHelper JQuery(this ViewPage page)
    {
        var key = typeof(JQueryHelper);
        var jquery = page.Items[key] as JQueryHelper;

        if (jquery == null)
        {
            page.Items[key] = jquery = new JQueryHelper(page.ViewContext, page);
        }

        return jquery;
    }
}

ViewPage的Items属性是一个页面级别的存储容器,可以使用键/值的方式存放任意数据,我们这里便利用这个特性,为每个页面提供唯一的JQueryHelper对象。

校验

因为一个页面可能有多个需要校验的区域,因此我们需要提供一个机制,能够保存多个“批次”的校验信息。于是,我们编写一个JQueryValidation类用来存放“一批”校验信息:

public class JQueryValidation
{
    public JQueryValidation(ViewPage page)
    {
        this.Page = page;
    }

    public ViewPage Page { get; private set; }

    ...
}

当然,在JQueryHelper这个入口上,需要有一个访问校验信息的方式。在这里,我们使用字符串来标识“一批”校验信息。此外,我们也提供了默认重载以方便某些简单场景下的使用:

public static class JQueryValidationExtensions
{
    public static JQueryValidation Validate(this JQueryHelper jquery)
    {
        return jquery.Validate("(default)");
    }

    public static JQueryValidation Validate(this JQueryHelper jquery, string name)
    {
        var key = typeof(JQueryValidation) + "+" + name;
        var page = jquery.Page;
        var validation = page.Items[key] as JQueryValidation;

        if (validation == null)
        {
            page.Items[key] = validation = new JQueryValidation(page);
        }

        return validation;
    }
}

作了那么多的铺垫,接下来便是最关键辅助方法编写了。通过对JavaScript代码进行分析之后,我们决定采用典型的“记录 - 汇总”模式来编写这个方法1。“记录 - 汇总”模式的原则便是通过“记录”将信息汇总后输出,在作为页面辅助方法时,我们可以在任何地方“记录”校验信息,然后在合适的地方输出一段汇总后的脚本。由于我们所需要的JavaScript非常规整,因此实现这个需求并不困难。

首先是记录,对于每个

public class JQueryValidation
{
    private Dictionary<string, Dictionary<string, object>> m_rules =
        new Dictionary<string, Dictionary<string, object>>();

    private Dictionary<string, Dictionary<string, string>> m_messages =
        new Dictionary<string, Dictionary<string, string>>();

    private void AddRuleAndMessage(string name, string rule, object value, string m
    {
        if (!this.m_rules.ContainsKey(name))
            this.m_rules[name] = new Dictionary<string, object>();
        this.m_rules[name][rule] = value;

        if (!String.IsNullOrEmpty(message))
        {
            if (!this.m_messages.ContainsKey(name))
                this.m_messages[name] = new Dictionary<string, string>();
            this.m_messages[name][rule] = message;
        }
    }

    public void Required(string name, string message)
    {
        this.AddRuleAndMessage(name, "required", true, message);
    }

    public void Email(string name, string message)
    {
        this.AddRuleAndMessage(name, "email", true, message);
    }

    public void Number(string name, string message)
    {
        this.AddRuleAndMessage(name, "number", true, message);
    }

    public void Range(string name, int min, int max, string message)
    {
        this.AddRuleAndMessage(name, "range", new int[] { min, max }, message);
    }

    ...
}

我们使用两个字典分别存放规则的描述(m_rules)与错误提示(m_messages),由于信息的统一,我们只需编写一个AddRuleAndMessage方法便可满足所有需要,而其他的方法只是定义了一个良好的接口,然后简单地把信息委托给AddRuleAndMessage而已。

最后便是汇总,我们使用JavaScriptSerializer把规模的描述与错误提示序列化成JSON字符串并输出:

public string ToScripts(string form)
{
    JavaScriptSerializer serializer = new JavaScriptSerializer();
    StringBuilder builder = new StringBuilder();

    builder.Append("$(");
    serializer.Serialize(form, builder);
    builder.Append(").validate(");
    serializer.Serialize(
        new
        {
            rules = this.m_rules,
            messages = this.m_messages,
            onkeyup = false
        }, builder);
    builder.Append(");");

    return builder.ToString();
}

使用

“记录 - 汇总”,无它耳。

<form method="post" action="" id="form">
    <p>
        <span>Name: </span> <!-- 必填 -->
        <input type="text" name="user.Name" />
        <% this.JQuery().Validate().Required("user.Name", "please provide your name!!!"); %>
    </p>
    <p>
        <span>Age: </span> <!-- 必填,15到28之间的数字 -->
        <input type="text" name="user.Age" />
        <% this.JQuery().Validate().Required("user.Age", null); %>
        <% this.JQuery().Validate().Number("user.Age", null); %>
        <% this.JQuery().Validate().Range("user.Age", 15, 28, null); %>
    </p>
    <p>
        <span>Email:</span> <!-- 必填,且为合法Email -->
        <input type="text" name="user.Email" />
        <% this.JQuery().Validate().Required("user.Email", "email please..."); %>
        <% this.JQuery().Validate().Email("user.Email", "valid email please..."); %>
    </p>
    <input type="submit" value="Submit" />
</form>

<script language="javascript" type="text/javascript">
    <%= this.JQuery().Validate().ToScripts("#form") %>
</script>

请看最后生成的HTML:

<script language="javascript" type="text/javascript">
    $("#form").validate({ "rules": { "user.Name": { "required": true }, …
</script>

因为编写了客户端辅助方法,我们已经把繁冗的客户端脚本变成了可以由Visual Studio提示并轻易输出的服务器端辅助方法——您觉得如何,是否满意?

其实老赵还不满意,不过接下去的工作,我们下次再继续吧。

 

注1:什么?您说没有听说过“记录 - 汇总”这个模式?嗯,这是正常的,因为这是老赵“发明”的模式之一。

Creative Commons License

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

Add your comment

14 条回复

  1. Anytao
    *.*.*.*
    链接

    Anytao 2009-04-29 00:37:00

    好像在最佳实践代码中看到这个影子。。。

  2. 老赵
    admin
    链接

    老赵 2009-04-29 00:44:00

    @Anytao
    没错,其实我还是在陆陆续续把这些东西写出来……虽然略有补充,但是现在觉得当时准备的东西实在太多了。

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

    温景良(Jason) 2009-04-29 00:57:00

    啥意思啊,看不太明白

  4. Nick Wang
    *.*.*.*
    链接

    Nick Wang 2009-04-29 05:54:00

    是否能够直接把字段的html代码生成与验证一同封装,例如
    <span>Age: </span> <!-- 必填,15到28之间的数字 -->
    <input type="text" name="user.Age" />
    <% this.JQuery().Validate().Required("user.Age", null); %>
    <% this.JQuery().Validate().Number("user.Age", null); %>
    <% this.JQuery().Validate().Range("user.Age", 15, 28, null); %>

    直接在服务器端View里:
    <%=HtmlHelper.TextField("Age:", "user.Age")
    .Required("Age is required")
    .Number()
    .Range(15,28) %>

  5. 史泽昊
    *.*.*.*
    链接

    史泽昊 2009-04-29 08:04:00

    注1:什么?您说没有听说过“记录 - 汇总”这个模式?嗯,这是正常的,因为这是老赵“发明”的模式之一。

    哈哈,厉害

  6. 阿牛
    *.*.*.*
    链接

    阿牛 2009-04-29 08:11:00

    正需要这一块的东西。

    现在生成的模型里,已经有了 [Required]元属性,是不是可以利用一下?

  7. 老赵
    admin
    链接

    老赵 2009-04-29 09:04:00

    @Nick Wang
    你在想你是不是看过我下一半文章了,就是做成你说得这样了,呵呵……
    在上次的ppt里已经有相关代码了,大约是这样的:JQuery.Validate().Element("user.Age").Required(null).Number(null).Range(15, 28, null);

  8. 老赵
    admin
    链接

    老赵 2009-04-29 09:07:00

    --引用--------------------------------------------------
    阿牛: 正需要这一块的东西。

    现在生成的模型里,已经有了 [Required]元属性,是不是可以利用一下?
    --------------------------------------------------------
    根据需要自行补充功能肯定是没有问题的,不过你说得这一点和这篇文章关系不是很大,这篇文章是把目光集中在纯粹的客户端辅助方法上。
    预计再下一次,就会有涉及了。

  9. Nick Wang
    *.*.*.*
    链接

    Nick Wang 2009-04-29 10:01:00

    @Jeffrey Zhao
    看来我有未卜先知的能力 :p

  10. E_wait
    *.*.*.*
    链接

    E_wait 2009-04-30 08:38:00

    哈哈,在您之前的单元测试代码里就使用了该辅助方法,那份示例代码对我最大的帮助就是学习了您的一些代码设计方法.非常非常的感谢

  11. 代震军
    *.*.*.*
    链接

    代震军 2009-04-30 12:34:00

    我是前两个看的视频

  12. 范福生
    *.*.*.*
    链接

    范福生 2009-06-04 18:33:00

    能否提供示例代码下载

  13. 蛙蛙王子
    *.*.*.*
    链接

    蛙蛙王子 2009-09-24 15:55:00

    IDataErrorInfo是干啥的呀?

  14. 蛙蛙王子
    *.*.*.*
    链接

    蛙蛙王子 2009-09-24 15:57:00

发表回复

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

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

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

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

使用Live Messenger联系我