HT for Web 数据模型手册

索引


设计模式

正确使用合适的设计模式可极大提高框架的可维护性和可扩展性,良好的设计接口可极大减少用户代码量并提高可读性, 有利于大规模项目开发的团队分工,甚至不需要接入后台数据即可完成对前端组件的单元测试, 有助于组件和模型的数据绑定和数据同步, 进而有助于开发厂商进一步提供所见即所得的可视化开发工具, 如 Adobe Flash BuilderMicrosoft Visual Studio

MVC

在早期的GUI设计领域,Java/SwingSWT/JFaceCocoaQt 都是基于 Model View Controller (MVC) 设计模式的典范。

MVP

MVC之后慢慢发展衍生的 Model View Presenter (MVP) 设计模式逐渐被新的GUI框架所采用, 如基于FlashApache/Flex 企业应用组件,以及在Swing基础上封装的 JGoodies 框架。 Martin FowlerGUI Architectures 对这两种设计模式进行了分析比较。

MVVM

近些年在MVP基础上再增加ViewModel层衍生的 Model View ViewModel (MVVM) 设计模式, 也被微软新一代的GUI开发框架 Silverlight/WPF 采用, 包括像 Knockout 这种为简化前端HTML组件和js数据绑定的框架也是基于MVVM设计模式。

OVM

MVC/MVP/MVVM等设计模式的提出,方便了大家对不同框架的理解和归类,任何GUI框架在具体实现上都会有自己的演变和特性, HT的整体框架更类似MVPMVVM的设计模式,但我们更倾向于称HT的设计模式为 Object View Mapping (OVM), 类比于 Object Relational Mapping (ORM), 通过面对对象方式的封装,屏蔽了各种视图组件的异构性,采用了统一的 DataModel 数据模型和 SelectionModel 选择模型,可驱动 ListViewTreeViewTableViewTreeTableViewGraphViewGraph3dView 等所有HT的视图组件。

HT这样的设计架构,用户仅需掌握统一的数据驱动接口,不会因视图组件增多带来额外学习成本,这是HT易上手易精通的根本。

数据类型

Data类型贯穿整个HT框架,是最基础数据类型。

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对象。

idtag的方式都是针对唯一标识的Data对象,若搜索非唯一属性可采用ht.QuickFinder插件

使用DataModel时需要特别注意:一般要求有父子关系的Data都应逐一加入容器。常遇到parent加入容器,但children未加入, 导致组件看不到children的问题,因为添加parent并不会自动加载所有子孙,这点务必注意。

Data类型有getDataModel()函数,当Data加入容器后data.getDataModel()能获得当前所在容器信息, 不允许一个Data对象同时加入多个DataModel容器中。

通过下面firePropertyChange的代码片段可以知道,oldValuenewValue相同时属性变化事件不会派发, 属性变化事件通过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

SelectionModel常用函数如下:

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());
    }
} 

欢迎交流 service@hightopo.com