数据呈现是RIA应用中的一个重点应用,各种JavaScript框架也一般都提供了自己的Grid小部件用于呈现表格类数据。而TreeGrid作为一种特殊的Grid,顾名思义,更是兼具了Tree多层级结构以及Grid的多数据项复杂数据展示的优点,是一种很好的处理复杂多级数据的控件。然而,无论对于Tree或者TreeGrid,通常由于实现方面的种种限制,对数据的延迟加载只能是针对层级结构而言的,即在展开某一节点时即时请求该节点下的全部子节点。尽管这对于一般的应用场景来说基本可以满足需求,但在当次级节点下数据结构较复杂,节点繁多的情况下,则可能造成极其严重的性能问题。针对这一特定需求,Dojo从1.6开始推出了一个全新的控件——LazyTreeGrid。
作为Dojo DataGrid的扩展,LazyTreeGrid在整体的数据结构上采用的仍然是MVC结构。LazyTreeGrid的结构
图1. LazyTreeGrid结构模型
图1就是TreeGrid的一个基本架构模型,就整体结构而言,LazyTreeGrid与TreeGrid、DataGrid并没有太大区别。视图即为用户直接可见的部分,包括了Grid的表头、行、列、单元格及TreeGrid特有的节点展开按钮等直观内容,整个TreeGrid通过内容视图中的虚拟滚动条的滚动事件以及节点展开按钮Expando的展开或关闭事件的触发来获取数据并构建内容。
然而,为了满足针对次级节点的分页延迟加载及渲染功能,LazyTreeGrid则需要基于树状层级结构要求在Model和View部分进行相应的扩展。下面就基于LazyTreeGrid的数据模型及视图结构来对其设计思路及实现方式做一个简单介绍。
LazyTreeGrid的数据模型
正常状态的树状结构数据是层级嵌套模式的,如下例所示:
data = { identifier: 'id',label: 'name',items: [ { id: 'AF',name: 'Africa',children: [ { id: 'EG',name: 'Egypt' },{ id: 'KE',name: 'Kenya',children: [ { id: 'Nairobi',name: 'Nairobi',type: 'city' },{ id: 'Mombasa',name: 'Mombasa',type: 'city' } ] },... ] },... ] }
与其他的可延迟加载设计的Tree类型应用的数据实现要求类似,为了达到延迟加载次级数据的目的,需要对父节点数据做压平处理,将其与子节点在结构上进行分离,这样才可以在进行数据的最初请求时只加载必须的父节点数据,仅在展开父节点时再延迟加载其下的次级数据。另外,在LazyTreeGrid中,对于某些子节点数目很少,不需要延迟加载的情况,这里也允许存在未被压平的数据节点,如下例所示:
LazyTreeGrid所要求的数据结构允许父节点将原有的嵌套数据替换为一个正整数数值或布尔值,用以代表其下的子节点数目或者是否有子节点(false,非正数或者没有相应属性都代表该节点没有子节点)。需要注意的是,LazyTreeGrid要求数据必须拥有一个唯一的主键id,这样才可以通过该id去服务器端请求相应数据条目的子数据。在LazyTreeGrid数据模型(model)部分,除了DataGrid原有的DataStore(Dojo数据存储器)外,由于Dojo本身的DataStore对树形数据结构的API方面的支持不足,另外增加了一个TreeModel用于提供针对树形结构数据的特定支持。而为了能够满足分页加载次级数据的要求,LazyTreeGrid实现了一个特殊的TreeModel:dojox.grid.LazyTreeGridStoreModel,其主要功能就是建立一个后台数据获取协议,通过指定父节点与子节点序列来使服务器端正确返回相应的分页数据,在请求次级数据时,LazyTreeGrid将向后台服务端发送类似如下的一条请求:
http://localhost:8080/TreeGrid/FakeDataServlet?parentId=root1&start=0&count=25
在这里,parentId即为要请求数据的父节点id,而start和count分别代表了请求起始的子节点序列和请求的节点个数。
下面的代码给出了如何建立一个简单的LazyTreeGridStoreModel:
// programmatic var treeModel = new dojox.grid.LazyTreeGridStoreModel({ store: queryReadStore,serverStore: true }); // declarative <span data-dojo-type="dojox.grid.LazyTreeGridStoreModel" data-dojo-props="store:queryReadStore,serverStore:true" > </span>建立一个LazyTreeGridStoreModel需要确定两个参数:store和serverStore,store用于指定获取数据的dojo DataStore;serverStore接收一个布尔值,用于确定是否数据由服务器端传递且满足数据条目是被压平存储、传输的(存储在客户端的数据没必要采用延迟加载模式)。对于次级数据量不大,不需要分页加载子数据的情况,用户也可以选择使用Dojo原有的dijit.tree.ForestStoreModel。
LazyTreeGrid的视图(View)
在Dojo1.6之前存在的dojo.grid.TreeGrid,采用的视图构建方式是认为所有的子节点都是最上级父节点的内容扩展,即在一行之内渲染出所有的展开的子节点结构,如下图所示:
在这个图例中,Grid每行最左侧的就是rowSelector——行选择按钮,根据rowSelector的分配情况,我们就可以清楚的看出其行结构是按第一级节点进行划分的。
尽管就整体视图结构来看这一做法并无不妥,但由于TreeGrid复用了DataGrid的按行结构进行分页的延迟加载与渲染机制,因此位于当前页的所有行的内容就都会被一次加载及渲染。那么当次级节点较多、较复杂的情况下,这种加载,尤其是渲染所带来的资源消耗以及响应时间就会变得非常突出和难以忍受了。
在LazyTreeGrid中,根据在数据模型中对数据进行的预处理,在保留层级信息的基础上对数据进行了分离处理,这样就可以在视图中将各条数据都作为一条独立数据行进行加载渲染。由图3中可以看出,每条数据都是单独的一行,因此,在复用了DataGrid的Virtual Scroller机制的前提下,即使数据中包含了很多的次级节点,也会忽略其层级结构,仅根据对Grid的分页配置进行划分,延迟加载与渲染数据条目,从而达到性能上的极大提升。
LazyTreeGrid中的其他特性
在树状结构数据中,可能会存在不同级别的数据条目中的数据列不完全相同的情况,更常见的是上级数据为概要据,而次级数据则作为详细数据存在。因此在这一类情况下,用户有可能需要对不同级别数据定制出不同的表达格式。针对这种需求,LazyTreeGrid分别支持分级合并单元格设置和分级数据格式化设置。
图4就是一个基于一个简单化的存储管理表格示例,最顶级数据是存储器群组,第二级和第三级数据分别是物理磁盘和虚拟磁盘。在这一示例中,就根据根级数据和次级数据的数据不一致性做了分级合并单元格和分级单元格格式化,使得整体视图更加清晰明了。
分级合并单元格需要在声明LazyTreeGrid时为其添加一个colSpans属性:
colSpans: {0: [ {start: 0,end: 1},{start: 2,end: 3,primary: 2}], 1: […],2:… }colSpans属性接收一个JSON对象,该对象中的键0/1/2/…分别对应各个级别,其值则用于指定单元格合并的细节,start代表从第几个单元格开始合并,end指合并到第几个单元格(都是以0作为起始值),而primary则指示了合并后显示第几个单元格的内容,在没有指定的情况下,其默认值等于start的值。
而分级单元格格式化则继承了与DataGrid的单元格格式化相同的方式,即分别为各个列设定formatter函数,不同的是,所提供的formatter函数的第三个参数为该行数据所在的级别,下例给出了一个简单的formatter函数:
var fmtByLevel = function(value,idx,level) { return level == 0 ? "root" : level == 1 ? "2nd" : "3rd"; };LazyTreeGrid的使用
下面,我们就基于图4中的示例,简述如何构建一个LazyTreeGrid。
简单而言,由于LazyTreeGrid继承于DataGrid,所以其基本创建过程与DataGrid基本一致,主要的区别就是前面提到过的需要在Grid和DataStore之间加入一个TreeModel。下面就是以JS编程方式创建这一LazyTreeGrid的前台代码示例。
首先需要对LazyTreeGrid的视图结构进行定义:
// LazyTreeGrid结构定义 var layout = [{ name: "Name",field: "name",width: "150px",formatter: fmtName },{ name: "Status",field: "status",width: "40px",formatter: fmtStatus },{ name: "Capacity",field: "capacity",width: "80px",formatter: fmtCapacity },{ name: "UID",field: "uid",width: "240px" }];Layout主要定义了Grid的列名、列宽度、相应的取值数据项等,另外我们也可以看到,针对前三个列分别定义了三个formatter函数,用于定义分级的单元格格式化。其中的fmtCapacity函数采用了对第一级数据返回一个progressbar的小部件:
// progress bar formatter var fmtCapacity = function (value,level) { return level == 0 new dijit.ProgressBar({ progress: value * 100,maximum: 100,report: function (percent) { return (percent * 100).toFixed(2) + "% Storage Online"; } }): value + "GB"; };DataStore方面采用的是dojox.data.QueryReadStore,通过同域中的一个servlet获取数据,并基于这个DataStore创建了一个LazyTreeGridStoreModel来连接到LazyTreeGrid。如下段代码所示:
// 创建DataStore var queryReadStore = new dojox.data.QueryReadStore({ id: "queryReadStore",url: "FakeDataServlet" }); // 创建TreeModel var treeModel = new dojox.grid.LazyTreeGridStoreModel({ id: "treeModel",serverStore: true,store: queryReadStore });最后,我们需要在页面中放入一个id为'gridContainer'的div作为LazyTreeGrid的容器元素,并在页面加载完成后对LazyTreeGrid的创建及初始化操作:
// 创建LazyTreeGrid var grid; dojo.addOnLoad(function () { grid = new dojox.grid.LazyTreeGrid({ id: "grid",rowSelector: true,treeModel: treeModel,structure: layout,colSpans: { 0: [{ start: 0,end: 1 },& nbsp; { start: 2,primary: 2 }] } }); dojo.byId('gridContainer').appendChild(grid.domNode); grid.startup(); });至此,我们已经完成了LazyTreeGrid的页面创建工作,接下来的工作就是创建合适的后台代码来正确响应LazyTreeGrid的数据请求。
小结
作为Dojo1.6新引入的一个Widget,尽管LazyTreeGrid仍有很多亟待解决的缺陷或问题,如尚不支持初始化及运行时的Expand/Collapse All的功能、可调用API较少等,但它作为一种呈现复杂多层级数据的RIA应用小部件,其对于延迟加载与延迟渲染方面所提出的解决方案还是具有一定的突破性的,具有这方面应用要求的用户不妨一试。
本文已经首发于InfoQ中文站,版权所有,原文为《Dojo中的LazyTreeGrid控件》,如需转载,请务必附带本声明,谢谢。
InfoQ中文站是一个面向中高端技术人员的在线独立社区,为Java、.NET、Ruby、SOA、敏捷、架构等领域提供及时而有深度的资讯、高端技术大会如QCon、线下技术交流活动QClub、免费迷你书下载如《架构师》等。