Dorado 5 : 4.事例四:分组显示的RadioBox (T37)

情景描述

本事例是按照保险行业中风险项的业务原型制作的分组显示的RadioBox。功能很明确:每组最多可以选择一个信息。

典型界面

图4.1是我们要达到的效果。

图4.1 分组显示的RadioBox
如同事例三一样我们重点展示客户端代码,被选中的信息以提示框的形式出现,并且同时提示已经被选择的分组和还没被选择的分组,如图4.2。

图4.2 提示被选择的信息
还需要一个右键菜单用来清除某组选择项,如图4.3。

图4.3 显示右键菜单

数据库准备

共使用了两张数据库表:
第一张:Mark_Branch分公司表。

第二张:Mark_Dept部门表。

Dataset结构

我们定义了AutoSqlDataset dsDeptBranch,连接Mark_Branch和Mark_Dept表,并对Branch_ID和Dept_ID进行排序,如图4.4。

图4.4

解决思路

首先达到分组的效果:

  1. 将Dataset记录按照分组字段排序。
  2. 设置DataTable分组列的groupable属性为true。

在这个基础上:

  1. 利用DataTable的列的onRefresh事件分组添加Radio。
  2. 将这些Radio统一管理。

代码清单

使用面向对象的思想编写扩展的组件。

//////////////////////////////////////////////////////
///////////////Entry入口(对外调用接口)///////////////////
//////////////////////////////////////////////////////
/**
* GroupRadioBoxTable的构造函数
* @param String id
* @param DataTable dataTable
* @param String groupCodeField 组编号字段名
* @param String groupNameField 组名称的字段名
* @param String entityCodeField 成员编号字段名
* @param String entityNameField 成员名称字段名
*/
function GroupRadioBoxTable( id, dataTable,
groupCodeField, groupNameField,
entityCodeField, entityNameField){
this.id = id;
this.dataTable = dataTable;
this.groupCodeField = groupCodeField;
this.groupNameField = groupNameField;
this.entityCodeField = entityCodeField;
this.entityNameField = entityNameField;
}
/**
* GroupRadioBoxTable的激活函数,
* 只有调用了该函数GroupRadioBoxTable的特性才能被附加到其DataTable上
*/
GroupRadioBoxTable.prototype.activate = function(){
//创建右键菜单
this.buildMenu();

var table = this.dataTable;
var radioBoxTable = this;
var groupColumn = table.getColumn(radioBoxTable.groupNameField);
var radioColumn = table.getColumn(radioBoxTable.entityNameField);

groupColumn.setGroupable(true);
radioBoxTable.addRadioFieldRefreshEvent(radioColumn);

if(table.isActive()){
table.rebuild();
table.refresh();
}
};
/////////////////////////////////////////////////////
///////////////////ViewRules Menu////////////////////
/////////////////////////////////////////////////////
/**
* 创建右键菜单
*/
GroupRadioBoxTable.prototype.buildMenu = function(){
var radioBoxTable = this;
var dataTable = this.dataTable;
var menuId = this.getMenuId();
var menu = window[menuId];
if(!menu){
menu = DoradoFactory.create("Menu", null, menuId);
window[menuId] = menu;
menu.setPopupContainer(dataTable.id);
var topItem = menu.getTopItem();
var item = new MenuItem("delete", "删除本组的选中项");
topItem.addItem(item);

EventManager.addDoradoEvent(menu, "onItemClick",
function(menu, item) {
var r = dataTable.getDataset().getCurrent();
switch(item.getName()){
case "delete":
radioBoxTable.deleteData(r);
break;
}
});
menu.activate();
}
};
/**
* 生成菜单ID
*/
GroupRadioBoxTable.prototype.getMenuId = function(){
return "$$"+ this.dataTable.id + "__" + "menu";
};

////////////////////////////////////////////////////
//////////////////////ViewRuls Entry////////////////
////////////////////////////////////////////////////
/**
* 给RadioField添加onRefresh事件,
* cell 中放置了一个radio和一段文本
* @param radioField DataTable.Column
*/
GroupRadioBoxTable.prototype.addRadioFieldRefreshEvent =
function(radioField){

var radioBoxTable = this;
if(!radioField){return;}
EventManager.addDoradoEvent(radioField,"onRefresh",
function(column,row,cell,value,record){
var radio = cell.childNodes[0];
if(!radio){
radio = document.createElement(
"<input type='radio' name='_$" +
record.getValue(radioBoxTable.groupCodeField) + "'>");
radio.id =radioBoxTable.getRadioBoxId(
record.getValue(radioBoxTable.groupCodeField),
record.getValue(radioBoxTable.entityCodeField));

radio.onclick = function(){
radioBoxTable.pushData(
record.getValue(radioBoxTable.groupCodeField),
record.getValue(radioBoxTable.groupNameField),
record.getValue(radioBoxTable.entityCodeField),
record.getValue(radioBoxTable.entityNameField));
};
cell.appendChild(radio);
}

var textNode = cell.childNodes[1];
if(!textNode){
textNode = document.createElement("label");
textNode.innerText = value;
cell.appendChild(textNode);
}

var groupCode = record.getValue(radioBoxTable.groupCodeField);
var data = radioBoxTable.datas[groupCode];
if(!data){
radioBoxTable.datas[groupCode] =
[null,record.getValue(radioBoxTable.groupNameField),null];
}

return false;
});
};
//////////////////////ViewRules.RadioControl////////////
/**
* 将radio设置为非选中状态
* @param String groupCode 组编码
* @param String entityCode 成员编码
*/
GroupRadioBoxTable.prototype.clearCheckedRadio =
function(groupCode, entityCode){

var radio = document.getElementById(
this.getRadioBoxId(groupCode, entityCode));
if(radio){
radio.checked = false;
}
};
/**
* 将radio设置为选中状态
* @param String groupCode 组编码
* @param String entityCode 成员编码
*/
GroupRadioBoxTable.prototype.setCheckedRadio =
function(groupCode, entityCode){

var radio = document.getElementById(
this.getRadioBoxId(groupCode, entityCode));
if(radio){
radio.checked = true;
}
};
/**
* 获得radio的id
* @param String groupCode 组编码
* @param String entityCode 成员编码
*/
GroupRadioBoxTable.prototype.getRadioBoxId =
function(groupCode, entityCode){

return "$$" + groupCode + "__" + entityCode;
};
////////////////////////////////////////////////////
//////////////////////ModelRules////////////////////
////////////////////////////////////////////////////
/**
* 按照Map的方式存放数据,格式为:
* <groupCode,[entityCode,groupName,entityName]>
*/
GroupRadioBoxTable.prototype.datas={};
/**
* 记录选中的数据
* @param groupCode 组编码
* @param groupName 组名称
* @param entityCode 成员编码
* @param entityName 成员名称
*/
GroupRadioBoxTable.prototype.pushData =
function(groupCode, groupName, entityCode, entityName){

if(!this.datas[groupCode]

this.datas[groupCode][0] != entityCode){
this.datas[groupCode] = [entityCode,groupName,entityName];
}
};
/**
* 移除选中的数据
* @param Record r
*/
GroupRadioBoxTable.prototype.deleteData = function(r){
if(!r){return;}
var groupCode = r.getValue(this.groupCodeField);
var data = this.datas[groupCode];
if(data){
this.clearCheckedRadio(groupCode, data[0]);
data[0] = null;
data[2] = null;
}
};
////////////////////////////////////////////////////
///////////////////////出口(返回值)///////////////////
////////////////////////////////////////////////////
/**
* 返回结果
* @param String groupCodeInnerSplitor 组内部的代码分隔符
* @param String groupNameInnerSplitor 组内部的名称分隔符
* @param String groupCodeSplitor 组间的代码分隔符
* @param String groupNameSplitor 组间的名称分隔符
*/
GroupRadioBoxTable.prototype.getResult =
function(groupCodeInnerSplitor, groupNameInnerSplitor,
groupCodeSplitor,groupNameSplitor){

groupCodeInnerSplitor = groupCodeInnerSplitor

":";
groupNameInnerSplitor = groupNameInnerSplitor

":";
groupCodeSplitor = groupCodeSplitor

";";
groupNameSplitor = groupNameSplitor

"\n";

var datas = this.datas;
var checkedResult = {codes:[],names:[]};
var unCheckedResult = {codes:[],names:[]};
var data,entityCode;
for(var groupCode in datas){
if(groupCode != "_hashCode"){
data = datas[groupCode];
entityCode = data[0];
if(entityCode){
checkedResult.names.push( data[1] + groupNameInnerSplitor + data[2]);
checkedResult.codes.push( groupCode + groupCodeInnerSplitor + entityCode);
}else{
unCheckedResult.names.push( data[1] );
unCheckedResult.codes.push( groupCode );
}
}
}
return {
checked:{
code: checkedResult.codes.join(groupCodeSplitor),
name: checkedResult.names.join(groupNameSplitor)
},
unchecked:{
code: unCheckedResult.codes.join(groupCodeSplitor),
name: unCheckedResult.names.join(groupNameSplitor)
}
};
};

可以看出上面的代码完全是对组件功能与表现形式的约束,也就是说是与业务无关的,所以这些代码的功能是可以被复用的,独立到脚本库中会更好。

知识点

本事例将会演示如何在Dorado中引入用户的JavaScript库,和如何将已有的组件改造成用途更加专一的组件。

引入用户JavaScript库

当你查看源代码时会发现,该View的functions域只有一条语句:

var groupRadioBoxTable;

View的onDatasetsPrapared事件中也只有三句代码:

var datatable = tblDeptBranch;
groupRadioBoxTable=new GroupRadioBoxTable( "groupRadioBoxTable", datatable, "branch_id", "branch_name", "dept_id", "dept_name");
groupRadioBoxTable.activate();

你可能会奇怪GroupRadioBoxTable是在哪里定义的呢?其实它是一段可以通用的代码被放到了一个单独js文件中,作为通用的JavaScript库文件使用。
引入用户的JavaScript库需要两个步骤:

  1. 将.js文件放到home\smartweb\v2\lib下,如果该文件夹还不存在,可以创建。如图4.5。


图4.5 用户JavaScript库文件的位置

  1. 在javascript-lib文件中声明js文件,如图4.6。


图4.6 声明需要使用js文件
通过上面的两个步骤group-box-table.js中的脚本在任何Dorado JSP中和视图模型中都可以使用了。

按照面向对象的思想扩展组件

从代码清单中可以看出没有使用VBC的风格管理代码,原因是该组件是面向功能的没有被添加任何业务约束,所以不会使用VBC风格。我们在这里使用了面向对象的代码编写风格,同时使用了按照代码功能分块注释的方法保证代码结构的清晰度。而且我们还构造了一个小型的MVC模型:
Model层是由GroupRadioBoxTable.prototype.datas={};
GroupRadioBoxTable.prototype.pushData; GroupRadioBoxTable.prototype.deleteData;构成;
View层是由GroupRadioBoxTable.prototype.addRadioFieldRefreshEvent; GroupRadioBoxTable.prototype.clearCheckedRadio; GroupRadioBoxTable.prototype.setCheckedRadio; GroupRadioBoxTable.prototype.getRadioBoxId;构成。
Cotroller层:使用了Dorado客户端的事件引擎。

组件包装器

从GroupRadioBoxTable的构造函数上我们可以看出,虽然我们是在DataTable的基础上做了进一步的封装,但是我们没有触碰DataTable的原始定义:没有为DataTable定义新的属性、事件、功能函数,而是将它们定义在GroupRadioTable中作为属性包容进来。这样我们不会与DataTable的原始定义发生任何冲突,GroupRadioTable就成了DataTable的一种包装器。

/**
* GroupRadioBoxTable的构造函数
* @param String id
* @param DataTable dataTable
* @param String groupCodeField 组编号字段名
* @param String groupNameField 组名称的字段名
* @param String entityCodeField 成员编号字段名
* @param String entityNameField 成员名称字段名
*/
function GroupRadioBoxTable( id, dataTable,
groupCodeField, groupNameField,
entityCodeField, entityNameField){
this.id = id;
this.dataTable = dataTable;
this.groupCodeField = groupCodeField;
this.groupNameField = groupNameField;
this.entityCodeField = entityCodeField;
this.entityNameField = entityNameField;
}

扩展组件的激活函数

Dorado所有的组件都有一个叫做activate的激活函数,激活时机是由Dorado引擎维护的,我们不应该在激活时机上做文章,而是应该考虑在组件激活前和激活后将扩展内容添加进去。如果想在激活前将扩展信息添加进去,需要在ViewModel的onDatasetsPrepared中动手;如果想在激活后添加扩展信息,需要定义onActive事件内容。GroupRadioBoxTable的激活函数也考虑到了这一点。

/**
* GroupRadioBoxTable的激活函数,
* 只有调用了该函数GroupRadioBoxTable的特性才能被附加到其DataTable上
*/
GroupRadioBoxTable.prototype.activate = function(){
//创建右键菜单
this.buildMenu();

var table = this.dataTable;
var radioBoxTable = this;
var groupColumn = table.getColumn(radioBoxTable.groupNameField);
var radioColumn = table.getColumn(radioBoxTable.entityNameField);

groupColumn.setGroupable(true);
radioBoxTable.addRadioFieldRefreshEvent(radioColumn);

if(table.isActive()){
table.rebuild();
table.refresh();
}
};

在这里DataTable是否已经被激活对于我们来说的差别是是否需要手动调用rebuild和refresh函数重绘DataTable而已。当然前提是需要在DataTable中显示定义出分组名与成员名的列。

客户端创建菜单

在图4.3中我们可以看见一个右键菜单,但在图4.4中却没有看见它的配置信息。这是因为这个菜单是我们通过JavaScript手动添加的,并且作为GroupRadioBoxTable组件的一部分。

/**
* 创建右键菜单
*/
GroupRadioBoxTable.prototype.buildMenu = function(){
var radioBoxTable = this;
var dataTable = this.dataTable;
var menuId = this.getMenuId();
var menu = window[menuId];
if(!menu){
menu = DoradoFactory.create("Menu", null, menuId);
window[menuId] = menu;
menu.setPopupContainer(dataTable.id);
var topItem = menu.getTopItem();
var item = new MenuItem("delete", "删除本组的选中项");
topItem.addItem(item);

EventManager.addDoradoEvent(menu, "onItemClick",
function(menu, item) {
var r = dataTable.getDataset().getCurrent();
switch(item.getName()){
case "delete":
radioBoxTable.deleteData(r);
break;
}
});
menu.activate();
}
};

上面的代码演示了如何手动创建一个Menu,如何向Menu中添加MenuItem,如何定义Menu的onItemClick事件。最后不要忘记调用激活函数。

视图约束(ViewRules)的入口

通常视图约束是通过组件的onRefresh事件表现出来的。我们下面定义了显示Radio的列的onRefresh事件。

/**
* 给RadioField添加onRefresh事件,
* cell 中放置了一个radio和一段文本
* @param radioField DataTable.Column
*/
GroupRadioBoxTable.prototype.addRadioFieldRefreshEvent =
function(radioField){

var radioBoxTable = this;
if(!radioField){return;}
EventManager.addDoradoEvent(radioField,"onRefresh",
function(column,row,cell,value,record){
var radio = cell.childNodes[0];
if(!radio){
radio = document.createElement(
"<input type='radio' name='_$" +
record.getValue(radioBoxTable.groupCodeField) + "'>");
radio.id =radioBoxTable.getRadioBoxId(
record.getValue(radioBoxTable.groupCodeField),
record.getValue(radioBoxTable.entityCodeField));

radio.onclick = function(){
radioBoxTable.pushData(
record.getValue(radioBoxTable.groupCodeField),
record.getValue(radioBoxTable.groupNameField),
record.getValue(radioBoxTable.entityCodeField),
record.getValue(radioBoxTable.entityNameField));
};
cell.appendChild(radio);
}

var textNode = cell.childNodes[1];
if(!textNode){
textNode = document.createElement("label");
textNode.innerText = value;
cell.appendChild(textNode);
}

var groupCode = record.getValue(radioBoxTable.groupCodeField);
var data = radioBoxTable.datas[groupCode];
if(!data){
radioBoxTable.datas[groupCode] =
[null,record.getValue(radioBoxTable.groupNameField),null];
}

return false;
});
};

注意上面的代码中创建radio时使用的是document.createElement("<input type='radio' name ='...'>");的方式。这是由于name属性在DHTML中的特殊作用引起的,否则radio的name属性的分组定义不会发挥作用。在添加了radio和label后,我们会将该组的信息登记到GroupRadioBoxTable的Model层中。

自定义组件的Model层

Dorado已经为我们提供了Model层,就是Dataset。Dataset具有很多优点,比如支持数据类型,支持事件模型,支持数据校验,方便与服务端通信等等,所以Dataset是Model层的第一选择。然而GroupRadioBoxTable仅仅需要一个简单的可以反应被分组选择的信息即可,所以我们制作了一个非常简单的组件级别的Model层:数据按照Map的方式和一定的格式存放在datas中,并且提供了两个内部使用的操作函数,而数据在DataTable的onRefresh事件中添加的。

/**
* 按照Map的方式存放数据,格式为:
* <groupCode,[entityCode,groupName,entityName]>
*/
GroupRadioBoxTable.prototype.datas={};
/**
* 记录选中的数据
* @param groupCode 组编码
* @param groupName 组名称
* @param entityCode 成员编码
* @param entityName 成员名称
*/
GroupRadioBoxTable.prototype.pushData =
function(groupCode, groupName, entityCode, entityName){

if(!this.datas[groupCode]

this.datas[groupCode][0] != entityCode){
this.datas[groupCode] = [entityCode,groupName,entityName];
}
};
/**
* 移除选中的数据
* @param Record r
*/
GroupRadioBoxTable.prototype.deleteData = function(r){
if(!r){return;}
var groupCode = r.getValue(this.groupCodeField);
var data = this.datas[groupCode];
if(data){
this.clearCheckedRadio(groupCode, data[0]);
data[0] = null;
data[2] = null;
}
};

自定义组件Model层对外的数据接口

Dorado的Model层是Dataset,所以当我们需要数据时通常从Dataset中获取。由于GroupRadioBoxTable的Model是我们定义的,所以不可能从Dataset中取值。现在GroupRadioBoxTable的Model的对外接口叫做getResult函数。

/**
* 返回结果
* @param String groupCodeInnerSplitor 组内部的代码分隔符
* @param String groupNameInnerSplitor 组内部的名称分隔符
* @param String groupCodeSplitor 组间的代码分隔符
* @param String groupNameSplitor 组间的名称分隔符
*/
GroupRadioBoxTable.prototype.getResult =
function(groupCodeInnerSplitor, groupNameInnerSplitor,
groupCodeSplitor,groupNameSplitor){

groupCodeInnerSplitor = groupCodeInnerSplitor

":";
groupNameInnerSplitor = groupNameInnerSplitor

":";
groupCodeSplitor = groupCodeSplitor

";";
groupNameSplitor = groupNameSplitor

"\n";

var datas = this.datas;
var checkedResult = {codes:[],names:[]};
var unCheckedResult = {codes:[],names:[]};
var data,entityCode;
for(var groupCode in datas){
if(groupCode != "_hashCode"){
data = datas[groupCode];
entityCode = data[0];
if(entityCode){
checkedResult.names.push( data[1] + groupNameInnerSplitor + data[2]);
checkedResult.codes.push( groupCode + groupCodeInnerSplitor + entityCode);
}else{
unCheckedResult.names.push( data[1] );
unCheckedResult.codes.push( groupCode );
}
}
}
return {
checked:{
code: checkedResult.codes.join(groupCodeSplitor),
name: checkedResult.names.join(groupNameSplitor)
},
unchecked:{
code: unCheckedResult.codes.join(groupCodeSplitor),
name: unCheckedResult.names.join(groupNameSplitor)
}
};
};

我们把已经选择的组和没有被选择的组信息都返回了,这样的好处是可以对客户的选择结果进行规范,比如必须每组都要有选择项。