内容


在 Spring Web MVC 环境下使用 Dojo

Comments

开始之前

关于本教程

本教程主要探讨如何在 Spring Web MVC 环境中使用 Dojo 的 widget,示例应用使用了 dojox.data.DataGrid,一个 Dojo Toolkit 1.2 新增的 widget 。 Dojo widget 与服务器交换数据的格式有很多种,本教程主要探讨在 Ajax 编程中比较常用的 JSON 格式的数据。本教程示例演示了 dojox.data.DataGrid 组件与 Spring Web MVC 控制器之间交换数据的细节,其中,服务器端使用了 Spring Json View 来呈现 JSON 数据。

先决条件

本教程假定读者已经熟悉 Spring Web MVC,并能配置相应的环境。但对 Dojo Toolkit 相关知识并无特别要求,教程里会细致讲解相关的 Dojo 知识。

系统要求

教程中的示例所用的工具和环境如下:

  • JDK 1.6.0+
  • Dojo Toolkit 1.2
  • Spring Framework 2.5.5 及其依赖项
  • Apache Maven 2.0.9
  • Tomcat 6.0.14
  • eclipse 3.4.1
  • Apache HTTP Server 2.2
  • Spring Json View

示例用到了 Apache Maven 的 jetty 插件,运行过程中如果显示部分依赖项安装不成功,也请读者从参考资源中找到相应网址,手动下载,并安装到 Maven 本地存储库中。

dojo.data 基础

传统桌面 MVC 模式中,决定视图(View)内容的是模型(Model),当模型的数据发生了改变,控制器(Controller)一般就会发出指令去刷新视图,桌面 MVC 模式中,模型起到了驱动作用。而用于 Web 的 MVC 则颠倒了视图和模型的驱动顺序,Web MVC 是由客户端对视图的访问,引发了控制器从模型中抽取相应的数据,在这里,视图起到了驱动作用。 Web MVC 的这种特性是由网络协议本身的特征决定的,在客户访问相应的视图之前,服务器不可能知道客户需要的是什么数据。

在 Web 应用的开发中,我们都习惯了由视图驱动的 MVC 模式,Dojo 之类 Ajax 工具包的出现却又像是回到了传统的桌面 MVC 模式,起驱动作用的重新变成了模型。 dojo.data 在 Dojo 工具包中起到的作用就相当于桌面 MVC 模式中的模型(Model),各种 Dojo widget 就相当于视图(View)。使用 Dojo widget 只需用某个标签(tag)声明,并在属性中指定提供数据的模型,其他的事情都由框架来完成,一旦与 widget 相关的数据发生了变化,则 widget 相应地会被更新。

dojo.data 中的特性(features)

Java 或 C++ 都是基于 class 来实现面向对象(Object Oriented),JavaScript 同样是面向对象的,但它的实现机理却很不一样,对此,本教程并不想深入探讨。为了更好理解 dojo.data 中的 feature ,不妨以一个 Java 程序员的眼光来看。撇去实现技术上的差异, dojo.data 中的 feature 就相当于 Java 中的接口(interface)。本教程后面的内容会把特性(feature)当作是接口(interface)的同义词来讲,而事实上两者并不相同,请大家牢记。

dojo.data 中的 feature 有以下四种:

  • dojo.data.api.Read 定义了从数据源读取数据的功能接口。
  • dojo.data.api.Write 定义了添加、修改、删除数据条目(item)的功能接口。
  • dojo.data.api.Identity 要求数据源中的每一条(item)数据都必须有唯一的标识符(很像是关系数据库表中的主键),dojo.data.api.Identity 提供的功能接口能够根据标识符快速访问相应的数据条目(item)。
  • dojo.data.api.Notification 定义了事件处理的接口,当数据条目(item)被添加、修改或删除时,就会触发相应的事件,通过覆盖 dojo.data.api.Notification 接口中相应的方法,即可处理对应的事件,这与传统图形界面事件编程机制是相同的。

这四种 feature 定义了 dojo.data 所能提供的所有功能接口,具体的实现类则根据不同的数据源(Data Source)来实现相应的功能。这几种 feature 之间的关系可用 UML 图表示,如图 1:

图 1 dojo.data 中四种 feature 的 UML 关系图
图 1 dojo.data 中四种 feature 的 UML 关系图
图 1 dojo.data 中四种 feature 的 UML 关系图

看起来,Dojo 的设计者通过 features 实现了面向对象设计中的一条基本准则——“接口与实现分离”。用 Java 程序员的眼光去看这些 features 实现的代码,会觉得比较另类,而最终的效果相去却并不太远。

面向对象设计中,与前一基本准则相联系的还有一条准则——“针对接口编程,而不是针对实现编程”。可惜的是,像 JavaScript 这样的动态语言不强调类型,JavaScript 中的变量可以是任何类型,对它的操作也就无所谓是针对哪个接口了。因此,使用 dojo.data 中各种具体实现类(Mastering Dojo 一书将其称为 Driver)时,需要程序员记住每个实现类所实现的接口有哪些。以下是几个常见的实现类:

  • dojo.data.ItemFileReadStore
  • dojo.data.ItemFileWriteStore
  • dojox.data.XmlStore

这几个类实现的接口如图 2 所示:

图 2 几个具体实现类的 UML 关系图
图 2 几个具体实现类的 UML 关系图
图 2 几个具体实现类的 UML 关系图

数据储备库

dojo.data 中各种 features 实现类的对象又被称为数据储备库,英文即 Data Store,是指用于从数据源(Data Source,可以是服务器的数据源,也可以是本地数据源)中获取数据,并提供特定功能接口的 JavaScript 对象。 Dojo 为多种数据源实现了相应的 Data Store,如用于处理 JSON 数据的 dojo.data.ItemFileReadStore,用于处理 CSV 数据的 dojox.data.CSVStore,甚至有用于处理 Google Picasa 服务数据的 dojox.data.PicasaStore 。 Dojo 每次发布新的版本都会带有新的 Data Store,这就意味着 Dojo 又可以处理一种新的数据源,但为处理数据提供的功能接口却保持不变。

dojo.data.api.Read 分析

从图 1 中可以看出 dojo.data.api.Read 提供了最基本的功能,其余几个 feature 都继承自 dojo.data.api.Read 。某个类若是实现了 dojo.data.api.Write 或 dojo.data.api.Identity 或 dojo.data.api.Notification,也就意味着它具有 dojo.data.api.Read 的功能。 Dojo 工具包所提供的所有数据储备库(Data Store)均实现了 dojo.data.api.Read 接口,一个不具备 dojo.data.api.Read 功能的数据储备库在 Dojo 中也不具备应用价值。 dojo.data.api.Read 构成了 Dojo Toolkit 数据处理的基础,通过分析 dojo.data.api.Read 就可以明了 dojo.data 所需数据的特定格式。

清单 1 给出了 dojo.data.api.Read 中几个常用函数的声明形式,由于 JavaScript 语言本身的特点,这些函数中的参数类型是以注释的形式给出的。

清单 1 dojo.data.api.Read 中几个成员的函数声明
getValue: function(/* item */ item, 
    /* attribute-name-string */ attribute, 
    /* value? */ defaultValue) 
getValues: function(/* item */ item, 
    /* attribute-name-string */ attribute) 
getAttributes: function(/* item */ item) 
hasAttribute: function(/* item */ item, 
    /* attribute-name-string */ attribute)

不难看出,这些用于读取数据的函数都用到了条目(item),dojo.data.api.Read 对数据的读取(getValue),是按 item 来读取的。某种能够被 dojo.data.api.Read 处理的数据源也必定是按 item 来分隔的。我们可以把数据储备库(Data Store)中的数据看作是由各种 item 构成的数组,通过 dojo.data.api.Read 接口定义的功能函数,就能从这个数组中抽取出相应 item 中的数据。清单 2 和清单 3 分别给出了符合 dojo.data.api.Read 接口要求的两种数据形式,清单 2 的数据用了 JSON 格式,而清单 3 的数据是 XML 格式。

清单 2 JSON 格式的数据实例
 { 
 identifier: 'name', 
 label: 'name', 
 items: [ 
 { name:'Africa', type:'continent', population:'900 million' }, 
        { name:'Egypt', type: ’ country ’ }, 
        { name:'Kenya', type:'country', 
        children:[{_reference:'Nairobi'}, {_reference:'Egypt'}] },}, 
 { name:'Nairobi', type:'city' }, 
 ] 
 }
清单 3 XML 格式的数据实例
<?xml version="1.0" encoding="ISO-8859-1"?> 
 <books> 
    <book> 
        <isbn>A9B57C</isbn> 
        <title>Title of 1</title> 
        <author>Author of 1</author> 
    </book> 
    <book> 
        <isbn>A9B57F</isbn> 
        <title>Title of 2</title> 
        <author>Author of 2</author> 
        <publisher>Addison Wesley</publisher> 
    </book> 
    <book> 
        <isbn>A9B577</isbn> 
        <title>Title of 3</title> 
        <author>Author of 3</author> 
    </book> 
 </books>

清单 2 所示的 JSON 数据有明显的 item 标记,名为 items 的属性,它的值即是由多个 item 构成的数组。清单 3 所示的 XML 数据虽没有 item 标记,但也可以看出 books 元素是由多个 book 元素构成的,每个 book 元素即相当于一个 item 。现在的 Dojo 已经能处理很多种形式的数据了,但 Dojo 对这些数据的处理都是以 item 为单位的。

另外,JavaScript 的弱类型特性使得数据储备库(Data Store)的格式方面有很大的灵活性。像 Java 这样的强类型语言中,如果要把不同的对象放在同一个数组中,那必须保证这些不同对象有共同的接口。 JavaScript 的数组却非常宽松,什么对象都可以放。从清单 2 和清单 3 可以看出,数据储备库中的数据虽是以 item 来分隔,但对每个 item 对象却没有严格的限制。一般情况下,item 只需提供某几个特定的属性(如清单 2 中的 name),其他就不用管了,可以在 item 中嵌入其他对象,其他对象再嵌点什么进去,也是没有关系,差不多就要到随心所欲的程度了。当然,需要什么形式主要是考虑业务的需要。

dojox.grid.DataGrid 入门

本教程在写作初期是预备以 dojox.grid.Grid 组件作为演示对象的,然而就在写作期间,Dojo Toolkit 1.2 发布了。新的 Dojo 工具包推出了新的 Grid 组件,叫做 dojox.grid.DataGrid,出于兼容以前版本的考虑,原先的 dojox.grid.Grid 依旧存在。官方网站的信息表明,在 Dojo 推出 2.0 版本之前,一直会保留 dojox.grid.Grid,但它不会得到维护。 dojox.grid.DataGrid 作为 Dojo 中新一代 Grid 组件,在配置上有所简化,与 dojo.data 的整合更加紧密。本教程将采用新的 dojox.grid.DataGrid 来演示实例。

dojox.grid.DataGrid 与 dojo.data 的关系

Bryan Forbes 在sitepen.com网站上发了篇blog,其中中讲了新的 Grid 组件取名为 DataGrid 的原因。与 Dojo 1.2 之前版本的 dojox.grid.Grid 相比,新 Grid 组件名字上多了一个词——“ Data ”,这里的“ Data ”即指 Data Store (数据储备库)。从内部实现机制来看, dojox.grid.Grid 是通过一个中间对象与 Data Store 交换数据的,而 dojox.grid.DataGrid 去掉了这层隔阂,原生地(natively)支持 Data Store 。

前面讨论过,dojo.data 中的 Data Store 对象在 MVC 设计模式中起到模型(Model)的作用,而 Dojo 所实现的 MVC 模式又是模型驱动的(Model Driven),因此,dojox.grid.DataGrid 中所有数据的更新其实都来自底层 Data Store 对象数据的更新。程序开发人员一旦实现了 dojox.grid.DataGrid 与底层 Data Store 对象的关联,他就不必再去关心视图更新的问题了。这时,开发人员的眼睛看到的只有 Data Store 对象提供的功能接口,即 dojo.data 所提供的各种 feature 。控制了 Data Store 对象中的数据,也就控制了 dojox.grid.DataGrid 呈现出来的数据。

使用 dojox.grid.DataGrid 的一般步骤如下:

  • 根据数据源的特点,构造 Data Store 对象。
  • 构造布局(Layout)对象,用于定义数据在 dojox.grid.DataGrid 中的呈现样式。
  • 构造 dojox.grid.DataGrid 对象,并将其关联到 Data Store 对象以获取数据,关联到 Layout 对象以获取数据显示样式。

示例应用的说明

本教程重在说明客户端 Dojo widget 与 Spring Web MVC 的交互过程,所用的示例非常简单:客户端通过 Ajax 请求从服务器获取数据,并在 dojox.grid.DataGrid 中呈现,用户通过双击 dojox.grid.DataGrid 中的某个单元格实现对数据的编辑,再通过 Ajax 请求将用户对数据的改变内容发送给服务器。

作为构建示例应用的第一部分,本节只考虑如何使用 dojox.grid.DataGrid 。 Data Store 对象所需数据就放在本地文件中。下面的示例假定 Dojo Toolkit 1.2 已经下载到本地,并放在 Web 服务器服务目录之下,目录名为“ dojoroot ”,参看图 3 。

图 3 Dojo Toolkit 1.2 在 Apache HTTP Server 2.2 中的目录结构
图 3 Dojo Toolkit 1.2 在 Apache HTTP Server 2.2 中的目录结构
图 3 Dojo Toolkit 1.2 在 Apache HTTP Server 2.2 中的目录结构

Dojo 基本配置

在 Web 服务器服务目录下新建 DataGridTest.html,并将清单 3 中的代码加入 head 标记之内。

清单 4 Dojo 基本配置
 <style type="text/css"> 
 _cnnew1@import "/dojoroot/dijit/themes/tundra/tundra.css"; 
 @import "/dojoroot/dojox/grid/resources/tundraGrid.css"; 
 </style> 
 <script type="text/javascript" 
 src="/ dojoroot/dojo/dojo.js" djConfig="parseOnLoad: true "> 
 dojo.require("dojo.parser"); 
 dojo.require("dojo.data.ItemFileReadStore"); 
 dojo.require("dojox.grid.DataGrid"); 
 </script>

本例采用 Dojo 的 tundra 主题,需要这两个 css 文件,其中 tundraGrid.css 尤其必需,如果没有引入,DataGrid 中的数据将无法正常呈现。

构造 Data Store 对象

新建 data.json 文件,文件内容清单 4 所示:

清单 5 data.json 文件内容
 { 
 "label":"name", 
 "identifier":"id", 
 "items":[ 
 {"id":20080230171101,"math":60,"physics":70,"chemistry":75,"name":" 张三 "}, 
 {"id":20080230171102,"math":20,"physics":80,"chemistry":95,"name":" 李四 "} 
 ] 
 }

清单 5 所示的数据的含义相当直观,表示两个学生的学号,以及数学、物理、化学三门课程的成绩。在 Dojo 中构造 Data Store 对象有两种方案,第一种是用 JavaScript 代码直接构造对象,第二种是在 HTML 标签中使用 Dojo 专用属性声明,使用第二种方案时需要配置 djConfig 参数,使得 parseOnLoad 的值为 true(参看清单 4)。本教程使用第二种方案,代码见清单 6:

清单 6 构造 Data Store 对象
 <span dojoType="dojo.data.ItemFileReadStore" jsId="dataStore" 
 url="data.json"> 
 </span>

其中的 dojoType 指明该对象的类型为 dojo.data.ItemFileReadStore 。 jsId 表示指向该对象的变量名,在其他 JavaScript 代码中可直接通过 jsId 的名字直接访问该 Data Store 对象。 url 参数指明数据源(Data Source)的位置,Data Store 对象即通过 url 所指向的位置获取相应的数据。在本例中,url 指向的即是清单 5 中 data.json 文件的内容。

构造布局(Layout)对象

Data Store 中的数据是以 item 为单位的,每个 item 对象都会成为 DataGrid 数据表中的一行。 JavaScript 中的对象可看作是多个属性(attribute)和对应值(value)构成的数组,当 item 对象映射到表格中时,就需要指明 item 对象的属性与表的列名之间的映射关系。对此,dojox.grid.DataGrid 组件的解决方案也有两种,第一种是在 HTML 标记中用专用的标签属性说明,第二种是定义一个专门的对象来描述这些映射信息。第二种方案灵活性更大,也更常用,本教程只讲第二种方案。 dojox.grid.DataGrid 布局对象的层次很多,要搞清楚为什么 dojox.data.DataGrid 的布局对象会如此复杂,需要结合它所要达到的控制效果来看,参看图 4:

图 4 dojo.data.DataGrid 显示实例
图 4 dojo.data.DataGrid 显示实例
图 4 dojo.data.DataGrid 显示实例

可以注意到,左边两列构成的视图(view)没有水平滚动条,而右边部分却可用水平滚动条调整视图。用 DataGrid Layout 的术语来讲,这个表有两个 view,分别是有滚动条的 view 和没有滚动条的 view,整个 DataGrid 的布局(Layout)就由多个 view 构成。每个 view 都由多个行(row)构成,布局对象中的 cells 属性就用来描述 view 中的多个 row,cells 属性的值就是由多个 row 构成的数组。每一行(row)又都会拥有多个单元格(cell),于是每一行就是由多个 cell 组成的数组。整个布局的构成需要三重数组,而每一重的特定含义又是非常明确的。对这三重组据简要归结如下:

  • 整个 layout 是由一个或多个 view 构成的数组。
  • 每个 view 是由一个或多个 row 构成的数组。
  • 每个 row 是由一个或多个 cell 构成的数组。

与图 4 所示效果对应的布局对象的代码如清单 7 所示:

清单 7 与图 4 对应的布局(Layout)对象
 var structure=[ 
 { 
 noscroll: true, 
 cells: [ 
 [ 
 {name: 'Alpha', value: '<input name="" type="checkbox" value="0">', rowSpan: 2}, 
 {name: 'Beta', get: get, width: 4.5} 
 ],[ 
 {name: 'Gamma', get: get} 
 ],[ 
 {name: 'Epsilon', value: '<button>Epsilon</button>', colSpan: 2} 
 ] 
 ] 
 }, 
 
 [ 
 [ 
 {name: 'Apple', value: '<button>Apple</button>', rowSpan: 3}, 
 {name: 'Banana', get: get, width: 20}, 
 {name: 'Kiwi', get: get, width: 20}, 
 {name: 'Pear', get: get, width: 20} 
 ],[ 
 {name: 'Beans', value: 'Happy to be grid!'}, 
 {name: 'Orange', value: '<img src="images/flatScreen.gif" height="48" width="48">'}, 
 {name: 'Tomato', value: '<input name="" type="file">'} 
 ],[ 
 {name: 'Zuchini', value: '<span style="letter-spacing: 10em;">wide</span>', colSpan: 3} 
 ] 
 ] 
 ];

清单 7 所示的数据纯为演示布局而用,Data Store 中的 item 数据最终要在 cell 对象中定义映射关系。本示例中布局对象的定义如清单 8 所示:

清单 8 定义布局(Layout)对象
 var gridLayout=[ // 布局是由多个 view 组成的数组
 { // 每个 view 对象包含名为 cells 的数组
 cells:[ //cells 是由多个 row 组成的数组
 [ //row 是由多个描述映射关系的对象组成的数组
 {name:' 学号 ',field:"id",width:"10em"}, 
 {name:' 姓名 ',field:"name",width:"5em"} 
 ] 
 ], 
 noscroll:true 
 }, 
 { 
 cells:[ 
 [ 
 {name:' 数学 ',field:"math",width:"5em" }, 
 {name:' 物理 ',field:"physics",width:"5em"}, 
 {name:' 化学 ',field:"chemistry",width:"5em"} 
 ] 
 ] 
 } 
 ];

其中,cell 对象中的 name 属性值即为列标题,field 属性值则指明对应的 item 对象的属性,这样就把 item 对象相应的属性映射到了表格的列中。

构造 dojox.data.DataGrid 对象

准备好了 Data Store 对象作为数据来源,显示数据的布局也已排定,接下来做的事情不过就是声明一个 dojox.data.DataGrid 对象,并将该组件对象与 Data Store 对象和布局(Layout)对象关联起来,代码如清单 9 所示:

清单 9 构造 dojox.data.DataGrid 对象
 <div id="grid" dojoType="dojox.grid.DataGrid" jsId="icGrid" 
 store="dataStore" clientSort="true" style="width: 30em; height: 15em;" 
 structure=gridLayout rowSelector="20px"> 
 </div>

其中,store 属性的值即清单 6 中的 Data Store 对象的 jsId 属性的名字,structure 属性的值即为清单 8 中布局对象的变量名。使用 rowSelector 是标识表格左边的行选择器,其效果大家可自行验证。

运行示例程序

启动 Web 服务器即可看到如图 5 所示效果。

图 5 运行效果
图 5 运行效果
图 5 运行效果

在 Spring Web MVC 环境中为 DataGrid 提供 JSON 格式的数据

通过前面的讨论,我们已经解决了客户端如何呈现 dojox.data.DataGrid 组件的相关技术问题,接下来要解决的就是服务器端如何向客户端提供数据的问题了。前面示例用的数据是放在固定文本中的,这在实际应用中几乎没有价值。大多数 Web 应用中,客户端组件所需的数据都是根据客户需要而动态生成的。另外,若用单独的 Servlet 为相应的客户端组件提供数据,同样没有太大的意义,因为这些组件所需的数据往往是和具体业务紧密结合在一起的,多个 Servlet 的使用会大大增加应用程序的复杂度。仅仅是出于某些组件的需要,服务器端就得提供相应的 Servlet,要是用上了上百个组件,每个组件都要有个 Servlet,那会是怎样的情形?任何一个 Web 开发者看到这种前景都会对开发失去信心。

与此同时,当前的 Java 世界中,开发 Web 应用却不用某种框架,已经是不可想象了。一旦你使用了某种 Web 框架,任何服务就必定要在这种框架的特定约束之下。像 Spring 这样的框架,如果为客户端 Dojo 组件提供数据的对象不能整合到 Spring IoC 容器中,那这样的解决方案差不多失去了实用价值。

摆在面前的问题,就是要在现有的 Web 框架中找到某种机制,能够比较方便地为 Dojo 组件提供数据。本教程主要讨论 Spring Web MVC 框架可提供的解决方案。

Spring Web MVC 处理请求的过程简述

DispatcherServlet 是 Spring Web MVC 的入口,一旦 DispatcherServlet 接收到请求,处理请求的大体步骤如下:

  • 从 WebApplicationContext 容器中找出相应的 Handler 处理请求,返回 ModelAndView 对象。
  • 若返回的 ModelAndView 对象包含某个 View 的名字,则从 WebApplicationContext 容器中找到 ViewResolver 来解析该 View 的名字,得到 View 对象。
  • 调用 View 对象的 render 方法,见清单 10 。
清单 10 View 对象调用 render 方法
view.render(mv.getModelInternal(), request, response);

这里专门把 View 对象调用 render 方法的代码贴出来,是为了更好地看清楚如下事实:

  • 客户端最终得到的响应(Response)其实是由 View 对象的 render 方法直接提供的。
  • 调用的 render 方法可直接使用 Model 中的数据。

解决方案

针对 Spring Web MVC 处理请求的过程,要找出为 Dojo 组件提供 JSON 数据的解决方案,整体思路如下:

  • 在处理请求的 Handler (大多数情况下用的是 Controller)中生成所需的数据,整个过程须符合 Java 的编程习惯。根据 Spring Web MVC 本身的特点,数据应该放在某个 Map 对象中,View 对象应能通过 getModelInternal() 方法访问到这些数据。
  • 定制 ViewResolver,使得相应的 View 的名字能够被解析。
  • 使用特定的 View,该 View 能够将 Model 中的数据转换成 Dojo 组件所需的 JSON 格式。

前面两条基本上能够保证,处理 Dojo 组件的数据请求与处理一般 Spring Web MVC 请求并无二致,不同之处其实只表现在 View 的设计中。这也正好体现了 Spring Web MVC 的灵活性:当我们让 Spring Web MVC 处理更多样的请求时,所需要的变化只是添加一种新的 View 类,其余都无需改变。

Spring Json View 简介

Spring 经常倡导的一句格言是“不要自己去发明轮子”,在寻找解决方案的过程中,我在sourceforge.net上发现已经有开发者做出这种 View,能够整合在 Spring Web MVC 框架中,并能在 View 中将 Model 的数据转换成 JSON 格式。这个叫做 Spring Json View 的项目在 sourceforge.net 中并不引人注目,整个开发团队仅有一人,在 2008 年 5 月 3 日发布 1.0 版后也再未更新。把 Spring Json View 用到解决为 Dojo 组件提供 JSON 数据的问题上来,那实在是再合适不过了。

Spring Json View 其实是 AbstractView 的实现,支持 Spring 框架的数据绑定(Data Binding)机制、数据验证(Validation)机制,但对我们来说,最有用的功能莫过于它能将 Model 直接转换成 JSON 字符串。

为示例应用配置 Spring Json View

本教程假定读者已经熟悉 Spring Web MVC 环境的配置,如果读者尚未搞清,可参看developerWorks 中国网站上的教程Spring Web Flow 2.0 入门

在搭建好 Spring Web MVC 的环境后,为示例应用配置 Spring Json View 可按以下步骤操作:

  • 配置 Controller 处理 Dojo Data Store 对象的请求,将所需的数据放入 Model 中,返回包含了 Spring Json View 名字的 ModelAndView 对象。
  • 配置 XmlViewResolver 解析 Spring Json View 的名字。

配置 Controller

示例应用显示了几个学生的学号、姓名以及数学、物理、化学三门课程的成绩,用 User 类来表示学生,代码如清单 11 所示:

清单 11 User 类
 package cn.developerworks.DataGridTest; 
 public class User { 
 private Long id; 
 private String name; 
 private int math; 
 private int physics; 
 private int chemistry; 
 // 默认构造方法,在进行 Data Binding 时会有用处
 public User() {} 
 public User(Long id,String name,int math,int physics,int chemistry) { 
 this.id=id; 
 this.name=name; 
 this.math=math; 
 this.physics=physics; 
 this.chemistry=chemistry; 
 } 
 // 重载 toString 方法是为后面的测试方便
 @Override 
 public String toString() { 
 return "ID="+this.getId()+",name="+this.getName()+"," 
 +"Math="+this.getMath()+",Physics="+this.getPhysics()+"," 
 +"Chemistry="+this.getChemistry(); 
 } 
 // 省略 getter 方法和 setter 方法
 }

我们已经按照 Java 语言的编程习惯写出了 User 类,Dojo Data Store 对象所需的数据在 Java 程序员的习惯里,应该以 User 对象的方式提供。在 Spring Web MVC 的配置文件中启用基于注解的配置,Data Store 请求的地址为“ /jsonTest.json ”,Controller 具体代码见清单 12:

清单 12 Controller 代码
 package cn.developerworks.DataGridTest; 
 // 省略 import 语句
 @Controller 
 @RequestMapping("/jsonTest.json") 
 public class JsonTestController { 
 @RequestMapping(method=RequestMethod.GET) 
 public ModelAndView processAjaxRequest(){ 
 Map<String, Object> model=new HashMap<String,Object>(); 
 model.put("identifier","id"); 
 model.put("label","name"); 
 List<User> items=new ArrayList<User>(); 
 items.add(new User(20080230171101L," 张三 ",60,70,75)); 
 items.add(new User(20080230171102L," 李四 ",20,80,95)); 
 model.put("items", items); 
 return new ModelAndView("jsonView",model); 
 } 
 }

这里不要忘记 dojo.data.Read 接口对数据格式上的要求,如果要提供 JSON 形式的数据,那么该 JSON 对象里面必须有一项属性的名字为“ items ”,items 属性的值是由多个 item 组成的数组。在清单 12 所示的代码中,我们先构造多个 User 对象,将这些 User 对象放入 List 容器中,再通过 model.put 方法把 List 对象放入 Model,该 List 对象在 Model 中对应的名字是“ items ”。剩下来的事情就都由 Spring Json View 来完成了,整个过程与通常情况下为 jsp 页面提供数据的代码并没有大的不同,唯一需要注意的就是要遵照 dojo.data.Read 所约定的格式。

配置 XmlViewResolver

清单 12 中返回的 ModelAndView 的名字为“ jsonView ”,通过配置 XmlViewResolver 可将“ jsonView ”解析为 Spring Json View 的实例,再由该实例将 Model 中的数据转换成 JSON 格式,并返回给 Data Store 对象。默认情况下,XmlViewResolver 会在名为 /WEB-INF/views.xml 的配置文件中查找相应的 View 。示例中 views.xml 放在 /WEB-INF/config/ 下,具体配置如清单 13 所示:

清单 13 view.xml 的配置
 <bean name="jsonView" class="org.springframework.web.servlet.view.json.JsonView"> 
 <property name="jsonWriter"><ref bean="jsonWriter"/></property> 
 </bean> 
 <bean name="jsonWriter" 
 class="org.springframework.web.servlet.view.json.writer.sojo.SojoJsonStringWriter"> 
 <property name="convertAllMapValues"><value>true</value></property> 
 </bean>

Spring Json View 可通过配置 StringJsonWriter 的一些属性来控制从 Model 转换到 JSON 字符串的细节,参看 Spring Json View 项目的相关文档可行到更多信息。

关于 ViewResolver 的配置内容见清单 14:

清单 14 配置 ViewResolver
 <bean 
     id="jspViewResolver" 
     class="org.springframework.web.servlet.view.InternalResourceViewResolver"> 
 <property name="viewClass" value="org.springframework.web.servlet.view.JstlView">
 </property> 
 <property name="prefix" value="/WEB-INF/jsp/"></property> 
 <property name="suffix" value=".jsp"></property> 
 <property name="order" value="2"></property> 
 </bean> 
 <bean 
     name="xmlViewResolver" 
     class="org.springframework.web.servlet.view.XmlViewResolver"> 
 <property name="order" value="1"></property> 
 <property name="location" value="/WEB-INF/config/views.xml">
 </property> 
 </bean>

这里用了两个 ViewResolver,分别针对 Dojo 的 Ajax 请求和普通 JSP 页面的请求。

运行示例应用

本教程附带的示例代码是用 Maven 组织的,系统在配置好 Maven 后,运行命令“ mvn jetty:run ”即可进行测试,具体效果与图 5 相同。

dojox.grid.DataGrid 如何向服务器提交数据的更改信息

到现在为止,示例应用的功能只能说完成了一半。从服务器获取数据的任务已经完成,现在的要做的是,如何把客户对表格内容的修改更新到服务器中。对此,Dojo 提供的解决方案叫做扩展点(Extension Point)机制,即由 Dojo 组件提供固定名字、参数的方法(即扩展点),开发人员可根据实际业务的需要覆盖(override)相应的方法(扩展点)。其设计思想源于面向对象中的继承机制,通过继承,子类会拥有父类的方法,但子类也可出于某些特定的需要,覆盖掉父类的方法。 Java 中 Object 类定义的许多方法大多具有类似的功能。

当我们更改了 DataGrid 中某个单元格、或某一行的数据时,希望以由此触发某个事件,通过事件处理向服务器更新数据。 Dojo 虽然名义上还只是工具包(Toolkit),实际上已经具备了框架所需的大部分要素,它已经为 DataGrid 组件定义了完整的事件机制,每种事件都提供了相应的扩展点,供开发人员根据需要进行覆盖。这些扩展点大多在 dojox.grid._Events 中定义,开发人员只要读过这些扩展点的名字,基本上就能明了所对应的事件了。一些常用扩展点如清单 15 所示:

清单 15 dojox.grid._Events 中常用的几种扩展点
 on[Header | HeaderCell | Cell | Row] MouseDown(event) 
 on[Header | HeaderCell | Cell | Row] Click(event) 
 on[Header | HeaderCell | Cell | Row] DoubleClick(event) 
 on[Header | HeaderCell | Cell | Row] ContextMenu(event) 
 on[Start | ApplyCell] Edit(inValue,inRowIndex) 
 on[Cancel | Apply] Edit(inRowIndex)

清单 15 所列出来的扩展点只是一部分,每个扩展点的含义都可直接通过名字的含义推测出来(在 Dojo 文档还不完整的情况下,这种推测是必须的)。后面的实例要用到 onApplyEdit 扩展点,这里需要多做解释。当 DataGrid 组件某行的数据更改被应用时,会触发事件并调用 onApplyEdit 方法,其参数 inRowIndex 表示被更改的行的序号。

在 Dojo 代码中覆盖扩展点的办法

在 Dojo 代码中覆盖扩展点可参看清单 16:

清单 16 覆盖扩展点的样例
 <div dojoType="dijit.form.Button"> 
 <script type="dojo/method" event="onClick" args="clickEvent"> 
 // 处理 click 事件的代码放在此处
 </script> 
 </div>

清单 16 的样例是基于标签声明方式的写法,用 JavaScript 代码来写,也就是把 onClick 属性用自定义的函数来覆盖。不过标签声明方式的写法中,type 属性有两种取值:

  • dojo/method,表示自定义函数会完全覆盖 Dojo 组件原来的处理函数。
  • dojo/connect,表示自定义函数只是连接到 Dojo 组件原来的处理函数中。

用 Java 程序员的眼光来看,第二种形式的覆盖(dojo/connect)就相当于是在子类的覆盖方法中使用了 super 关键字调用了父类的方法。有区别的地方在于,Dojo 组件原来的处理函数可能会在自定义函数之前被执行,也可能是在它之后,编程人员不可自作主张。这就好比是说,我们无法知道 super 调用是在覆盖代码之前,还是在它之后。

在 Dojo 中远程传送数据的办法

现在,我们已经可以通过 Dojo 的扩展点机制去捕捉用户对数据的更改,接下来就要解决如何向服务器传送数据的问题。 Dojo Toolkit 提供了三种方式向服务器传送数据,分别是:

  • 使用 XmlHttpRequest(XHR)对象。
  • 动态 iframe 元素。
  • 动态 script 元素。

这三种办法各有特点,在大多数情况下我们都使用 XmlHttpRequest 对象来异步传输数据。 Dojo Toolkit 中使用 XmlHttpRequest 向服务器发送请求的方法都是以 dojo.xhr 开头,dojo.xhrGet 的示例代码如清单 17 所示:

清单 17 dojo.xhrGet 示例
 dojo.xhrGet({ 
 url: "demo" , 
 content: item, 
 load: function(response){alert(response);}, 
 error: function(error){alert(error.message);} 
 });

dojo.xhrGet 方法的参数就是某个临时定义的 JavaScript 对象,为了完成约定的功能,这个临时对象必须提供一些固定的属性,如 url 属性是必不可少的,它表示 GET 请求的 URL 。如果需要通过 GET 传递其他参数的值,则可通过 content 属性来描述,清单 17 中的请求 URL 中会包含 item 中所有属性作为参数。 load 和 error 是两个回调函数,load 在 dojo.xhrGet 访问成功后得到调用,而一旦请求出错,则会调用 error 函数。

更改示例应用,使其能通过 Dojo 的事件处理机制向服务器更新数据

有了前面的讨论,接下来就可以修改示例应用,使其能够向服务器更新客户对数据的更改。客户端页面代码的更新操作步骤如下:

  • 将 Data Store 对象的类型改成 dojo.data.ItemFileWriteStore 以支持数据修改。
  • 更改 dojox.grid.DataGrid 的布局(Layout)对象,增加对编辑数据的支持。
  • 扩展 dojox.grid.DataGrid 的 onApplyEdit 扩展点,向服务器更新数据改动信息。

更改 Data Store 对象类型为 dojo.data.ItemFileWriteStore

再加过头看图 1,可以明白,改变 Data Store 对象并不会改变数据读取的方式,因为 dojo.data.ItemFileWriteStore 是继承自 dojo.data.ItemFileReadStore,拥有读取数据功能的同时,再添加了更改数据的接口。具体代码这里不再罗列,可参看教程的附件。不过有一点需要指出,如果开发者选择 dojo.data.api.Write 提供的 onSet 扩展点来更新数据,那扩展点需写到 dojo.data.ItemFileWriteStore 标签的内部。

更改 dojox.grid.DataGrid 的布局对象

作为示例,本教程中“数学”对应的列中的元素用普通编辑器更改,而“物理”对应列中的元素要用 dijit.form.NumberTextBox 组件来限制数字范围,对“化学”对应列则没有提供编辑器。新的布局对象代码如清单 18 所示:

清单 18 支持修改的布局对象
 var gridLayout=[ 
 { 
 cells:[ 
 [ 
 {name:' 学号 ',field:"id",width:"10em"}, 
 {name:' 姓名 ',field:"name",width:"5em"} 
 ] 
 ], 
 noscroll:true 
 }, 
 { 
 cells:[ 
 [ 
     {name:' 数学 ',field:"math",width:"5em",editable:true}, 
     {name:' 物理 ',field:"physics",width:"5em",editable:true, 
         type: dojox.grid.cells._Widget,widgetClass:"dijit.form.NumberTextBox",
	     constraint:{min:0,max:100},editorProps:{required:true}
	 }, 
     {name:' 化学 ',field:"chemistry",width:"5em"} 
 ] 
 ] 
 } 
 ];

这里尤其需要注意,对 dijit.form.* 的编辑器的支持,新的 Dojo 1.2 版的 DataGrid 与以往版本的定义方式不一样。如清单 18 所示,需要先将该列的类型(type)定义为 dojox.grid.cells._Widget,再通过 widgetClass 属性定义 dijit.form.NumberTextBox 。有了清单 18 所示的定义以后,当用户试图将物理对应的列的元素值改成 0 - 100 范围以外的数值时,会得到错误提示。

覆盖 onApplyEdit 扩展点

当用户编辑好某一行数据,用鼠标点击其他列时,会触发 dojox.grid.DataGrid 对象的 onApplyEdit 事件,相应的方法会得到调用,代码见清单 19:

清单 19 覆盖 onApplyEidt 扩展点
 <script type="dojo/method" event="onApplyEdit" args="inRowIndex"> 
 var item=icGrid.getItem(inRowIndex); 
 if(item){ 
 dojo.xhrPost({ 
 url:"<c:url value="/spring/jsonTest.json"/>", 
 content:item, 
 timeout:3000, 
 error:function(error){ 
 alert("The cell wasn\'t saved!"); 
 }, 
 load:function(response){ 
 console.debug(response); 
            } 
 }); 
 //console.dir(item); 
 } 
 </script>

Spring Web MVC 环境中获取用户对数据的更新

服务器端的更改相对简单一些,应用 Spring Web MVC 的数据绑定(Data Binding)机制,可方便地将客户端传送过来的 item 对象绑定到 User 对象中,作为示例,本教程只是简单地用了 println 方法,将绑定后的 User 对象打印输出,程序运行后在服务器的控制台可看到效果。对数据更新请求的处理代码如清单 20 所示:

清单 20 Spring Web MVC 中对数据更新请求的处理
 @RequestMapping(method=RequestMethod.POST) 
 public ModelAndView processAjaxPost(User user) throws IOException 
 { 
 System.out.println(user); 
 return new ModelAndView("jsonView"); 
 }

这里依旧使用了基于注解(Annotation)的映射机制,在 processAjaxPost 方法中的 User 类型的参数会自动与请求参数进行绑定,这样 dojo.xhrPost 方法中 content 属性的参数会与 User 对象绑定。

运行程序,观察效果

在工程目录下运行 mvn jetty:run,在浏览器打开初始页面,双击物理所在列的单元格可对数据进行编辑,如果输入数据超出范围,dijit.form.NumberTextBox 组件会提示数据输入出错,效果如图 6 所示:

图 6 dijit.form.NumberTextBox 生效
图 6 dijit.form.NumberTextBox 生效
图 6 dijit.form.NumberTextBox 生效

在输入正确的数据后,鼠标点击第一行某处,即可触发第二行的 onApplyEdit 事件。在服务器控制台可看到相应输出,如清单 21 所示:

清单 21 服务器端输出
 [INFO] Started Jetty Server 
 ID=20080230171102,name= 李四 ,Math=20,Physics=45,Chemistry=95

小结

至此,我们已经实现了客户端 Dojo 组件从服务器中获取数据,并且通过 Dojo 的事件处理机制将客户对数据的更改发送到服务器。由于服务器端的应用完全整合在 Spring Web MVC 环境中,因此就能充分利用 Spring IoC 容器提供的各种功能,与其他业务逻辑也能更方便地整合在一起。由于可以使用 Data Binding 和 Validator ,在 Spring Web MVC 环境中处理客户端的 Ajax 请求,与处理普通的 HTTP 请求相比,几乎没有大的不同。在清单 19 中,dojo.xhrPost 方法传送的对象只是简单地写成“ content:item ”,而服务器端使用了 RequestMapping 注解后,只是在处理函数中声明了参数“ User user ”,然后就能在处理函数中使用该对象了,就好像这个对象就在本地构造一样。唯一需要做出的约定就是,JavaScript 中 item 对象的属性要和 Java 中的 User Bean 的属性对应。假如默认的绑定不够用,那么还可以通过注册相应的属性编辑器(Property Editor)来解决。

dojo.data 能够处理的数据有很多种,并不仅限于本教程所使用的 JSON 。很多情况下,XML 数据也是个不错的选择,目前已经有多种 XML 与 Java Bean 绑定的技术(如 JAXB、XMLBean 等),通过这些技术构造出类似于 Spring Json View 的 View 来,应该不是难事。本教程就不讲那么多了,就此结束。


下载资源


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Web development, Java technology
ArticleID=361778
ArticleTitle=在 Spring Web MVC 环境下使用 Dojo
publish-date=12302008