Dorado 5 : 3.事例三:具有三种状态的静态树 (T37)

情景描述

本事例是按照常规的权限管理模块中菜单分配情景为原型制作的一颗带有三种显示状态的节点的静态树。效果如图3.1。这棵树的规则很简单:

  1. 由于节点数量小,所以不采用懒加载节点的方式,而是当页面刷新时所有的节点完全展示出来。
  2. 每个节点具有三种状态:第一种:全选择状态。表明自己和所有的子孙节点都被选中了,也称作ON状态,使用图标 表示。第二种:非选择状态。表明自己和所以的子孙节点都没被选中,也称作OFF状态,使用图标 表示。第三种:半选中状态。表明自己被选择了,子孙节点有的被选中,有的没有被选中,有的可能也是半选中状态,也称作PART状态,使用图标 表示。
  3. 节点的状态可以随着用户鼠标单击操作正确切换。在OFF和PART状态下可以切换到ON状态,ON状态切换到OFF状态。

典型界面


图3.1  具有三种状态的静态树

图3.3  提示被选择的节点信息

Dataset结构

这个例子中服务器端代码仅仅模拟了构成树的具有层次感的记录而已,这些记录被放在CustomDataset dsMenu中的。并没有演示如何将新的节点状态信息创递给服务器端完成持久化,因为这不是该事例的重点,如果您确实对这部分感兴趣,可以参考Dorado的其他文档或事例。在图3.2中我们展示dsMenu的结构,共有四个字段: MENU_ID主键,PARENT_ID父节点ID,NAME名称,STATE状态(on或off或part)。通过图标可以看出treeMenu是Tree而不是DataTree,即没有与数据集绑定。点击btnSave会提示所有的被选择的节点,如图3.2。

图3.3 事例三的视图模型元素图

解决思路

由于该事例重点演示客户端如何通过脚本制作更加个性的树,忽略了对服务器端数据来源的讨论,所以使用了Java代码模拟数据。客户端的代码仍然按照VBC的风格编写,思路如下:

  1. 根据dsMenu中的记录构造treeMenu的节点。记录的parent_id字段就是树节点层次的依据。同时需要将节点的状态信息赋值给节点。
  2. 定义treeMenu的onRefreshNode事件。根据节点的状态信息分配图标,显示节点的文字信息。
  3. 由于用户是通过点击图标改变节点状态的,所以需要定义图标的onclick事件。

代码清单

代码清单如图3.4,仍然按照VBC的风格编写。

var Constants={
ON : "on",
OFF : "off",
PART : "part",
ON_PIC : __CONTEXT_PATH + "/images/check2.gif",
PART_PIC : __CONTEXT_PATH + "/images/check1.gif",
OFF_PIC : __CONTEXT_PATH + "/images/check0.gif"
};

////////////////////////////////////////////////////////////////////
var CodePoint = {};
/**
* 根据parentId找到menuArray
* @param String parentId
* @return Array<Record menu>
*/
CodePoint.getMemuArray = function(parentId){
var menuArray = [];
for(var menu = dsMenu.find(["PARENT_ID"],[parentId]);
menu;
menu = dsMenu.find(["PARENT_ID"],[parentId])){
menuArray.push(menu);
dsMenu.deleteRecord(menu);
}
return menuArray;
}
/**
* 将node的所有被选择(ON||PART)状态的子孙节点添加到nodeArray中,
* 也可能包括node自身.
* @param DefaultTreeNode node
* @param Array<TreeNode node> nodeArray
* @return null;
*/
CodePoint.getAllSelectMenu = function( node, nodeArray ){
var node = node

treeMenu.getTopNode();
nodeArray = nodeArray

new Array();
var children = node.getNodes().toArray();
for(var i=0; i<children.length; i+=1){
var child = children[i];
if(child.$$state != Constants.OFF){
var childSelectArray = CodePoint.getAllSelectMenu( child, nodeArray );
if(childSelectArray.length > 0){
nodeArray.concat(childSelectArray);
}
}
}
if(node.getLevel()>=1 && node.$$state != Constants.OFF){
nodeArray.push(node);
}
return nodeArray;
}
/**
* 获得所有被选择(ON||PART)状态的节点ID
* @return String IDs.
*/
CodePoint.getAllSelectMenuId = function(){
var resultArray =[];
var menuArray = CodePoint.getAllSelectMenu();
for(var i=0; i<menuArray.length; i+=1){
resultArray.push(menuArray[i].$$menuId);
}
return resultArray.join("\n");
}
////////////////////////////////////////////////////////////////////
var ViewRules = {};
/**
* 构造完整的MenuTree
*/
ViewRules.buildMenuTree = function(){
var topNode = treeMenu.getTopNode();
var parentId = "";
var menuArray = CodePoint.getMemuArray(parentId);
ViewRules.buildMenuNode(topNode, menuArray);
}
/**
* 根据menuArray中的Record向node添加子节点.
* @param DefaultTreeNode node
* @param Array<Record menu> menuArray
*/
ViewRules.buildMenuNode = function(node, menuArray){
if(!menuArray

menuArray.length <=0){ return; }
for(var i=0; i<menuArray.length; i+=1){
var menu = menuArray[i];
var newNode = new DefaultTreeNode();

newNode.setLabel(menu.getValue("name"));
newNode.$$menuId = menu.getValue("menu_id");
newNode.$$state = menu.getValue("state");

node.addNode(newNode);
treeMenu.expandNode(newNode);

var parentId = menu.getValue("menu_id");
var childMenuArray = CodePoint.getMemuArray(parentId);
ViewRules.buildMenuNode(newNode, childMenuArray);
}
}
/**
* menuTree节点的展现形式,
* @event 用于Tree的onRefreshNode事件.
*/
ViewRules.refreshMenuNode = function(tree, cell, value, node){
var pic;
if(!cell.childNodes[0]){
pic = document.createElement("img");
cell.appendChild(pic);
cell.appendChild(document.createTextNode(value));
}else{
pic = cell.childNodes[0];
}
ViewRules.refreshMenuPic(node, pic);
}
/**
* 根据node的属性设置pic的属性和事件句柄.
* @param DefaultTreeNode node
* @param HTMLImageElement pic
* @return null
*/
ViewRules.refreshMenuPic = function(node, pic){
switch(node.$$state){
case Constants.ON :
if(pic.src != Constants.ON_PIC){
pic.onclick = function(){
BusinessRules.offMenus(node);
};
window.setTimeout(function(){
pic.src = Constants.ON_PIC;
},0);
}
break;
case Constants.OFF :
if(pic.src != Constants.OFF_PIC){
pic.onclick = function(){
BusinessRules.onMenus(node);
};
window.setTimeout(function(){
pic.src = Constants.OFF_PIC;
},0);
}
break;
case Constants.PART :
if(pic.src != Constants.PART_PIC){
pic.onclick = function(){
BusinessRules.onMenus(node);
}
window.setTimeout(function(){
pic.src = Constants.PART_PIC;
},0);
}
break;
}
}
/////////////////////////////////////////////////////////////////////
var BusinessRules = {};
/**
* 将节点node设置为OFF状态,
* 此时node的子孙节点和祖先节点的状态也可能发生变化.
* @param DefaultTreeNode node
*/
BusinessRules.offMenus = function(node){
//设置node状态
node.$$state = Constants.OFF;
node.refresh();
//设置node子孙节点状态
treeMenu.expandNode(node);
var children = node.getNodes().toArray();
for(var i=0; i<children.length; i+=1){
var child = children[i];
BusinessRules.offMenus(child);
}
//设置node祖先节点状态
BusinessRules.updateAncestorsState(node);
}
/**
* 将节点node设置为ON状态,
* 此时node的子孙节点和祖先节点的状态也可能发生变化.
* @param DefaultTreeNode node
*/
BusinessRules.onMenus = function(node){
//设置node状态
node.$$state = Constants.ON;
node.refresh();
//设置node子孙节点状态
treeMenu.expandNode(node);
var children = node.getNodes().toArray();
for(var i=0; i<children.length; i+=1){
var child = children[i];
BusinessRules.onMenus(child);
}
//设置node祖先节点状态
BusinessRules.updateAncestorsState(node);
}
/**
* 根据node的状态设置祖先节点的状态.
* @param DefaultTreeNode node
*/
BusinessRules.updateAncestorsState = function(node){
if(node.getLevel() <=1){return;}

var toOn = false;
var toOff = false;
var toPart = false;
var father = node.getParent();
var children = father.getNodes().toArray();
var child,state;
for(var i=0; i<children.length; i+=1){
child = children[i];
state = child.$$state;
if(state == Constants.PART){
toPart = true;
break;
}
if(state == Constants.ON){
toOn = true;
continue;
}
if(state == Constants.OFF){
toOff = true;
continue;
}
}
//
if(toPart

(toOn && toOff)){
father.$$state = Constants.PART;
father.refresh();
}else{
if(toOff){//(!toOn && toOff)
father.$$state = Constants.OFF;
father.refresh();
}else{ //(toOn && !toOff)
father.$$state = Constants.ON;
father.refresh();
}
}

BusinessRules.updateAncestorsState(father);
}

图3.4 事例三的客户端代码清单

知识点

__CONTEXT_PATH常量

在上面的代码清单中我们使用了Constants来管理常量:

var Constants={
ON : "on",
OFF : "off",
PART : "part",
ON_PIC : __CONTEXT_PATH + "/images/check2.gif",
PART_PIC : __CONTEXT_PATH + "/images/check1.gif",
OFF_PIC : __CONTEXT_PATH + "/images/check0.gif"
};

__CONTEXT_PATH是我们可以直接引用的每张Dorado页面都拥有的常量,表示请求的上下文,相当于我们在JSP中书写<%=request.getContextPath()%>。该常量有助于我们使用绝对地址引用服务器资源。
我们先来解决根据dsMenu向treeMenu添加节点的问题。根据子节点的PARENT_ID等于父节点的MENU_ID的原则,我们编写了一个工具类:

CodePoint.getMemuArray = function(parentId){
var menuArray = [];
for(var menu = dsMenu.find(["PARENT_ID"],[parentId]);
menu;
menu = dsMenu.find(["PARENT_ID"],[parentId])){
menuArray.push(menu);
dsMenu.deleteRecord(menu);
}
return menuArray;
}

CodePoint.getMemuArray的功能是在dsMenu中找到PARENT_ID字段为parentId的记录数组,如果parentId为空表明查找的是根节点对应的记录。根据每个记录只能对应一个节点的原则,我们在每次找到记录后将其删除是为了提高dsMenu的find方法的效率。

手动为节点添加子节点

与之配合使用的是ViewRules.buildMenuNode负责根据CodePoint.getMemuArray的结果为node添加子节点。

ViewRules.buildMenuNode = function(node, menuArray){
if(!menuArray

menuArray.length <=0){ return; }
for(var i=0; i<menuArray.length; i+=1){
var menu = menuArray[i];
var newNode = new DefaultTreeNode();

newNode.setLabel(menu.getValue("name"));
newNode.$$menuId = menu.getValue("menu_id");
newNode.$$state = menu.getValue("state");

node.addNode(newNode);
treeMenu.expandNode(newNode);

var parentId = menu.getValue("menu_id");
var childMenuArray = CodePoint.getMemuArray(parentId);
ViewRules.buildMenuNode(newNode, childMenuArray);
}
}

注意:在目前的实现中我们将dsMenu中对节点有用的信息直接作为属性赋值给了节点,并没有建立treeMenu节点与dsMenu记录直接的关联,虽然这也是一种不错的方法。

利用递归方法构造完整的树

将ViewRules.buildMenuNode与CodePoint.getMemuArray关联起来的方法叫做ViewRules.buildMenuTree:

ViewRules.buildMenuTree = function(){
var topNode = treeMenu.getTopNode();
var parentId = "";
var menuArray = CodePoint.getMemuArray(parentId);
ViewRules.buildMenuNode(topNode, menuArray);
}

显然上面的方法是个递归函数,入口是为treeMenu的topNode构造子节点,出口是被构造的所有节点都不在有子节点。在本事例中我们多次使用递归函数,这也是对树操作常见的计算方式。
通过上面的三个方法我们已经构造除了一颗简陋但完整的树了,效果如图3.5。

图3.5 简陋的树

自定义Tree的onRefreshNode事件

图3.5与图3.1相比每个节点缺少了表达状态的图标,为了达到这个效果需要定义treeMenu的onRefreshNode事件,下面是该事件的代码:

ViewRules.refreshMenuNode = function(tree, cell, value, node){
var pic;
if(!cell.childNodes[0]){
pic = document.createElement("img");
cell.appendChild(pic);
cell.appendChild(document.createTextNode(value));
}else{
pic = cell.childNodes[0];
}
ViewRules.refreshMenuPic(node, pic);
};

通过阅读上面的代码可以知道我们手动的为每个节点添加一个图片和一段文本,图片的信息描述在ViewRules.refreshMenuPic函数中,代码如下:

ViewRules.refreshMenuPic = function(node, pic){
switch(node.$$state){
case Constants.ON :
if(pic.src != Constants.ON_PIC){
pic.onclick = function(){
BusinessRules.offMenus(node);
};
window.setTimeout(function(){
pic.src = Constants.ON_PIC;
},0);
}
break;
case Constants.OFF :
if(pic.src != Constants.OFF_PIC){
pic.onclick = function(){
BusinessRules.onMenus(node);
};
window.setTimeout(function(){
pic.src = Constants.OFF_PIC;
},0);
}
break;
case Constants.PART :
if(pic.src != Constants.PART_PIC){
pic.onclick = function(){
BusinessRules.onMenus(node);
};
window.setTimeout(function(){
pic.src = Constants.PART_PIC;
},0);
}
break;
}
};

在该函数中我们可以看出Contants的价值,所有的常量被统一管理,消除了通用常量的硬编码,有利于代码维护。ViewRules.refreshMenuPic的功能是根据node的$$state属性设置图片pic的src属性和onclick的事件句柄,并且调用了BusinessRules空间下的onMenus和offMenus方法,接下来列出他们的代码:

同步父子节点状态同步的算法

BusinessRules.offMenus = function(node){
//设置node状态
node.$$state = Constants.OFF;
node.refresh();
//设置node子孙节点状态
treeMenu.expandNode(node);
var children = node.getNodes().toArray();
for(var i=0; i<children.length; i+=1){
var child = children[i];
BusinessRules.offMenus(child);
}
//设置node祖先节点状态
BusinessRules.updateAncestorsState(node);
};

BusinessRules.offMenus的功能是将node设置为OFF状态,那么他的所有子孙也应该被设置为OFF状态,他的祖先们的状态也会做出相应的调整。

BusinessRules.onMenus = function(node){
//设置node状态
node.$$state = Constants.ON;
node.refresh();
//设置node子孙节点状态
treeMenu.expandNode(node);
var children = node.getNodes().toArray();
for(var i=0; i<children.length; i+=1){
var child = children[i];
BusinessRules.onMenus(child);
}
//设置node祖先节点状态
BusinessRules.updateAncestorsState(node);
};

BusinessRules.onMenus的功能是将node和他的所有子孙节点都被设置为ON状态,他的祖先们的状态也会做出相应的调整。

BusinessRules.updateAncestorsState = function(node){
if(node.getLevel() <=1){return;}

var toOn = false;
var toOff = false;
var toPart = false;
var father = node.getParent();
var children = father.getNodes().toArray();
var child,state;
for(var i=0; i<children.length; i+=1){
child = children[i];
state = child.$$state;
if(state == Constants.PART){
toPart = true;
break;
}
if(state == Constants.ON){
toOn = true;
continue;
}
if(state == Constants.OFF){
toOff = true;
continue;
}
}
//
if(toPart

(toOn && toOff)){
father.$$state = Constants.PART;
father.refresh();
}else{
if(toOff){//(!toOn && toOff)
father.$$state = Constants.OFF;
father.refresh();
}else{ //(toOn && !toOff)
father.$$state = Constants.ON;
father.refresh();
}
}

BusinessRules.updateAncestorsState(father);
};

BusinessRules.updateAncestorsState会被BusinessRules.onMenus和BusinessRules.offMenus方法调用,功能是根据节点node的状态即$$state属性正确设置祖先节点的状态。设置每个祖先状态的思路是:检查祖先的直系子孙节点的状态,如果都是ON状态,祖先也应该被这支撑ON状态;如果所有直系子孙的状态都是OFF状态,那么祖先也应该被设置为OFF状态;否则祖先被设置为PART状态。当然算法是可以改进的。