索引
正确使用合适的设计模式可极大提高框架的可维护性和可扩展性,良好的设计接口可极大减少用户代码量并提高可读性, 有利于大规模项目开发的团队分工,甚至不需要接入后台数据即可完成对前端组件的单元测试, 有助于组件和模型的数据绑定和数据同步, 进而有助于开发厂商进一步提供所见即所得的可视化开发工具, 如 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或removeChildaddChild(child, index)添加孩子节点,index为孩子插入索引,为空则插入作为最后的孩子,内部会自动调用setParentremoveChild(child)删除指定孩子节点,内部会自动调用setParentclearChildren()删除所有孩子节点,内部会自动调用setParentonChildAdded(child, index)添加孩子时的回调函数,可重载做后续处理onChildRemoved(child, index)删除孩子时的回调函数,可重载做后续处理onParentChanged(oldParent, parent)改变父亲节点时的回调函数,可重载做后续处理 size()返回孩子总数hasChildren()判断是否有孩子,有则返回true,无则返回falseisEmpty()判断是否有孩子,有则返回false,无则返回truegetChildren()获取所有孩子节点,该函数返回内部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为agestyle类型属性名前加s:前缀以区分,如setStyle('age', 98)触发事件的e.property为s:ageattr类型属性名前加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数组,简写为assetSelection(datas)设置选中对象,参数可为单个对象,也可为ht.List或Array数组,简写为ssremoveSelection(datas)取消选中对象,参数可为单个对象,也可为ht.List或Array数组,简写为rsclearSelection()取消所有选中对象,简写为csselectAll()选中DataModel中所有对象,简写为sasize()返回当前选中对象个数isEmpty()判断当前是否没有选中对象contains(data)判断data对象是否被选中,简写为cogetFirstData()返回首个被选中的对象,如果没有选中对象则返回空,简写为fdgetLastData()返回最后被选中的对象,如果没有选中对象则返回空,简写为ldeach(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());
}
}