索引
正确使用合适的设计模式可极大提高框架的可维护性和可扩展性,良好的设计接口可极大减少用户代码量并提高可读性, 有利于大规模项目开发的团队分工,甚至不需要接入后台数据即可完成对前端组件的单元测试, 有助于组件和模型的数据绑定和数据同步, 进而有助于开发厂商进一步提供所见即所得的可视化开发工具, 如 Adobe Flash Builder 和 Microsoft Visual Studio。
在早期的GUI
设计领域,Java/Swing,SWT/JFace,
Cocoa 和 Qt
都是基于 Model View Controller (MVC) 设计模式的典范。
MVC
之后慢慢发展衍生的 Model View Presenter (MVP) 设计模式逐渐被新的GUI
框架所采用,
如基于Flash
的 Apache/Flex 企业应用组件,以及在Swing
基础上封装的 JGoodies 框架。
Martin Fowler
的 GUI Architectures 对这两种设计模式进行了分析比较。
近些年在MVP
基础上再增加ViewModel
层衍生的 Model View ViewModel (MVVM) 设计模式,
也被微软新一代的GUI
开发框架 Silverlight/WPF 采用,
包括像 Knockout 这种为简化前端HTML
组件和js
数据绑定的框架也是基于MVVM
设计模式。
MVC/MVP/MVVM
等设计模式的提出,方便了大家对不同框架的理解和归类,任何GUI
框架在具体实现上都会有自己的演变和特性,
HT
的整体框架更类似MVP
和MVVM
的设计模式,但我们更倾向于称HT
的设计模式为 Object View Mapping (OVM)
,
类比于 Object Relational Mapping (ORM),
通过面对对象方式的封装,屏蔽了各种视图组件的异构性,采用了统一的 DataModel 数据模型和
SelectionModel 选择模型,可驱动 ListView
,TreeView
,TableView
,
TreeTableView
、GraphView
和 Graph3dView
等所有HT
的视图组件。
HT
这样的设计架构,用户仅需掌握统一的数据驱动接口,不会因视图组件增多带来额外学习成本,这是HT
易上手易精通的根本。
Data
类型贯穿整个HT
框架,是最基础数据类型。
getId()
和setId(id)
获取和设置唯一编号,系统会自动分配,设置需注意DataModel说明,DataModel#getDataById(id)
可查找getTag()
和setTag(tag)
获取和设置标识号,通过DataModel#getDataByTag(tag)
可查找getName()
和setName(name)
获取和设置名称getIcon()
和setIcon(icon)
获取和设置小图标,常作为TreeView
和ListView
等组件上的节点小图标getDisplayName()
和setDisplayName(displayName)
获取和设置显示名称,常作为Column
和Property
的列头和属性名称显示getToolTip()
和setToolTip(tooltip)
获取和设置组件上该节点或图元的文字提示信息 getParent()
和setParent(parent)
获取和设置父亲节点,作为树层次结构的信息,内部会自动调用addChild
或removeChild
addChild(child, index)
添加孩子节点,index
为孩子插入索引,为空则插入作为最后的孩子,内部会自动调用setParent
removeChild(child)
删除指定孩子节点,内部会自动调用setParent
clearChildren()
删除所有孩子节点,内部会自动调用setParent
onChildAdded(child, index)
添加孩子时的回调函数,可重载做后续处理onChildRemoved(child, index)
删除孩子时的回调函数,可重载做后续处理onParentChanged(oldParent, parent)
改变父亲节点时的回调函数,可重载做后续处理 size()
返回孩子总数hasChildren()
判断是否有孩子,有则返回true
,无则返回false
isEmpty()
判断是否有孩子,有则返回false
,无则返回true
getChildren()
获取所有孩子节点,该函数返回内部ht.List
类型数组对象引用toChildren(matchFunc, scope)
根据matchFunc
函数逻辑构建所有匹配图元的新ht.List
类型数组对象eachChild(func, scope)
遍历所有孩子,可指定函数scope
域getChildAt(index)
返回指定索引位置孩子 isParentOf(data)
判断本图元是否为指定data
的父亲图元isDescendantOf(data)
判断本图元是否为指定data
图元的子孙 isRelatedTo(data)
判断本图元与指定data
图元是否有父子或子孙关系layer
属性通过getLayer()
和setLayer(layer)
操作,对应图元在GraphView
组件中的图层位置,默认值为空isAdjustChildrenToTop()
和setAdjustChildrenToTop(true)
,默认为false
,ht.Node
类型默认为true
,
GraphView
默认点击图元会自动sendToTop
,该属性决定是否对子图元也进行sendToTop
操作firePropertyChange(property, oldValue, newValue)
派发属性变化事件,可使用fp
的简写方式onPropertyChanged(event)
属性变化回调函数,可重载做后续处理invalidate()
该函数用户强制触发属性变化事件通知界面更新,内部实现为this.fp('*', false, true)
getStyleMap()
返回图元内部样式映射信息,getStyle(name)
时如果styleMap
对应值为空,自动会返回ht.Style
定义的信息getStyle('name')
和setStyle('name', value)
获取和设置图元样式,可采用s(name/name,value/json)
的简写方式onStyleChanged(name, oldValue, newValue)
当style
属性变化时会回调该函数,可重载做后续处理 getAttrObject()
和setAttrObject(obj)
获取和设置attr
属性对象,该属性默认为空,用于存储用户业务信息getAttr(name)
和setAttr(name, value)
获取和设置attr
对象的属性,可采用a(name/name,value/json)
的简写方式onAttrChanged(name, oldValue, newValue)
当attr
属性变化时会回调该函数,可重载做后续处理 toLabel()
返回值默认作为TreeView
和GraphView
等组件上的图元文字标签,默认返回displayName||name
信息addStyleIcon(name, icons)
和removeStyleIcon(name)
增加和删除style
中icons
属性,可参考icon章节getSerializableProperties()
返回需要序列化的属性名称map
,参见序列化手册getSerializableStyles()
返回需要序列化的style
属性名称map
,参见序列化手册getSerializableAttrs()
返回需要序列化的attr
属性名称map
,参见序列化手册ht.Default.setImage('edit', 'res/edit.png');
ht.Default.setImage('mail', 'res/mail.png');
ht.Default.setImage('readmail', 'res/readmail.png');
ht.Default.setImage('search', 'res/search.png');
ht.Default.setImage('settings', 'res/settings.png');
function init(){
dataModel = new ht.DataModel();
treeView = new ht.widget.TreeView(dataModel);
view = treeView.getView();
view.className = 'main';
document.body.appendChild(view);
window.addEventListener('resize', function (e) {
treeView.invalidate();
}, false);
var inbox = addData('Inbox', 'mail');
addData('Read Mail', 'readmail', inbox);
addData('Drafts', 'edit');
var search = addData('Search Folders', 'search');
addData('Categorized Mail', 'search', search);
addData('Large mail', 'search', search);
addData('UnRead Mail', 'search', search);
addData('Settings', 'settings');
treeView.expandAll();
treeView.getSelectionModel().setSelection(search);
}
function addData(name, icon, parent){
var data = new ht.Data();
data.setName(name);
data.setIcon(icon);
data.setParent(parent); // or parent.addChild(data);
dataModel.add(data);
return data;
}
数据容器ht.DataModel
(以下简称DataModel
)作为承载Data
数据的模型,管理着Data
数据的增删以及变化事件派发,
HT
框架所有组件都是通过绑定DataModel
,以不同的形式呈现到用户界面;同时组件也会监听DataModel
模型的变化事件,
实时同步更新界面数据信息,掌握了DataModel
的操作就掌握了所有组件的模型驱动方式。
Data
类型对象构造时内部会自动被赋予一个id
属性,可通过data.getId()
和data.setId(id)
获取和设置,
Data
对象添加到DataModel
之后不允许修改id
值,可通过dataModel.getDataById(id)
快速查找Data
对象。
一般建议id
属性由HT
自动分配,用户业务意义的唯一标示可存在tag
属性上,通过Data#setTag(tag)
函数允许任意动态改变tag
值,
通过DataModel#getDataByTag(tag)
可查找到对应的Data
对象,并支持通过DataModel#removeDataByTag(tag)
删除Data
对象。
id
和tag
的方式都是针对唯一标识的Data
对象,若搜索非唯一属性可采用ht.QuickFinder插件。
使用DataModel
时需要特别注意:一般要求有父子关系的Data
都应逐一加入容器。常遇到parent
加入容器,但children
未加入,
导致组件看不到children
的问题,因为添加parent
并不会自动加载所有子孙,这点务必注意。
Data
类型有getDataModel()
函数,当Data
加入容器后data.getDataModel()
能获得当前所在容器信息,
不允许一个Data
对象同时加入多个DataModel
容器中。
add(data, index)
添加Data
对象,index
一般无需指定,其只在data
的parent
为空时才起作用,指定插入roots
数组的索引位置remove(data)
删除Data
对象,该操作有以下副作用:DataModel
中删除data.setParent(null)
Edge
类型通过edge.setSource(null)
和edge.setTarget(null)
断开节点关系Node
类型会将其关联的连线从DataModel
中删除Node
类型通过data.setHost(null)
断开与宿主吸附节点关系clear()
删除容器中所有Data
对象,该操作一次性清空,没有逐个remove
的过程,不会影响Data
父子关系onAdded(data)
图元添加时回调函数,可重载做后续处理onRemoved(data)
图元删除时回调函数,可重载做后续处理contains(data)
判断容器是否包含该data
对象size()
返回当前容器中Data
对象的总数isEmpty()
判断容器是否为空getRoots()
返回所有parent
为空的Data
对象getDataById(id)
返回指定id
的Data
对象removeDataById(id)
删除指定id
的Data
对象getDataByTag(tag)
返回指定tag
标示的Data
对象removeDataByTag(tag)
删除指定tag
标示的Data
对象each(func, scope)
遍历所有Data
对象eachByDepthFirst(func, data, scope)
以data
为起始深度优先遍历Data
对象eachByBreadthFirst(func, data, scope)
以data
为起始广度优先遍历Data
对象getDatas()
返回所有添加到容器的Data
数据ht.List
数组toDatas(matchFunc, scope)
返回筛选后的新ht.List
对象数组,第一参数为空相当于复制全部对象数组getSelectionModel()
获取该容器的选择模型,可用简写sm()
addDataModelChangeListener(function(e){}, scope)
增加数据模型增删变化事件监听器,可用简写mm(func, scope)
e.kind === 'add'
代表添加Data
对象,e.data
为被添加的对象e.kind === 'remove'
代表删除Data
对象,e.data
为被删除的对象e.kind === 'clear'
代表容器被清除removeDataModelChangeListener(func, scope)
删除数据模型增删变化事件监听器,可用简写umm(func, scope)
addDataPropertyChangeListener(function(e){}, scope)
增加模型中Data
数据属性变化事件监听器,可用简写md(func, scope)
e.data
代表属性变化的对象e.property
代表变化属性的名字e.newValue
代表属性的新值e.oldValue
代表属性的老值Data
对象在设置属性值函数内调用firePropertyChange(property, oldValue, newValue)
触发属性变化事件:get/set
类型属性,如setAge(98)
触发事件的e.property
为age
style
类型属性名前加s:
前缀以区分,如setStyle('age', 98)
触发事件的e.property
为s:age
attr
类型属性名前加a:
前缀以区分,如setAttr('age', 98)
触发事件的e.property
为a:age
removeDataPropertyChangeListener(func, scope)
删除模型中Data
数据属性变化事件监听器,可用简写umd(func, scope)
onDataPropertyChanged(data, e)
图元属性变化回调函数,可重载做后续处理getSiblings(data)
获取和data
同父子层次的兄弟数组,如果data父亲为空,则返回dataModel.getRoots()
moveTo(data, newIndex)
移动图元到同层兄弟数组中得指定索引moveUp(data)
移动图元到同层兄弟数组中的上一个位置moveDown(data)
移动图元到同层兄弟数组中的下一个位置moveToTop(data)
移动图元到同层兄弟数组的顶部moveToBottom(data)
移动图元到同层兄弟数组的底部moveSelectionUp(sm)
移动当前选中图元到同层兄弟数组中的上一个位置,sm
为空则采用DataModel
绑定的选中模型moveSelectionDown(sm)
移动当前选中图元到同层兄弟数组中的下一个位置,sm
为空则采用DataModel
绑定的选中模型moveSelectionToTop(sm)
移动当前选中图元到同层兄弟数组的顶部,sm
为空则采用DataModel
绑定的选中模型moveSelectionToBottom(sm)
移动当前选中图元到同层兄弟数组的底部,sm
为空则采用DataModel
绑定的选中模型serialize(space)
将数据模型序列化成JSON
格式字符串,space
为缩进空格数toJSON
将数据模型序列化成JSON
格式对象deserialize(json, rootParent, setId)
反序列化数据到数据模型json
数据信息对象,用于解析生成对应的Data
对象并添加到数据容器rootParent
父节点对象,如果不为空,则反序列化的对象若无父亲者,设置rootParent
为其父亲setId
指定反序列化时,是否设置json
信息上的id
值通过下面
firePropertyChange
的代码片段可以知道,oldValue
和newValue
相同时属性变化事件不会派发, 属性变化事件通过handleDataPropertyChange
传递给DataModel
继续做处理, 后续处理包括继续派发事件给通过addDataPropertyChangeListener
添加到DataModel
的属性变化监听器。
firePropertyChange: function (property, oldValue, newValue) {
if (oldValue === newValue) {
return false;
}
var e = {
property: property,
oldValue: oldValue,
newValue: newValue,
data: this
};
if (this._dataModel) {
this._dataModel.handleDataPropertyChange(e);
}
this.onPropertyChanged(e);
return true;
}
ht.SelectionModel
管理DataModel
模型中Data
对象的选择状态,
每个DataModel
对象都内置一个SelectionModel
选择模型,控制这个SelectionModel
即可控制所有绑定该DataModel
的组件的对象选择状态,
这意味着共享同一DataModel
的组件默认就具有选中联动功能。
如果希望某些组件不与其他组件选中联动,可通过调用view.setSelectionModelShared(false)
,
这样该view
将创建一个专属的SelectionModel
实例。
综上所述有两种途径可得到SelectionModel
:
dataModel.getSelectionModel()
获取数据容器中组件共享的选中模型。view.getSelectionModel()
获取当前组件使用的选中模型,selectionModelShared
为false
时,返回view
专用的选择模型。SelectionModel
常用函数如下:
getSelectionMode()
和setSelectionMode(selectionMode)
获取和设置选中模式none
:不可选中。single
:只可单选。multiple
:默认值,允许多选。getFilterFunc()
和setFilterFunc(func)
设置过滤器自定义可选择对象规则,参见过滤器章节appendSelection(datas)
追加选中对象,参数可为单个对象,也可为ht.List
或Array
数组,简写为as
setSelection(datas)
设置选中对象,参数可为单个对象,也可为ht.List
或Array
数组,简写为ss
removeSelection(datas)
取消选中对象,参数可为单个对象,也可为ht.List
或Array
数组,简写为rs
clearSelection()
取消所有选中对象,简写为cs
selectAll()
选中DataModel
中所有对象,简写为sa
size()
返回当前选中对象个数isEmpty()
判断当前是否没有选中对象contains(data)
判断data
对象是否被选中,简写为co
getFirstData()
返回首个被选中的对象,如果没有选中对象则返回空,简写为fd
getLastData()
返回最后被选中的对象,如果没有选中对象则返回空,简写为ld
each(function(data){}, scope)
遍历所有被选中对象getSelection()
获取所有被选中对象数组,注意不可直接对返回数组进行增删操作。toSelection(matchFunc, scope)
返回过滤后的选中对象,matchFunc
为空时代表复制全部到新数组addSelectionChangeListener(function(e){}, scope)
增加监听器,监听选中变化事件,简写为ms
:e.datas
包含所有选中状态变化的对象,之前选中现在取消选中,或之前没选中现在被选中的对象e.kind === 'set'
代表此事件由setSelection(datas)
引发e.kind === 'remove'
代表此事件由removeSelection(datas)
引发e.kind === 'append'
代表此事件由appendSelection(datas)
引发e.kind === 'clear'
代表此事件由clearSelection(datas)
引发removeSelectionChangeListener(function(e){}, scope)
删除选中变化事件监听器,简写为ums
:index = 0;
dataModel = new ht.DataModel();
selectionModel = dataModel.getSelectionModel();
// monitor data property change event
dataModel.addDataPropertyChangeListener(function(e){
document.getElementById('property').innerText = e.data + '\'s ' + e.property + ' changed';
});
// monitor data model change event
dataModel.addDataModelChangeListener(function(e){
var output;
if(e.kind === 'add'){
output = e.data + ' added, ';
}
else if(e.kind === 'remove'){
output = e.data + ' removed, ';
}
else if(e.kind === 'clear'){
output = 'data model cleared, ';
}
output += 'size:' + dataModel.size();
document.getElementById('model').innerText = output;
});
// monitor selection model change event
selectionModel.addSelectionChangeListener(function(e){
var output = '';
size = selectionModel.size();
if(size === 0){
output = 'nothing selected';
}
else if(size === 1){
output = selectionModel.getLastData() + ' selected';
}
else{
output = size + ' datas selected';
}
document.getElementById('selection').innerText = output;
});
graphPane.getGraphView().setEditable(true);
addData();
addData();
selectionModel.setSelection(addData());
function addData(){
var node = new ht.Node();
node.setPosition(50 + index % 12 * 50, 50);
node.setName('node' + index++);
dataModel.add(node);
return node;
}
function removeData(){
while(selectionModel.size() > 0){
dataModel.remove(selectionModel.getLastData());
}
}