本文介绍如何利用 Dojo EnhancedGrid 控件进行数据异步传输和保存,让使用者在浏览器中操作表格能像 Excel 一样方便,并且能实现数据自动保存到数据库中。Dojo EnhancedGrid 提供了一套完整的 API 和解决方案,避免了软件开发人员去开发繁琐的页面代码,轻松实现页面数据的操作功能。
设计概要:介绍示例应用程序的应用和体系结构
业务场景:
客户需要做一个公司信息管理界面,管理员进入后只要点击查询按钮就能查出所需要的信息,在查询的过程中页面不需要刷新,减少用户的等待时间。
前台框架设计:struts 2.0+Dojo 1.7.1
业务逻辑:Spring 3.1
持久层:mybatis 3.0.6 + DB2 9.7
回页首
展现层:介绍示例应用程序的展现层的设计
Dojo EnhancedGrid 简介
顾名思义,EnhancedGrid 就是 Grid 的加强版,从 Dojo 1.4 开始就有了。它的存在是由于原先的 DataGrid 虽然功能强大,但很多地方写得比较死,不太便于扩展。因此,继承自 DataGrid 的 EnhancedGrid 就提供了一种较为灵活的插件机制,一方面能使后来的开发者较少受到现有代码的制约;另一方面也是为功能日益繁多的 Grid 瘦身(不用的插件可以不加载)。EnhancedGrid 提供了如下一些新的插件功能:
- Nested Sorting – 多行排序。
- Indirect Selection – 通过单选按钮和复选按钮选择行。
- Filter – 支持自定义类型规则来过滤表格的内容。
- Exporter – 把表格内容导出到各种内容。
- DnD – Drag-and-drop( 拖放 ) 支持行 / 列 / 表格单元,在单元内外都可以。
- Pagination – 分页功能。
- CellMerge – 合并相邻的表格单元到一行中。
- Search – 通过正则表达式和字符串匹配的方式来查找表格中的内容。
EnhancedGrid 很像 client/server 架构,基本上一个 grid 就是一个 Excel spreadsheet, 通常它被包裹在一个大的表格里面,借于 HTML 的规范,它用自己的形式来展示表格。具体展现形式如下:
图 1. EnhancedGrid 例图
属性简介:
Dojo EnhancedGrid 有很多属性,这里仅介绍常用的几个属性:
表 1. EnhancedGrid 常用属性介绍
如何使用 Dojo EnhancedGrid
引入 Dojo EnhancedGrid 控件
要使用 Dojo EnhancedGrid 控件,首先要把此控件加入页面 :
清单 1. 在 JS 文件中引入 Dojo 代码
require(['dojo/_base/array', 'dojo/_base/lang', 'dojo/_base/event', 'dojo/on', 'dojox/grid/EnhancedGrid', 'dojo/data/ItemFileWriteStore', 'dijit/form/Button', 'dojo/dom', 'dojo/parser', 'dojo/domReady!'], function(array, lang, event, on, EnhancedGrid, ItemFileWriteStore, Button, dom, parser){ parser.parse();}
声明表格的标头
清单 2. 在 Dojo EnhancedGrid 中使用 JSON 定义表头
// 定义 layout 布局,就是定义标题的列标题,宽度 var layout = [[ {'name': 'Id', 'field': 'id', 'width': '3%', editable: true}, {'name': 'Agent Team', 'field': 'agentTeam', 'width': '8%', editable: true}, {'name': 'Community UUid', 'field': 'communityUuid', 'width': '17%', \ editable: true}, {'name': 'View members', 'field': 'communityUuid', 'width': '10%',\ formatter: formatHref}, {'name': 'Forum name', 'field': 'forum', 'width': '10%', editable: true}, {'name': 'Forum Type', 'field': 'forumType', 'width': '6%', editable: true,\ type: dojox.grid.cells.Select, options:["Awards","Program","FAQ","HelpDesk",\ "Best Practices"], values: [ '1', '2','3','4','5' ],formatter: formatForumType}, {'name': 'URL', 'field': 'url', 'width': 'auto', editable: true} ]];
创建 EnhancedGrid 控件对象
清单 3. 创建 EnhancedGrid 控件对象
grid = new EnhancedGrid({ id: 'grid', store: store,// 连接 datastore 控件 structure: layout, rowSelector: '20px'}); /* 把 grid 控件加入到 HTML div 标签中 */ grid.placeAt("gridDiv"); );
下面来自定义一个新增函数来实现异步保存数据到数据库中:
清单 4. 自定义函数来实现异步添加数据
function onNew(item) { // 得到表格单元的内容 var agentTeam=item.agentTeam; var communityUuid=item.communityUuid; var forum=item.forum; var url=item.url; var forumType=item.forumType; dojo.xhrPost({ //The URL of the request url: "/yourproject/new", // 要处理内容的格式 handleAs: "json", // 请求参数 content: { agentTeam: agentTeam, communityUuid: communityUuid, forum: forum, url: url, forumType: forumType }, // 错误处理函数 error: function(error, ioArgs) { alert(error.message); } }); // 添加记录后自动刷新页面 ; setTimeout(function() {window.location.reload();},2000); }
dojo.xhrPost 是使用 RESTFUL POST 的方式来添加新数据。
添加新增和删除按钮:
清单 5. 新增按钮 HTML 代码
<! —添加新增按钮 - <p align="center" role="region" aria-label="operation buttons"> <span data-dojo-id='button2' data-dojo-type='dijit.form.Button'> Add One Row </span> <! —添加删除按钮 - <span data-dojo-id='button1' data-dojo-type='dijit.form.Button'> Remove Selected Rows </span> </p>
将函数绑定到 EnhancedGrid 事件上
最后将自定义函数绑定到 EnhancedGrid 事件上:
清单 6. 绑定事件函数代码
grid = new EnhancedGrid({ id: 'grid', store: store,// 连接 datastore 控件 structure: layout, rowSelector: '20px'});/ /* 把 grid 控件加入到 HTML div 标签中 */ grid.placeAt("gridDiv"); // 以下为绑定函数到特定的事件 // 绑定函数到新增功能 dojo.connect(store, "onNew", this,onNew); // 绑定函数到修改功能 dojo.connect(store, "onSet", this,onSet); // 绑定函数到删除功能 dojo.connect(store, "onDelete", this,onDelete); /* 为 button2 按钮添加 click 处理事件 */ on(button2,'click', function(e){ /* set the properties for the new item: */ var myNewItem = {id: '', agentTeam: 'new',communityUuid:'new',forum: 'new', \ url: 'new', forumType: 0}; /* Insert the new item into the store:*/ store.newItem(myNewItem);// 新增行 } ); /* 为 button1 按钮添加 click 处理事件 */ on(button1,'click', function(e){ /* 选择被选中的行 */ var items = grid.selection.getSelected(); if(items.length){ if(!confirm("Are you sure to delete?")){ return false; } /* 循环遍历所选择的行 */ array.forEach(items, function(selectedItem){ if(selectedItem !== null){ /* Delete the item from the data store: */ store.deleteItem(selectedItem); } /* end if */ }); /* end forEach */ }else{ alert("Please select row(s)"); } event.stop(e); } );
dojo.connect 模型
此处用到了 dojo.connect 模型:
dojo.connect(store, "onNew", this,onNew);
只要用户出发了 onNew 事件 ( 创建一条新记录 ),就会触发 onNew 函数(用户可以自己定义)。
每个流行的工具包中,总有一些异常出彩的闪光点。dojo.connect 就是 Dojo 工具包中,与 JavaScript 事件机制相关的重磅功能。
在 JavaScript 的使用场景中,我们经常需要侦听某些事件的触发,然后进行相应的(函数)处理。比如最常见的,当点击登录页面的登录节点时,JavaScript 能够察觉到,并随之将用户登录信息发送到后台。从表面上看,dojo.connect 就是一个单纯的函数,完成单纯的事件关联功能。但由于支持参数的灵活配置及和其他 Dojo 函数的有机组合,有时候可以造成一些奇妙效果。而探索这些效果并用于解决一些特定的问题,正是技术的魅力所在吧!
这儿很浅显地讨论一些目前能想到的 dojo.connect 特性。也欢迎有兴趣的同志给出更多的应用场景。
回页首
服务层:介绍示例应用程序的服务层的设计
本文的服务层采用的是 Spring Framework, 关于 Spring 的其他优点这里不再多说,有兴趣的朋友可以到 Spring 社区去查看。直接上代码:
清单 7.Spring 配置文件代码
<!-- 创建 jdbc 数据源 --> <bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean" scope="singleton" > <property name="jndiName" value="jdbc/tld" /> <property name="resourceRef" value="false" /> </bean> <!-- 创建 SqlSessionFactory,同时指定数据源 --> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean" > <property name="configLocation" value="classpath:MyBatis- Configuration.xml" ></property> <property name="dataSource" ref="dataSource" /> </bean> <!-- 创建数据映射器,数据映射器必须为接口 --> <bean id="adminMaintainDao" class="org.mybatis.spring.mapper.MapperFactoryBean" > <property name="mapperInterface" value="com.ibm.wah.widgets.dao.AdminMaintainDao" ></property> <property name="sqlSessionFactory" ref="sqlSessionFactory" ></property> </bean>
上面在 datasource 使用了 JNDI,学者可以将它替换为自己的 JDBC 连接方式。
回页首
数据访问层:介绍示例应用程序的数据访问层的设计
本例中使用 MyBatis 来实现数据访问层的设计。MyBatis 作为持久层框架,其主要思想是将程序中的大量 SQL 语句剥离出来,配置在配置文件中,实现 SQL 的灵活配置。这样做的好处是将 SQL 与程序代码分离,可以在不修改程序代码的情况下,直接在配置文件中修改 SQL。下面给出本例子中的部分代码并解释其意思。
Dao 接口类 AdminMaintainDao 代码
清单 8. AdminMaintainDao 代码
public interface AdminMaintainDao { /** 从数据库中查询所有的记录 * */ public List<AdminMaintain> getAllAdminMaintain(); /** 新增记录 * */ public void add(AdminMaintain am); /** 修改记录 * */ public void update(AdminMaintain am); /** 删除记录 * */ public void delete(String id); }
清单 9. 数据映射配置文件 AdminMaintainDaoMapper.xml 的代码
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.ibm.wah.widgets.dao.AdminMaintainDao"> <!-- 数据库表字段到 java 类的映射 --> <resultMap type="com.ibm.wah.widgets.vo.AdminMaintain" id="AdminMaintain"> <result property="id" column="maintain_id"/> <result property="agentTeam" column="agentTeam"/> <result property="forum" column="forum"/> <result property="url" column="url"/> <result property="forumType" column="forum_Type"/> <result property="communityUuid" column="COMMUNITY_UUID"/> </resultMap> <!-- 从数据库表中取得所有的记录并按降序排序 --> <select id="getAllAdminMaintain" resultMap="AdminMaintain"> SELECT * FROM TLD.Admin_Maintain m order by m.maintain_id desc </select> <insert id="add" parameterType="com.ibm.wah.widgets.vo.AdminMaintain"> insert into TLD.Admin_Maintain(agentTeam, forum, url,forum_Type,COMMUNITY_UUID) values (#{agentTeam}, #{forum}, #{url},#{forumType},#{communityUuid}) </insert> <update id="update" parameterType="com.ibm.wah.widgets.vo.AdminMaintain"> update TLD.Admin_Maintain set agentTeam=#{agentTeam}, forum=#{forum}, url=#{url},forum_Type=#{forumType},COMMUNITY_UUID=#{communityUuid} where maintain_id=#{id} </update> <delete id="delete" parameterType="String"> delete from TLD.Admin_Maintain where maintain_id = #{id} </delete> </mapper>
清单 10. 数据库表定义语句
CREATE TABLE "TLD"."ADMIN_MAINTAIN" ( "MAINTAIN_ID" BIGINT NOT NULL GENERATED ALWAYS AS IDENTITY ( START WITH +0 INCREMENT BY +1 MINVALUE +0 MAXVALUE +9223372036854775807 NO CYCLE CACHE 20 NO ORDER ) , "AGENTTEAM" CHAR(50) NOT NULL , "FORUM" CHAR(50) NOT NULL , "URL" CHAR(200) NOT NULL , "FORUM_TYPE" INTEGER NOT NULL , "COMMUNITY_UUID" CHAR(50) NOT NULL ) IN "USERSPACE1" ;
下面对上面的 dao 代码进行单元测试:
清单 11.DAO 单元测试代码
public class TestAdminMaintainDao { private Logger logger = Logger.getLogger(this.getClass()); private ClassPathXmlApplicationContext ctx = \ new ClassPathXmlApplicationContext("applicationContext.xml"); // 从配置文件中初始化实例 private AdminMaintainDao dao = \ (AdminMaintainDao)ctx.getBean("adminMaintainDao"); /** 测试查询 */ @Test public void testGetAllAdminMaintain() { List<AdminMaintain> list=dao.getAllAdminMaintain(); for (AdminMaintain am : list) { logger.info(am.getAgentTeam()+" : "+am.getUrl()); //add your code here; } } /** 测试新增 */ @Test public void testAdd() { AdminMaintain am=new AdminMaintain(); am.setAgentTeam("C"); am.setCommunityUuid("UUidssss8080-980sf"); am.setForum("C Awards"); am.setUrl("http:// c wards"); am.setForumType(1); dao.add(am); } /** 测试修改 */ @Test public void testUpdate() { AdminMaintain am=new AdminMaintain(); am.setId(11); am.setAgentTeam("D"); am.setCommunityUuid("sfs080-sfs"); am.setForum("D FAQ"); am.setUrl("http:// D FAQ"); am.setForumType(3); dao.update(am); } /** 测试删除 */ @Test public void testDelete() { dao.delete("11"); } }
回页首
测试:测试示例应用程序
登录到编辑页面,EnhancedGrid 就会从后台把记录列出来,点击“新增”按钮,就会新增一条空的记录。编辑该记录,编辑完后数据就自动的保存到后台数据库,选中所要删除的记录 ( 可以多选 ),点击删除按钮,确认删除后会把所选的记录全部删除,整个过程不用刷新页面就能完成,感觉就像在操作 Excel 表格一样 ( 如图 1),用户交互性很强。
回页首
结束语
EnhancedGrid 是一个功能强大、丰富的表格控件,提供伸缩式的表格数据渲染,还包括分页、排序、过滤、列移动、选择、编辑、键盘导航,导出和打印功能等等。EnhancedGrid 与 store 的紧密联系使得在 Grid 中对数据处理进行配置非常容易,通过与 dojo.connect 在一起使用可以很容易的来绑定自定义函数来实现对表格数据的异步存储。