Extjs 数据的传输和存储

本章内容

q   Ext.data简介

本章内容

q   Ext.data简介

q   Ext.data.Connection

q   Ext.data.Record

q   Ext.data.Store

q   常用proxy

q   常用reader

q   高级store

q   EXT中的Ajax

q   关于scopecreateDelegate()

q   DWREXT整合

10.1Ext.data简介

Ext.data在命名空间中定义了一系列storereaderproxyGridComboxBox都是以Ext.data为媒介获取数据的,它包含异步加载、类型转换、分页等功能。Ext.data默认支持ArrayJSONXML等数据格式,可以通过MemoryHTTPScriptTag等方式获得这些格式的数据。如果要实现新的协议和新的数据结构,只需要扩展readerproxy即可。DWRProxy就实现了自身的proxyreader,让EXT可以直接从DWR获得数据。

10.2Ext.data.Connection

Ext.data.Connection是对Ext.lib.Ajax的封装,它提供了配置使用Ajax的通用方式,它在内部通过Ext.lib.Ajax实现与后台的异步调用。与底层的Ext.lib.Ajax相比,Ext.data. Connection提供了更简洁的配置方式,使用起来更方便。

Ext.data.Connection主要用于在Ext.data.HttpProxyExt.data.ScriptTagProxy中执行与后台交互的任务,它会从指定的URL获得数据,并把后台返回的数据交给HttpProxyScriptTagProxy处理,Ext.data.Connection的使用方式如代码清单10-1所示。

代码清单10-1 使用Ext.data.Connection

var conn = newExt.data.Connection({

    autoAbort:false,

    defaultHeaders:{

        referer:'http://localhost:8080/'

    },

    disableCaching: false,

    extraParams: {

        name:'name'

    },

    method: 'GET',

    timeout: 300,

    url: '01-01.txt'

});

在使用Ext.data.Connection之前,都要像上面这样创建一个新的Ext.Connection实例。我们可以在构造方法里配置对应的参数,比如autoAbort表示链接是否会自动断开、default- Headers参数表示请求的默认首部信息、disableCaching参数表示请求是否会禁用缓存、extraParams参数代表请求的额外参数、method参数表示请求方法、timeout参数表示连接的超时时间、url参数表示请求访问的网址等。

在创建了conn之后,可以调用request()函数发送请求,处理返回的结果,如下面的代码所示。

conn.request({

    success:function(response) {

        Ext.Msg.alert('info',response.responseText);

    },

    failure:function() {

        Ext.Msg.alert('warn','failure');

    }

});

Request()函数中可以设置successfailure两个回调函数,分别在请求成功和请求失败时调用。请求成功时,success函数的参数就是后台返回的信息。

我们再来看一下request函数中的其他参数。

q   url:String:请求url

q   params:Object/String/Function:请求传递的参数。

q   method:String:请求方法,通常为GETPOST

q   callback:Function:请求完成后的回调函数,无论是成功还是失败,都会执行。

q   success:Function:请求成功时的回调函数。

q   failure:Function:请求失败时的回调函数

q   scope:Object:回调函数的作用域。

q   form:Object/String:绑定的form表单。

q   isUpload:Boolean:是否执行文件上传。

q   headers:Object:请求首部信息。

q   xmlData:ObjectXML文档对象,可以通过URL附加参数的方式发起请求。

q   disableCaching:Boolean:是否禁用缓存,默认为禁用。

Ext.data.Connection还提供了abort([Number transactionId])函数,当同时有多个请求发生时,根据指定的事务id放弃其中的某一个请求。如果不指定事务id,就会放弃最后一个请求。isLoading([NumbertransactionId])函数的用法与abort()类似,可以根据事务id判断对应的请求是否完成。如果未指定事务id,就判断最后一个请求是否完成。

10.3Ext.data.Record

Ext.data.Record就是一个设定了内部数据类型的对象,它是Ext.data.Store的最基本组成部分。如果把Ext.data.Store看作是一张二维表,那么它的每一行就对应一个Ext.data. Record实例。

Ext.data.Record的主要功能是保存数据,并且在内部数据发生改变时记录修改的状态,它还可以保留修改之前的原始值。

我们使用Ext.data.Record时通常都是由create()函数开始,首先用create()函数创建一个自定义的Record类型,如下面的代码所示。

var PersonRecord= Ext.data.Record.create([

    {name:'name', type: 'string'},

    {name:'sex', type: 'int'}

]);

PersonRecord就是我们定义的新类型,包含字符串类型的name和整数类型的sex两个属性,然后我们使用new关键字创建PersonRecord的实例,如下面的代码所示。

var boy = new PersonRecord({

    name:'boy',

    sex:0

});

创建对象时,可以直接通过构造方法为对象赋予初始值,将'boy'赋值给name0赋值给sex

现在,我们得到了PersonRecord的实例boy,如何才能得到它的属性呢?以下三种方式都可以获得boyname属性的数据,如下面的代码所示。

alert(boy.data.name);

alert(boy.data['name']);

alert(boy.get('name'));

这里涉及Ext.data.Recorddata属性,这是定义在Ext.data.Record中的一个公共属性,用于保存当前record对象的所有数据。它是一个JSON对象,可以直接从它里面获得需要的数据。可以通过Ext.data.Recordget()函数方便地从data属性中获得指定的属性值。

如果我们需要修改boy中的数据,请不要使用以下方式直接操作data,如下面的代码所示。

    boy.data.name= 'boy name';

    boy.data['name']= 'boy name';

而应该使用set()函数,如下面的代码所示。

    boy.set('name','body name');

set()函数会判断属性值是否发生了改变,如果改变了,就要将当前对象的dirty属性设置为true,并将修改之前的原始值放入modified对象中,供其他函数使用。如果直接操作data中的值,record就无法记录属性数据的修改情况。

Record的属性数据被修改后,我们可以执行如下几种操作。

q   commit()(提交):这个函数的效果是设置dirtyfalse,并删除modified中保存的原始数据。

q   reject()(撤销):这个函数的效果是将data中已经修改了的属性值都恢复成modified中保存的原始数据,然后设置dirtyfalse,并删除保存原始数据的modified对象。

q   getChanges()获得修改的部分:这个函数会把data中经过修改的属性和数据放在一个JSON对象里并返回。例如上例中,getChanges()返回的结果是{name:’body name’}

q   我们还可以调用isModified()判断当前record中的数据是否被修改。

      Ext.data.Record还提供了用于复制record实例的函数copy()

  varcopyBoy = boy.copy();

这样我们就得到了boy的一个副本,它里面包含了boydata数据,但copy()函数不会复制dirtymodified等额外的属性值。

Ext.data.Record中其他的参数大多与Ext.data.Store有关,请参考与Ext.data.Store相关的讨论。

10.4Ext.data.Store

Ext.data.StoreEXT中用来进行数据交换和数据交互的标准中间件,无论是Grid还是ComboBox,都是通过它实现数据读取、类型转换、排序分页和搜索等操作的。

Ext.data.Store中有一个Ext.data.Record数组,所有数据都存放在这些Ext.data. Record实例中,为后面的读取和修改操作做准备。

10.4.1 基本应用

在使用之前,首先要创建一个Ext.data.Store的实例,如下面的代码所示。

var data = [

    ['boy',0],

    ['girl',1]

];

var store = newExt.data.Store({

    proxy:new Ext.data.MemoryProxy(data),

reader:new Ext.data.ArrayReader({}, PersonRecord)

});

store.load();

每个store最少需要两个组件的支持,分别是proxyreaderproxy用于从某个途径读取原始数据,reader用于将原始数据转换成Record实例。

这里我们使用的是Ext.data.MemoryProxyExt.data.ArrayReader,将data数组中的数据转换成对应的几个PersonRecord实例,然后放入store中。store创建完毕之后,执行store.load()实现这个转换过程。

经过转换之后,store里的数据就可以提供给GridComboBox使用了,这就是Ext.data. Store的最基本用法。

10.4.2 对数据进行排序

Ext.data.Store提供了一系列属性和函数,利用它们对数据进行排序操作。

可以在创建Ext.data.Store时使用sortInfo参数指定排序的字段和排序方式,如下面的代码所示。

var store = newExt.data.Store({

    proxy:new Ext.data.MemoryProxy(data),

reader:new Ext.data.ArrayReader({}, PersonRecord),

    sortInfo:{field: 'name', direction: 'DESC'}

});

这样,在store加载数据之后,就会自动根据name字段进行降序排列。对store使用store.setDefaultSort('name','DESC');也会达到同样效果。

也可以在任何时候调用sort()函数,比如store.sort('name','DESC');,对store中的数据进行排序。

如果我们希望获得store的排序信息,可以调用getSortState()函数,返回的是类似{field: "name", direction: " DESC"}JSON对象。

与排序相关的参数还有remoteSort,这个参数是用来实现后台排序功能的。当设置为remoteSort:true时,store会在向后台请求数据时自动加入sortdir两个参数,分别对应排序的字段和排序的方式,由后台获取并处理这两个参数,在后台对所需数据进行排序操作。remoteSort:true也会导致每次执行sort()时都要去后台重新加载数据,而不能只对本地数据进行排序。

详细的用法可以参考第2章。

10.4.3 从store中获取数据

store中获取数据有很多种途径,可以依据不同的要求选择不同的函数。最直接的方法是根据recordstore中的行号获得对应的record,得到了record就可以使用get()函数获得里面的数据了,如下面的代码所示。

store.getAt(0).get('name')

通过这种方式,我们可以遍历store中所有的record,依次得到它们的数据,如下面的代码所示。

for (var i = 0;i < store.getCount(); i++) {

    varrecord = store.getAt(i);

    alert(record.get('name'));

}

Store.getCount()返回的是store中的所有数据记录,然后使用for循环遍历整个store,从而得到每条记录。

除了使用getCount()的方法外,还可以使用each()函数,如下面的代码所示。

store.each(function(record){

    alert(record.get('name'));

});

Each()可以接受一个函数作为参数,遍历内部record,并将每个record作为参数传递给function()处理。如果希望停止遍历,可以让function()返回false

也可以使用getRange()函数连续获得多个record,只需要指定开始和结束位置的索引值,如下面的代码所示。

var records =store.getRange(0, 1);

for (var i = 0;i < records.length; i++) {

    varrecord = records[i];

    alert(record.get('name'));

}

如果确实不知道recordid,也可以根据record本身的idstore中获得对应的record,如下面的代码所示。

store.getById(1001).get('name')

EXT还提供了函数find()findBy(),可以利用它们对store中的数据进行搜索,如下面的代码所示。

find( Stringproperty, String/RegExp value, [Number startIndex], [Boolean anyMatch],

[BooleancaseSensitive] )

在这5个参数中,只有前两个是必须的。第一个参数property代表搜索的字段名;第二个参数value是匹配用字符串或正则表达式;第三个参数startIndex表示从第几行开始搜索,第四个参数anyMatchtrue时,不必从头开始匹配;第五个参数caseSensitivetrue时,会区分大小写。

如下面的代码所示:

var index =store.find('name','g');

alert(store.getAt(index).get('name'));

find()函数对应的findBy()函数的定义格式如下:

findBy( Functionfn, [Object scope], [Number startIndex] ) : Number

findBy()函数允许用户使用自定义函数对内部数据进行搜索。fn返回true时,表示查找成功,于是停止遍历并返回行号。fn返回false时,表示查找失败(即未找到),继续遍历,如下面的代码所示。

index =store.findBy(function(record, id) {

    returnrecord.get('name') == 'girl' && record.get('sex') == 1;

});

alert(store.getAt(index).get('name'));

通过findBy()函数,我们可以同时判断record中的多个字段,在函数中实现复杂逻辑。

我们还可以使用queryqueryBy函数对store中的数据进行查询。与findfindBy不同的是,queryqueryBy返回的是一个MixCollection对象,里面包含了搜索得到的数据,如下面的代码所示。

alert(store.query('name','boy'));

    alert(store.queryBy(function(record){

        returnrecord.get('name') == 'girl' && record.get('sex') == 1;

    }));

10.4.4 更新store中的数据

可以使用add(Ext.data.Record[] records)store末尾添加一个或多个record,使用的参数可以是一个record实例,如下面的代码所示。

store.add(newPersonRecord({

    name:'other',

    sex:0

}));

Add()的也可以添加一个record数组,如下面的代码所示:

store.add([newPersonRecord({

    name:'other1',

    sex:0

}), newPersonRecord({

    name:'other2',

    sex:0

})]);

Add()函数每次都会将新数据添加到store的末尾,这就有可能破坏store原有的排序方式。如果希望根据store原来的排序方式将新数据插入到对应的位置,可以使用addSorted()函数。它会在添加新数据之后立即对store进行排序,这样就可以保证store中的数据有序地显示,如下面的代码所示。

store.addSorted(newPersonRecord({

    name:'lili',

    sex:1

}));

store会根据排序信息查找这条record应该插入的索引位置,然后根据得到的索引位置插入数据,从而实现对整体进行排序。这个函数需要预先为store设置本地排序,否则会不起作用。

如果希望自己指定数据插入的索引位置,可以使用insert()函数。它的第一个参数表示插入数据的索引位置,可以使用record实例或record实例的数组作为参数,插入之后,后面的数据自动后移,如下面的代码所示。

store.insert(3,new PersonRecord({

    name:'other',

    sex:0

}));

store.insert(3,[new PersonRecord({

    name:'other1',

    sex:0

}), new PersonRecord({

    name:'other2',

    sex:0

})]);

删除操作可以使用remove()removeAll()函数,它们分别可以删除指定的record和清空整个store中的数据,如下面的代码所示。

store.remove(store.getAt(0));

store.removeAll();

store中没有专门提供修改某一行record的操作,我们需要先从store中获取一个record。对这个record内部数据的修改会直接反映到store上,如下面的代码所示。

store.getAt(0).set('name','xxxx');

修改record的内部数据之后有两种选择:执行rejectChanges()撤销所有修改,将修改过的record恢复到原来的状态;执行commitChanges()提交数据修改。在执行撤销和提交操作之前,可以使用getModifiedRecords()获得store中修改过的record数组。

与修改数据相关的参数是pruneModifiedRecords,如果将它设置为true,当每次执行删除或reload操作时,都会清空所有修改。这样,在每次执行删除或reload操作之后,getModifiedRecords()返回的就是一个空数组,否则仍然会得到上次修改过的record记录。

10.4.5 加载及显示数据

store创建好后,需要调用load()函数加载数据,加载成功后才能对store中的数据进行操作。load()调用的完整过程如下面的代码所示。

store.load({

    params:{start:0,limit:20},

    callback:function(records, options, success){

        Ext.Msg.alert('info','加载完毕');

    },

    scope:store,

    add:true

});

q   params是在store加载时发送的附加参数。

q   callback是加载完毕时执行的回调函数,它包含3个参数:records参数表示获得的数据,options表示执行load()时传递的参数,success表示是否加载成功。

q   Scope用来指定回调函数执行时的作用域。

q   Addtrue时,load()得到的数据会添加在原来的store数据的末尾,否则会先清除之前的数据,再将得到的数据添加到store中。

一般来说,为了对store中的数据进行初始化,load()函数只需要执行一次。如果用params参数指定了需要使用的参数,以后再次执行reload()重新加载数据时,store会自动使用上次load()中包含的params参数内容。

如果有一些需要固定传递的参数,也可以使用baseParams参数执行,它是一个JSON对象,里面的数据会作为参数发送给后台处理,如下面的代码所示。

store.baseParams.start= 0;

store.baseParams.limit= 20;

store加载数据之后,有时不需要把所有数据都显示出来,这时可以使用函数filterfilterBystore中的数据进行过滤,只显示符合条件的部分,如下面的代码所示。

filter( Stringfield, String/RegExp value, [Boolean anyMatch],
[Boolean caseSensitive] ) : void

filter()函数的用法与之前谈到的find()相似,如下面的代码所示。

store.filter('name','boy');

对应的filterBy()findBy()类似,也可以在自定义的函数中实现各种复杂判断,如下面的代码所示。

store.filterBy(function(record){

    returnrecord.get('name') == 'girl' && record.get('sex') == 1;

});

如果想取消过滤并显示所有数据,那么可以调用clearFilter()函数,如下面的代码所示。

store.clearFilter();

如果想知道store上是否设置了过滤器,可以通过isFiltered()函数进行判断。

10.4.6 其他功能

除了上面提到的数据获取、排序、更新、显示等功能外,store还提供了其他一些功能函数。

collect( StringdataIndex, [Boolean allowNull], [Boolean bypassFilter] ) : Array

collect函数获得指定的dataIndex对应的那一列的数据,当allowNull参数为true时,返回的结果中可能会包含nullundefined或空字符串,否则collect函数会自动将这些空数据过滤掉。当bypassFilter参数为true时,collect的结果不会受查询条件的影响,无论查询条件是什么都会忽略掉,返回的信息是所有的数据,如下面的代码所示。

alert(store.collect('name'));

这样会获得所有name列的值,示例中返回的是包含了'boy''girl'的数组。

getTotalCount()用于在翻页时获得后台传递过来的数据总数。如果没有设置翻页,get- TotalCount()的结果与getCount()相同,都是返回当前的数据总数,如下面的代码所示。

alert(store.getTotalCount());

indexOf(Ext.data.Record record)indexOfId(String id)函数根据recordrecordid获得record对应的行号,如下面的代码所示。

alert(store.indexOf(store.getAt(1)));

alert(store.indexOfId(1001));

loadData(object data, [Boolean append])从本地JavaScript变量中读取数据,appendtrue时,将读取的数据附加到原数据后,否则执行整体更新,如下面的代码所示。

store.loadData(data,true);

Sum(String property, Number start, Number end):Number用于计算某一个列从startend的总和,如下面的代码所示。

alert(store.sum('sex'));

如果省略参数startend,就计算全部数据的总和。

store还提供了一系列事件(见表10-1),让我们可以为对应操作设定操作函数。

10-1store提供的事件

事件名

参  数

add

( Store this, Ext.data.Record[] records, Number index )

beforelaod

( Store this, Object options )

clear

( Store this )

datachanged

( Store this )

load

( Store this, Ext.data.Record[] records, Object options )

loadexception

()

metachange

( Store this, Object meta. )

remove

( Store this, Ext.data.Record record, Number index )

update

( Store this, Ext.data.Record record, String operation )

至此,storerecord等组件已经讲解完毕,下面我们主要讨论一下常用的proxyreader组件。

10.5 常用proxy

10.5.1MemoryProxy

MemoryProxy只能从JavaScript对象获得数据,可以直接把数组,或JSONXML格式的数据交给它处理,如下面的代码所示。

var proxy = new Ext.data.MemoryProxy([

    ['id1','name1','descn1'],

    ['id2','name2','descn2']

]);              

10.5.2HttpProxy

HttpProxy使用HTTP协议,通过Ajax去后台取数据,构造它时需要设置url:'xxx.jsp'参数。这里的url可以替换成任何一个合法的网址,这样HttpProxy才知道去哪里获取数据,如下面的代码所示。

var proxy = new Ext.data.HttpProxy({url:'xxx.jsp'});              

后台需要返回EXT所需要的JSON格式的数据,下面的内容就是后台使用JSP的一个范例,如下面的代码所示。

response.setContentType("application/x-json");

Writer out =response.getWriter();

out.print("[" +

        "['id1','name1','descn1']" +

        "['id2','name2','descn2']" +

    "]");              

请注意,这里的HttpProxy不支持跨域,它只能从同一域中获得数据。如果想跨域,请参考下面的ScriptTagProxy

10.5.3ScriptTagProxy

ScriptTagProxy的用法几乎和HttpProxy一样,如下面的代码所示。

var proxy = new Ext.data.ScriptTagProxy({url:'xxx.jsp'});              

从这里也看不出来它是如何支持跨域的,我们还需要在后台进行相应的处理,如下面的代码所示。

String cb =request.getParameter("callback");

response.setContentType("text/javascript");

Writer out =response.getWriter();

out.write(cb+ "(");

out.print("[" +

        "['id1','name1','descn1']" +

        "['id2','name2','descn2']" +

    "]");

out.write(");");

其中的关键就在于从请求中获得的callback参数,这个参数叫做回调函数。ScriptTag- Proxy会在当前的HTML页面里添加一个<scripttype="text/javascript"src="xxx.jsp"> </script>标签,然后把后台返回的内容添加到这个标签中,这样就可以解决跨域访问数据的问题。为了让后台返回的内容可以在动态生成的标签中运行,EXT会生成一个名为callback的回调函数,并把回调函数的名称传递给后台,由后台生成callback(data)形式的响应内容,然后返回给前台自动运行。

虽然上述处理过程比较难理解,但是我们只需要了解ScriptTagProxy的用法就足够了。如果还想进一步了解ScriptTagProxy的运行过程,可以使用Firebug查看动态生成的HTML以及响应的JSON内容。

最后我们来分析一下EXTAPI文档中提供的示例,这段后台代码会自动判断请求的类型,返回支持ScriptTagProxyHttpProxy的数据,如代码清单10-2所示。

代码清单10-2 在后台同时支持HttpProxyScriptTagProxy

boolean scriptTag = false;

String cb =request.getParameter("callback");

if (cb != null) {

    scriptTag= true;

    response.setContentType("text/javascript");

} else {

    response.setContentType("application/x-json");

}

Writer out =response.getWriter();

if (scriptTag) {

    out.write(cb+ "(");

}

out.print(dataBlock.toJsonString());

if (scriptTag) {

    out.write(");");

}              

代码中通过判断请求中是否包含callback参数来决定返回何种数据类型。如果包含,就返回ScriptTagProxy需要的数据;否则,就当作HttpProxy处理。

10.6 常用Reader

10.6.1ArrayReader

proxy中读取的数据需要进行解析,这些数据转换成Record数组后才能提供给Ext.data. Store使用。

ArrayReader的作用是从二维数组里依次读取数据,然后生成对应的Record。默认情况下是按列顺序读取数组中的数据,不过你也可以考虑用mapping指定record与原始数组对应的列号。ArrayReader的用法很简单,但缺点是不支持分页。使用二维数组的方式如下面的代码所示。

var data = [

    ['id1','name1','descn1'],

    ['id2','name2','descn2']

];

对应的ArrayReader如下面的代码所示。

var reader = new Ext.data.ArrayReader({

    id:1

},[

    {name:'name',mapping:1},

    {name:'descn',mapping:2},

    {name:'id',mapping:0},

]);

我们演示的是字段顺序不一致的情况,如果字段顺序和列顺序一致,就不用额外配置mapping

10.6.2JsonReader

JavaScript中,JSON是一种非常重要的数据格式,key:value的形式比XML那种复杂的标签结构更容易理解,代码量也更小,很多人倾向于使用它作为EXT的数据交换格式。为Json- Reader准备的JSON数据如下面的代码所示。

var data = {

    id:0,

    totalProperty:2,

    successProperty:true,

    root:[

        {id:'id1',name:'name1',descn:'descn1'},

        {id:'id2',name:'name2',descn:'descn2'}

    ]

};

与数组相比,JSON的最大优点就是支持分页,我们可以使用totalProperty参数表示数据的总量。successProperty参数是可选的,可以用它判断当前请求是否执行成功,进而判断是否进行数据加载。在不希望JsonReader处理响应数据时,可以把successProperty设置成false

现在来讨论一下JsonReader,看看它是如何与上面的JSON数据对应的,如下面的代码所示。

var reader = new Ext.data.JsonReader({

    successProperty: "successproperty",

    totalProperty: "totalProperty",

    root: "root",

    id: "id"

}, [

    {name:'id',mapping:'id'},

    {name:'name',mapping:'name'},

    {name:'descn',mapping:'descn'}

]);

上例中的对应方式不够简洁,因为namemapping部分的内容是相同的,其实这里的mapping可以省略,默认会用name参数从JSON中获得对应的数据。如果不想与JSON里的名字一样,也可以使用mapping修改。不过,mapping在这里还有其他用途,如代码清单10-3所示。

代码清单10-3 为JsonReader设置mapping进行数据映射

var data = {

    id:0,

    totalProperty:2,

    successProperty:true,

    root:[

        {id:'id1',name:'name1',descn:'descn1',person:{

            id:1,name:'man',sex:'male'

        }},

        {id:'id2',name:'name2',descn:'descn2',person:{

            id:2,name:'woman',sex:'female'

        }}

    ]

};

var reader = new Ext.data.JsonReader({

    successProperty: "successproperty",

    totalProperty: "totalProperty",

    root: "root",

    id: "id"

}, [

    'id','name','descn',

    {name:'person_name',mapping:'person.name'},

    {name:'person_sex',mapping:'person.sex'}

]);

在上面的代码中,我们使用JSON支持更复杂的嵌套结构,其中的person对象自身就拥有id namesex等属性。在JsonReader中可以用mapping把这些嵌套的内部属性映射出来,赋予对应的record,而其他字段都不变。

10.6.3XmlReader

XML是非常通用的数据传输格式,XmlReader使用的XML格式的数据如代码清单10-4所示。

代码清单10-4XmlReader使用的XML格式的数据

<?xml version="1.0" encoding="utf-8"?>

<dataset>

    <id>1</id>

    <totalRecords>2</totalRecords>

    <success>true</success>

    <record>

        <id>1</id>

        <name>name1</name>

        <descn>descn1</descn>

    </record>

    <record>

        <id>2</id>

        <name>name2</name>

        <descn>descn2</descn>

    </record>

</dataset>

这里一定要用dataset作为XML根元素。再让我们看一下如何对XmlReader进行配置,从而读取上面示例中的XML数据,如下面的代码所示。

var reader = new Ext.data.XmlReader({

   totalRecords: 'totalRecords',

   success: 'success'

   record: 'record',

   id: "id"

}, ['id','name','descn']);              

XmlReader使用的参数与之前介绍的JsonReader有些不同,我们可以看到这里用到了totalRecordsrecord两个参数,其中totalRecords用来指定从’totalRecords’标签里获得后台数据总数,record则表示XML中放在record标签里的数据是我们需要显示的结果数据。其他两个参数successid的含义和JsonReader中对应的参数相似,分别用来判断操是否成功和这次返回的id。因为XML中的标签和reader里需要的名字是相同的,所以简化了配置,将[{name:’id’},{name:’name’},{name:’descn’}]直接写成了[‘id’,’name’,’descn’]

因为XmlReader不能将JavaScript中的字符串自动解析成XML格式的数据,因此我们需要利用其他方法进行演示。参考localXHR.js中构造XML的方式,我们有了下面的解决方案,如代码清单10-5所示。

代码清单10-5 通过本地字符串构造XML对象

var data = "<?xmlversion='1.0' encoding='utf-8'?>" +

    "<dataset>" +

        "<id>1</id>" +

        "<totalRecords>2</totalRecords>" +

        "<success>true</success>" +

        "<record>" +

            "<id>1</id>" +

            "<name>name1</name>" +

            "<descn>descn1</descn>" +

        "</record>" +

        "<record>" +

            "<id>2</id>" +

            "<name>name2</name>" +

            "<descn>descn2</descn>" +

        "</record>" +

    "</dataset>";

var xdoc;

if(typeof(DOMParser) == 'undefined'){

    xdoc= new ActiveXObject("Microsoft.XMLDOM");

    xdoc.async="false";

    xdoc.loadXML(data);

}else{

    var domParser = new DOMParser();

    xdoc= domParser.parseFromString(data, 'application/xml');

    domParser= null;

}

var proxy = new Ext.data.MemoryProxy(xdoc);

var reader = new Ext.data.XmlReader({

    totalRecords: 'totalRecords',

    success: 'success',

    record: 'record',

    id:"id"

}, ['id','name','descn']);

var ds = new Ext.data.Store({

    proxy:proxy,

    reader:reader

});

10.7 高级store

实际开发时,并不需要每次都对proxyreaderstore这三个对象进行配置,EXT为我们提供了几种可选择的整合方案。

q   SimpleStore = Store + MemoryProxy + ArrayReader

  var ds = Ext.data.SimpleStore({

      data:[

            ['id1','name1','descn1'],

            ['id2','name2','descn2']

      ],

      fields:['id','name','descn']

  });

SimpleStore是专为简化读取本地数组而设计的,设置上MemoryProxy需要的dataArrayReader需要的fields就可以使用了。

q   JsonStore = Store + HttpProxy + JsonReader

  var ds = Ext.data.JsonStore({

      url: 'xxx.jsp',

      root: 'root',

      fields:['id','name','descn']

  });

JsonStoreJsonReaderHttpProxy整合在一起,提供了一种从后台读取JSON信息的简便方法,大多数情况下可以考虑直接使用它从后台读取数据。

q   Ext.data.GroupingStore对数据进行分组

Ext.data.GroupingStore继承自Ext.data.Store,它的主要功能是可以对内部的数据进行分组,我们可以在创建Ext.data.GroupingStore时指定根据某个字段进行分组,也可以在创建实例后调用它的groupBy()函数对内部数据重新分组,如下面的代码所示。

    vards = new Ext.data.GroupingStore({

        data:[

            ['id1','name1','female','descn1'],

            ['id2','name2','male','descn2'],

            ['id3','name3','female','descn3'],

            ['id4','name4','male','descn4'],

            ['id5','name5','female','descn5']

        ],

        reader:new Ext.data.ArrayReader({

            fields:['id','name','sex','descn']

        }),

        groupField:'sex',

        groupOnSort:true

    });

上例中,我们使用groupField作为参数,为Ext.data.Grouping设置了分组字段,另外还设置了groupOnSort参数,这个参数可以保证只有在进行分组时才会对Ext.data.Grouping- Store内部的数据进行排序。如果采用默认值,就需要手工指定sortInfo参数,从而指定默认的排序字段和排序方式,否则就会出现错误。

创建Ext.data.GroupingStore的实例之后,我们还可以调用groupBy()函数重新对数据进行分组。因为我们设置了groupOnSort:true,所以在重新分组时,EXT会使用分组的字段对内部数据进行排序。如果不希望对数据进行分组,也可以调用clearGrouping()函数清除分组信息,如下面的代码所示。

    ds.groupBy('id');

    ds.clearGrouping();

10.8EXT中的Ajax

EXT与后台交换数据时,很大程度上依赖于底层实现的Ajax。所谓底层实现,就是说很可能就是我们之前提到的 PrototypejQueryYUI中提供的Ajax功能。为了统一接口,EXT在它们的基础上进行了封装,让我们可以用同一种写法游走于各种不同的底层实现之间。

10.8.1 最容易看到的Ext.Ajax

Ext.Ajax的基本用法如下所示。

Ext.Ajax.request({

    url:'07-01.txt',

    success:function(response) {

        Ext.Msg.alert('成功', response.responseText);

    },

    failure:function(response) {

        Ext.Msg.alert('失败', response.responseText);

    },

    params:{ name: 'value' }

});

这里调用的是Ext.Ajaxrequest函数,它的参数是一个JSON对象,具体如下所示。

q   url参数表示将要访问的后台网址。

q   success参数表示响应成功后的回调函数。

上例中我们直接从response取得返回的字符串,用Ext.Msg.alert显示出来。

q   failure参数表示响应失败后的回调函数。

注意,这里的响应失败并不是指数据库操作之类的业务性失败,而是指HTTP返回404500错误,请不要把HTTP响应错误与业务错误混淆在一起。

q   params参数表示请求时发送到后台的参数,既可以使用JSON对象,也可以直接使用"name=value"形式的字符串。

上面的示例可以在10.store/07-01.html中找到。

Ext.Ajax直接继承自Ext.data.Connection,不同的是,它是一个单例,不需要用new创建实例,可以直接使用。在使用Ext.data.Connection前需要先创建实例,因为Ext.data. Connection是为了给Ext.data中的各种proxy提供Ajax功能,分配不同的实例更有利于分别管理。Ext.Ajax为用户提供了一个简易的调用接口,实际使用时,可以根据自己的需要进行选择。

10.8.2Ext.lib.Ajax是更底层的封装

其实Ext.AjaxExt.data.Connection的内部功能实现都是依靠Ext.lib.Ajax来完成的,在Ext.lib.Ajax下面就是各种底层库的Ajax了。

如果使用Ext.lib.Ajax实现以上的功能,就需要写成下面的形式,如下面的代码所示。

Ext.lib.Ajax.request(

    'POST',

    '07-01txt',

    {success:function(response){

        Ext.Msg.alert('成功', response.responseText);

    },failure:function(){

        Ext.Msg.alert('失败', response.responseText);

    }},

    'data='+ encodeURIComponent(Ext.encode({name:'value'}))

);

我们可以看到,使用Ext.lib.Ajax时需要传递4个参数,分别为methodurlcallbackparams。它们的含义与Ext.Ajax中的参数都是一一对应的,唯一没有提到过的method参数表示请求HTTP的方法,它也可以在Ext.Ajax中使用method:'POST'的方式设置。

相对于Ext.Ajax来说,Ext.lib.Ajax有如下几个缺点。

q   参数的顺序被定死了,第一个参数是method,第二个参数是url,第三个参数是回调函数callback,第四个参数是params。这样既不容易记忆,也无法省略其中某个不需要的参数。Ext.Ajax中用JSON对象来定义参数,使用起来更灵活。

q   params部分,Ext.lib.Ajax必须使用字符串形式,显得有些笨重。Ext.Ajax则可以在JSON对象和字符串之间随意选择,非常灵活。

比与Ext.Ajax相比,Ext.lib.Ajax的唯一优势就是它可以在EXT 1.x中使用。如果你使用的是EXT 2.0或更高的版本,那么就放心大胆地使用Ext.Ajax吧,它会带给你更多的惊喜。

该示例在10.store/07-02.html中。

10.9 关于scopecreateDelegate()

关于JavaScriptthis的使用,这是一个由来已久的问题了。我们这里就不介绍它的发展历史了,只结合具体的例子,告诉大家可能会遇到什么问题,在遇到这些问题时EXT是如何解决的。在使用EXT时,最常碰到的就是使用Ajax回调函数时出现的问题,如下面的代码所示。

<input type="text" name="text" id="text">

<inputtype="button" name="button" id="button"value="button">

现在的HTML 页面中有一个text输入框和一个按钮。我们希望按下这个按钮之后,能用Ajax去后台读取数据,然后把后台响应的数据放到text中,实现过程如代码清单10-6所示。

代码清单10-6Ajax中使用回调函数

function doSuccess(response) {

    text.dom.value= response.responseText;

}

Ext.onReady(function(){

    Ext.get('button').on('click',function(){

        vartext = Ext.get('text');

        Ext.lib.Ajax.request(

            'POST',

            '08.txt',

            {success:doSuccess},

            'param='+ encodeURIComponent(text.dom.value)

        );

    });

});

在上面的代码中,Ajax已经用Ext.get('text')获得了text,以为后面可以直接使用,没想到回调函数success不会按照你写的顺序去执行。当然,也不会像你所想的那样使用局部变量text。实际上,如果什么都不做,仅仅只是使用回调函数,你不得不再次使用Ext.get('text')重新获得元素,否则浏览器就会报text未定义的错误。

在此使用Ext.get('text')重新获取对象还比较简单,在有些情况下不容易获得需要处理的对象,我们要在发送Ajax请求之前获取回调函数中需要操作的对象,有两种方法可供选择:scopecreateDelegate

q   Ajax设置scope

  function doSuccess(response) {

      this.dom.value = response.responseText;

  }

  Ext.lib.Ajax.request(

      'POST',

      '08.txt',

      {success:doSuccess,scope:text},

      'param=' + encodeURIComponent(text.dom.value)

  );              

Ajaxcallback参数部分添加一个scope:text,把回调函数的scope指向text,它的作用就是把doSuccess函数里的this指向text对象。然后再把doSuccess里改成this.dom. value,这样就可以了。如果想再次在回调函数里用某个对象,必须配上scope,这样就能在回调函数中使用this对它进行操作了。

q   success添加createDelegate()

  function doSuccess(response) {

      this.dom.value = response.responseText;

  }

  Ext.lib.Ajax.request(

      'POST',

      '08.txt',

      {success:doSuccess.createDelegate(text)},

      'param=' + encodeURIComponent(text.dom.value)

  );

createDelegate只能在function上调用,它把函数里的this强行指向我们需要的对象,然后我们就可以在回调函数doSuccess里直接通过this来引用createDelegate()中指定的这个对象了。它可以作为解决this问题的一个备选方案。

如果让我选择,我会尽量选择scope,因为createDelegate是要对原来的函数进行封装,重新生成function对象。简单环境下,scope就够用了,倒是createDelegate还有其他功能,比如修改调用参数等。

示例在10.store/08.html中。

10.10DWREXT整合

据不完全统计,从事Ajax开发的Java程序员有一大半都使用DWR。我们下面来介绍一下如何在EXT中使用DWR与后台交互。

10.10.1 在EXT中直接使用DWR

因为DWR在前台的表现形式和普通的JavaScript完全一样,所以我们不需要特地去做些什么,直接使用EXT调用DWR生成的JavaScript函数即可。以Grid为例,比如现在我们要显示一个通讯录的信息,后台记录的数据有:idnamesexemailteladdTimedescn。编写对应的POJO,代码如下所示。

public class Info {

    longid;

    Stringname;

    intsex;

    Stringemail;

    Stringtel;

    DateaddTime;

    Stringdescn;

}

然后编写操作POJOmanager类,代码如下所示。

public class InfoManager {

    privateList infoList = new ArrayList();

    publicList getResult() {

        returninfoList;

    }

}

代码部分有些删减,我们只保留了其中的关键部分,就这样把这两个类配置到dwr.xml中,让前台可以对这些类进行调用。

下面是EXTDWR交互的关键部分,我们要对JavaScript部分做如下修改,如代码清单10-7所示。

代码清单10-7 使用EXT调用DWR

var cm = new Ext.grid.ColumnModel([

    {header:'编号',dataIndex:'id'},

    {header:'名称',dataIndex:'name'},

    {header:'性别',dataIndex:'sex'},

    {header:'邮箱',dataIndex:'email'},

    {header:'电话',dataIndex:'tel'},

    {header:'添加时间',dataIndex:'addTime'},

    {header:'备注',dataIndex:'descn'}

]);

var store = newExt.data.JsonStore({

    fields:["id","name","sex",'email','tel','addTime','descn']

});

// 调用DWR取得数据

infoManager.getResult(function(data){

    store.loadData(data);

});

var grid = newExt.grid.GridPanel({

    renderTo:'grid',

    store:store,

    cm:cm

});

注意,执行infoManager.getResult()函数时,DWR就会使用Ajax去后台取数据了,操作成功后调用我们定义的匿名回调函数。在这里我们只做一件事,那就是将返回的data直接注入到ds中。

DWR返回的data可以被JsonStore直接读取,我们需要设置对应的fields参数,以告诉JsonReader需要哪些属性。

在这里,EXTDWR两者之间没有任何关系,将它们任何一方替换掉都可以。实际上它们只是在一起运行,并没有整合。我们给出的这个示例也是说明了一种松耦合的可能性,实际操作中完全可以使用这种方式。

10.10.2DWRProxy

要结合使用EXTDWR,不需要对后台程序进行任何修改,可以直接让前后台数据进行交互。不过还要考虑很多细节,比如Grid分页、刷新、排序、搜索等常见的操作。EXT的官方网站上已经有人放上了DWRProxy,借助它可以让DWREXT连接得更加紧密。不过,需要在后台添加DWRProxy所需要的Java类,这可能不是最好的解决方案。但我们相信,通过对它的内在实现的讨论,我们可以有更多的选择和想象空间。

注意     这个DWRProxy.js一定要放在ext-base.jsext-all.js后面,否则会出错。

我们现在就用DWRProxy来实现一个分页的示例。除了准备好插件DWRProxy.js外,还要在后台准备一个专门用于分页的封装类。因为不仅要告诉前台显示哪些数据,还要告诉前台一共有多少条数据。现在我们来重点看一下ListRange.java,如下面的代码所示。

public classListRange {

    Object[]data;

    inttotalSize;

}

其实ListRange非常简单,只有两个属性:提供数据的data和提供数据总量的totalSize。再看一下InfoManager.java,为了实现分页,我们专门编写了一个getItems方法,代码如下所示。

public ListRange getItems(Map conditions) {

    intstart = 0;

    intpageSize = 10;

    intpageNo = (start / pageSize) + 1;

    try{

        start= Integer.parseInt(conditions.get("start").toString());

        pageSize= Integer.parseInt(conditions.get("limit").toString());

        pageNo= (start / pageSize) + 1;

    }catch (Exception ex) {

        ex.printStackTrace();

    }

    Listlist = infoList.subList(start, start + pageSize);

    returnnew ListRange(list.toArray(), infoList.size());

}

getItems()的参数是Map,我们从中获得需要的参数,比如startlimit。不过HTTP里的参数都是字符串,而我们需要的是数字,所以要对类型进行相应的转换。根据startlimit两个属性从全部数据中截取一部分,放进新建的ListRange中,然后把生成的ListRange返回给前台,于是一切都解决了。

重头戏要上演了,我们就要使用传说中的Ext.data.DWRProxy了,还有Ext.data.List- RangeReader。通过这两个扩展,EXT完全可以支持DWR的数据传输协议。实际上,这正是EXT要把数据和显示分离设计的原因,这样你只需要添加自定义的proxyreader,不需要修改EXT的其他部分,就可以实现从特定途径获取数据的功能。后台还是DWR,所以至少在Grid部分,我们可以很好地使用它们的结合,主要代码如下所示。

var store = new Ext.data.Store({

    proxy: new Ext.data.DWRProxy(infoManager.getItems, true),

    reader:new Ext.data.ListRangeReader({

        totalProperty:'totalSize',

        root:'data',

        id:'id'

    },info),

    remoteSort:true

});

与我们上面说的一样,我们修改了proxy,也修改了reader,其他地方都不需要进行修改,Grid已经可以正常运行了。需要提醒的是DWRProxy的用法,其中包括两个参数:第一个是dwr- Call,它把一个DWR函数放进去,它对应的是后台的getItems方法;第二个参数是paging- AndSort,这个参数控制DWR是否需要分页和排序。

ListRangeReader部分与后台的ListRange.java对应。totalProperty表示后台数据总数,我们通过它指定从ListRange中读取totalSize属性的值来作为后台数据总数。还需要指定root参数,以告诉它在ListRange中的数据变量的名称为data,随后DWRProxy会从ListRange中的data属性中获取数据并显示到页面上。如果不想使用我们提供的ListRange.java,也可以自己创建一个类,只要把totalPropertydata两个属性与之对应即可。

10.10.3DWRTreeLoader

我们现在来尝试一下让树形也支持DWR。有了前面的基础,整合DWRtree就更简单了。在后台,我们需要树形节点对应的TreeNode.java。目前,只要idtextleaf三项就可以了。

public class TreeNode {

    Stringid;

    Stringtext;

    booleanleaf;

}

id是节点的唯一标记,知道了id就能知道是在触发哪个节点了。text是显示的标题,leaf比较重要,它用来标记这个节点是不是叶子。

这里还是用异步树,TreeNodeManager.java里的getTree()方法将获得一个节点的id作为参数,然后返回这个节点下的所有子节点。我们这里没有限制生成的树形的深度,你可以根据自己的需要进行设置。TreeNodeManager.java的代码如下所示。

public List getTree(String id) {

    Listlist = new ArrayList();

    Stringseed1 = id + 1;

    Stringseed2 = id + 2;

    Stringseed3 = id + 3;

    list.add(newTreeNode(seed1, "" + seed1, false));

    list.add(newTreeNode(seed2, "" + seed2, false));

    list.add(newTreeNode(seed3, "" + seed3, true));

    returnlist;

}

上面的代码并不复杂,它实现的效果与在Java中使用List或数组是相同的,因为返回给前台的数据都是JSON格式的。前台使用JavaScript处理返回信息的部分更简单,先引入DWRTree- Loader.js,然后把TreeLoader替换成DWRTreeLoder即可,如下面的代码所示。

var tree = new Ext.tree.TreePanel('tree', {

    loader:new Ext.tree.DWRTreeLoader({dataUrl: treeNodeManager.getTree})

});

参数依然是dataUrl,它的值treeNodeManager.getTree代表的是一个DWR函数,我们不需要对它进行深入研究,它的内部会自动处理数据之间的对应关系。DWR有时真的很方便。

10.10.4DWRProxyComboBox

DWRProxy既然可以用在Ext.data.Store中,那么它也可以为ComboBox服务,如代码清单10-8所示。

代码清单10-8DWRProxyComboBox整合

var info = Ext.data.Record.create([

    {name:'id', type: 'int'},

    {name:'name', type: 'string'}

]);

var store = newExt.data.Store({

    proxy:new Ext.data.DWRProxy(infoManager.getItems, true),

    reader:new Ext.data.ListRangeReader({

        totalProperty:'totalSize',

        root:'data',

        id:'id'

    },info)

});

var combo = newExt.form.ComboBox({

    store:store,

    displayField:'name',

    valueField:'id',

    triggerAction:'all',

    typeAhead:true,

    mode:'remote',

    emptyText:'请选择',

    selectOnFocus:true

});

combo.render('combo');

我们既可以用mode:'remote'triggerAction:'all'在第一次选择时读取数据,也可以设置mode:'local',然后手工操作store.load()并读取数据。

DWR要比Json-lib方便得多,而且DWR返回的数据可以直接作为JSON使用,使用Json-lib时还要面对无休无止的循环引用。

这次的示例稍微复杂一些,因为包括依赖jar包、classXMLJSP,所以示例单独放在10.store/dwr2/下,请将它们复制到tomcatwebapps下,然后再使用浏览器访问。

10.11localXHR支持本地使用Ajax

Ajax是不能在本地文件系统中使用的,必须把数据放到服务器上。无论是IISApache Tomcat,还是你熟悉的其他服务器,只要支持HTTP协议,就可以使用EXT中的Ajax

至于本地为何不能用Ajax,主要是因为Ajax要判断HTTP响应返回的状态,只有status=200时才认为这次请求是成功的。所以,localXHR做的就是强行修改响应状态,让Ajax可以继续下去。

下面我们来分析一下localXHR的源代码。

q   加入了一个forceActiveX属性,默认是false,它用来控制是否强制使用activexactivex是在IE下专用的。

q   修改createXhrObject函数,只是在最开始处加了一条判断语句,如下所示。

  if(Ext.isIE7 && !!this.forceActiveX){throw("IE7forceActiveX");}              

q   增加了getHttpStatus函数,这是为了处理HTTP的响应状态,如代码清单10-9所示。

代码清单10-9 处理HTTP响应状态

getHttpStatus: function(reqObj){

    var statObj = {

        status:0

        ,statusText:''

        ,isError:false

        ,isLocal:false

        ,isOK:false

    };

    try {

        if(!reqObj)throw('noobj');

        statObj.status= reqObj.status || 0;

        statObj.isLocal= !reqObj.status && location.protocol == "file:" ||

                           Ext.isSafari&& reqObj.status == undefined;

        statObj.statusText= reqObj.statusText || '';

        statObj.isOK= (statObj.isLocal ||

                        (statObj.status> 199 && statObj.status < 300) ||

                         statObj.status== 304);

    } catch(e){

        //status may not avail/valid yet.

        statObj.isError= true;

    }

    return statObj;

},

它为状态增添了更多语义,status表示状态值,statusText表示状态描述,isError表示是否有错误,isLocal表示是否在本地进行Ajax访问,isOK表示操作是否成功。

判断isLocal是否为本地的有两种方法:reqObj没有status,而且请求协议是file:;浏览器是Safari,而且reqObj.status没有定义。

statObj中的isOK属性用来判断此次请求是否成功。判断请求是否成功的条件很多,例如:isLocal的属性为true、响应状态值在199~300之间、响应状态值是304等。如果处理过程中出现了异常,就会将isError属性设置为true,最后会把配置好的statObj对象返回,等待下一个步骤的处理。

localXHR.jshandleTransactionResponse函数进行了简化。因为增加的getHttpStatus函数很好地封装了与请求相关的各种状态信息,所以在handleTransactionResponse函数中我们不会看到让人头晕目眩的响应状态代码。取而代之的是isErrorisOK这些更容易理解的属性,localXHR.js直接使用这些属性来处理响应。

createResponseObject函数被大大强化了。其实前半部分都是一样的,localXHR.js中对isLocal做了大量的处理,响应中的responseText可以从连接中获得。如果需要XML,它就使用ActiveXObject("Microsoft.XMLDOM")new DOMParser()responseText解析成XML放到response里,响应状态也是重新计算的,这样就能让Ajax正常调用了。

最后处理的是asyncRequest函数,如果在异步请求时出现异常,就调用handleTransac- tionResponse返回响应,然后根据各种情况稍微修改header属性。

我们来看看下面这行代码:

Ext.lib.Ajax.forceActiveX= (document.location.protocol == 'file:');              

如果协议是file:,就强制使用activex

10.12 本章小结

本章系统地讨论了Ext.data包中的各个类的功能和使用方式,还涉及如何将EXTDWR通过自定义的proxy相结合的示例。我们介绍了如何使用Ext.data.Connection与后台进行数据交互,还专门介绍了它的子类Ext.Ajax,并讨论了EXTAjax的应用以及在回调函数中使用scopecreateDelegate()解决this的问题。

接着详细介绍了类Ext.data.RecordExt.data.Store的功能和使用方法,这两个类结合起来形成了Ext.data中的主体数据模型,很多组件(包括GridComboBox)都是建立在它们之上的。除此之外,还讨论了常用的proxyreaderstoreSimpleStoreJsonStore,以及它们的应用场景。

最后我们介绍了扩展插件localXHR.js,它可以解决EXTAjax无法访问本地文件的问题。

本章内容

q   Ext.data简介

q   Ext.data.Connection

q   Ext.data.Record

q   Ext.data.Store

q   常用proxy

q   常用reader

q   高级store

q   EXT中的Ajax

q   关于scopecreateDelegate()

q   DWREXT整合

10.1Ext.data简介

Ext.data在命名空间中定义了一系列storereaderproxyGridComboxBox都是以Ext.data为媒介获取数据的,它包含异步加载、类型转换、分页等功能。Ext.data默认支持ArrayJSONXML等数据格式,可以通过MemoryHTTPScriptTag等方式获得这些格式的数据。如果要实现新的协议和新的数据结构,只需要扩展readerproxy即可。DWRProxy就实现了自身的proxyreader,让EXT可以直接从DWR获得数据。

10.2Ext.data.Connection

Ext.data.Connection是对Ext.lib.Ajax的封装,它提供了配置使用Ajax的通用方式,它在内部通过Ext.lib.Ajax实现与后台的异步调用。与底层的Ext.lib.Ajax相比,Ext.data. Connection提供了更简洁的配置方式,使用起来更方便。

Ext.data.Connection主要用于在Ext.data.HttpProxyExt.data.ScriptTagProxy中执行与后台交互的任务,它会从指定的URL获得数据,并把后台返回的数据交给HttpProxyScriptTagProxy处理,Ext.data.Connection的使用方式如代码清单10-1所示。

代码清单10-1 使用Ext.data.Connection

var conn = newExt.data.Connection({

    autoAbort:false,

    defaultHeaders:{

        referer:'http://localhost:8080/'

    },

    disableCaching: false,

    extraParams: {

        name:'name'

    },

    method: 'GET',

    timeout: 300,

    url: '01-01.txt'

});

在使用Ext.data.Connection之前,都要像上面这样创建一个新的Ext.Connection实例。我们可以在构造方法里配置对应的参数,比如autoAbort表示链接是否会自动断开、default- Headers参数表示请求的默认首部信息、disableCaching参数表示请求是否会禁用缓存、extraParams参数代表请求的额外参数、method参数表示请求方法、timeout参数表示连接的超时时间、url参数表示请求访问的网址等。

在创建了conn之后,可以调用request()函数发送请求,处理返回的结果,如下面的代码所示。

conn.request({

    success:function(response) {

        Ext.Msg.alert('info',response.responseText);

    },

    failure:function() {

        Ext.Msg.alert('warn','failure');

    }

});

Request()函数中可以设置successfailure两个回调函数,分别在请求成功和请求失败时调用。请求成功时,success函数的参数就是后台返回的信息。

我们再来看一下request函数中的其他参数。

q   url:String:请求url

q   params:Object/String/Function:请求传递的参数。

q   method:String:请求方法,通常为GETPOST

q   callback:Function:请求完成后的回调函数,无论是成功还是失败,都会执行。

q   success:Function:请求成功时的回调函数。

q   failure:Function:请求失败时的回调函数

q   scope:Object:回调函数的作用域。

q   form:Object/String:绑定的form表单。

q   isUpload:Boolean:是否执行文件上传。

q   headers:Object:请求首部信息。

q   xmlData:ObjectXML文档对象,可以通过URL附加参数的方式发起请求。

q   disableCaching:Boolean:是否禁用缓存,默认为禁用。

Ext.data.Connection还提供了abort([Number transactionId])函数,当同时有多个请求发生时,根据指定的事务id放弃其中的某一个请求。如果不指定事务id,就会放弃最后一个请求。isLoading([NumbertransactionId])函数的用法与abort()类似,可以根据事务id判断对应的请求是否完成。如果未指定事务id,就判断最后一个请求是否完成。

10.3Ext.data.Record

Ext.data.Record就是一个设定了内部数据类型的对象,它是Ext.data.Store的最基本组成部分。如果把Ext.data.Store看作是一张二维表,那么它的每一行就对应一个Ext.data. Record实例。

Ext.data.Record的主要功能是保存数据,并且在内部数据发生改变时记录修改的状态,它还可以保留修改之前的原始值。

我们使用Ext.data.Record时通常都是由create()函数开始,首先用create()函数创建一个自定义的Record类型,如下面的代码所示。

var PersonRecord= Ext.data.Record.create([

    {name:'name', type: 'string'},

    {name:'sex', type: 'int'}

]);

PersonRecord就是我们定义的新类型,包含字符串类型的name和整数类型的sex两个属性,然后我们使用new关键字创建PersonRecord的实例,如下面的代码所示。

var boy = newPersonRecord({

    name:'boy',

    sex:0

});

创建对象时,可以直接通过构造方法为对象赋予初始值,将'boy'赋值给name0赋值给sex

现在,我们得到了PersonRecord的实例boy,如何才能得到它的属性呢?以下三种方式都可以获得boyname属性的数据,如下面的代码所示。

alert(boy.data.name);

alert(boy.data['name']);

alert(boy.get('name'));

这里涉及Ext.data.Recorddata属性,这是定义在Ext.data.Record中的一个公共属性,用于保存当前record对象的所有数据。它是一个JSON对象,可以直接从它里面获得需要的数据。可以通过Ext.data.Recordget()函数方便地从data属性中获得指定的属性值。

如果我们需要修改boy中的数据,请不要使用以下方式直接操作data,如下面的代码所示。

    boy.data.name= 'boy name';

    boy.data['name']= 'boy name';

而应该使用set()函数,如下面的代码所示。

    boy.set('name','body name');

set()函数会判断属性值是否发生了改变,如果改变了,就要将当前对象的dirty属性设置为true,并将修改之前的原始值放入modified对象中,供其他函数使用。如果直接操作data中的值,record就无法记录属性数据的修改情况。

Record的属性数据被修改后,我们可以执行如下几种操作。

q   commit()(提交):这个函数的效果是设置dirtyfalse,并删除modified中保存的原始数据。

q   reject()(撤销):这个函数的效果是将data中已经修改了的属性值都恢复成modified中保存的原始数据,然后设置dirtyfalse,并删除保存原始数据的modified对象。

q   getChanges()获得修改的部分:这个函数会把data中经过修改的属性和数据放在一个JSON对象里并返回。例如上例中,getChanges()返回的结果是{name:’body name’}

q   我们还可以调用isModified()判断当前record中的数据是否被修改。

      Ext.data.Record还提供了用于复制record实例的函数copy()

  varcopyBoy = boy.copy();

这样我们就得到了boy的一个副本,它里面包含了boydata数据,但copy()函数不会复制dirtymodified等额外的属性值。

Ext.data.Record中其他的参数大多与Ext.data.Store有关,请参考与Ext.data.Store相关的讨论。

10.4Ext.data.Store

Ext.data.StoreEXT中用来进行数据交换和数据交互的标准中间件,无论是Grid还是ComboBox,都是通过它实现数据读取、类型转换、排序分页和搜索等操作的。

Ext.data.Store中有一个Ext.data.Record数组,所有数据都存放在这些Ext.data. Record实例中,为后面的读取和修改操作做准备。

10.4.1 基本应用

在使用之前,首先要创建一个Ext.data.Store的实例,如下面的代码所示。

var data = [

    ['boy',0],

    ['girl',1]

];

var store = newExt.data.Store({

    proxy:new Ext.data.MemoryProxy(data),

    reader:new Ext.data.ArrayReader({}, PersonRecord)

});

store.load();

每个store最少需要两个组件的支持,分别是proxyreaderproxy用于从某个途径读取原始数据,reader用于将原始数据转换成Record实例。

这里我们使用的是Ext.data.MemoryProxyExt.data.ArrayReader,将data数组中的数据转换成对应的几个PersonRecord实例,然后放入store中。store创建完毕之后,执行store.load()实现这个转换过程。

经过转换之后,store里的数据就可以提供给GridComboBox使用了,这就是Ext.data. Store的最基本用法。

10.4.2 对数据进行排序

Ext.data.Store提供了一系列属性和函数,利用它们对数据进行排序操作。

可以在创建Ext.data.Store时使用sortInfo参数指定排序的字段和排序方式,如下面的代码所示。

var store = newExt.data.Store({

    proxy:new Ext.data.MemoryProxy(data),

    reader:new Ext.data.ArrayReader({}, PersonRecord),

    sortInfo:{field: 'name', direction: 'DESC'}

});

这样,在store加载数据之后,就会自动根据name字段进行降序排列。对store使用store.setDefaultSort('name','DESC');也会达到同样效果。

也可以在任何时候调用sort()函数,比如store.sort('name','DESC');,对store中的数据进行排序。

如果我们希望获得store的排序信息,可以调用getSortState()函数,返回的是类似{field: "name", direction: " DESC"}JSON对象。

与排序相关的参数还有remoteSort,这个参数是用来实现后台排序功能的。当设置为remoteSort:true时,store会在向后台请求数据时自动加入sortdir两个参数,分别对应排序的字段和排序的方式,由后台获取并处理这两个参数,在后台对所需数据进行排序操作。remoteSort:true也会导致每次执行sort()时都要去后台重新加载数据,而不能只对本地数据进行排序。

详细的用法可以参考第2章。

10.4.3 从store中获取数据

store中获取数据有很多种途径,可以依据不同的要求选择不同的函数。最直接的方法是根据recordstore中的行号获得对应的record,得到了record就可以使用get()函数获得里面的数据了,如下面的代码所示。

store.getAt(0).get('name')

通过这种方式,我们可以遍历store中所有的record,依次得到它们的数据,如下面的代码所示。

for (var i = 0;i < store.getCount(); i++) {

    varrecord = store.getAt(i);

    alert(record.get('name'));

}

Store.getCount()返回的是store中的所有数据记录,然后使用for循环遍历整个store,从而得到每条记录。

除了使用getCount()的方法外,还可以使用each()函数,如下面的代码所示。

store.each(function(record){

    alert(record.get('name'));

});

Each()可以接受一个函数作为参数,遍历内部record,并将每个record作为参数传递给function()处理。如果希望停止遍历,可以让function()返回false

也可以使用getRange()函数连续获得多个record,只需要指定开始和结束位置的索引值,如下面的代码所示。

var records =store.getRange(0, 1);

for (var i = 0;i < records.length; i++) {

    varrecord = records[i];

    alert(record.get('name'));

}

如果确实不知道recordid,也可以根据record本身的idstore中获得对应的record,如下面的代码所示。

store.getById(1001).get('name')

EXT还提供了函数find()findBy(),可以利用它们对store中的数据进行搜索,如下面的代码所示。

find( Stringproperty, String/RegExp value, [Number startIndex], [Boolean anyMatch],

[BooleancaseSensitive] )

在这5个参数中,只有前两个是必须的。第一个参数property代表搜索的字段名;第二个参数value是匹配用字符串或正则表达式;第三个参数startIndex表示从第几行开始搜索,第四个参数anyMatchtrue时,不必从头开始匹配;第五个参数caseSensitivetrue时,会区分大小写。

如下面的代码所示:

var index =store.find('name','g');

alert(store.getAt(index).get('name'));

find()函数对应的findBy()函数的定义格式如下:

findBy( Functionfn, [Object scope], [Number startIndex] ) : Number

findBy()函数允许用户使用自定义函数对内部数据进行搜索。fn返回true时,表示查找成功,于是停止遍历并返回行号。fn返回false时,表示查找失败(即未找到),继续遍历,如下面的代码所示。

index =store.findBy(function(record, id) {

    returnrecord.get('name') == 'girl' && record.get('sex') == 1;

});

alert(store.getAt(index).get('name'));

通过findBy()函数,我们可以同时判断record中的多个字段,在函数中实现复杂逻辑。

我们还可以使用queryqueryBy函数对store中的数据进行查询。与findfindBy不同的是,queryqueryBy返回的是一个MixCollection对象,里面包含了搜索得到的数据,如下面的代码所示。

alert(store.query('name','boy'));

    alert(store.queryBy(function(record){

        returnrecord.get('name') == 'girl' && record.get('sex') == 1;

    }));

10.4.4 更新store中的数据

可以使用add(Ext.data.Record[] records)store末尾添加一个或多个record,使用的参数可以是一个record实例,如下面的代码所示。

store.add(newPersonRecord({

    name:'other',

    sex:0

}));

Add()的也可以添加一个record数组,如下面的代码所示:

store.add([newPersonRecord({

    name:'other1',

    sex:0

}), newPersonRecord({

    name:'other2',

    sex:0

})]);

Add()函数每次都会将新数据添加到store的末尾,这就有可能破坏store原有的排序方式。如果希望根据store原来的排序方式将新数据插入到对应的位置,可以使用addSorted()函数。它会在添加新数据之后立即对store进行排序,这样就可以保证store中的数据有序地显示,如下面的代码所示。

store.addSorted(newPersonRecord({

    name:'lili',

    sex:1

}));

store会根据排序信息查找这条record应该插入的索引位置,然后根据得到的索引位置插入数据,从而实现对整体进行排序。这个函数需要预先为store设置本地排序,否则会不起作用。

如果希望自己指定数据插入的索引位置,可以使用insert()函数。它的第一个参数表示插入数据的索引位置,可以使用record实例或record实例的数组作为参数,插入之后,后面的数据自动后移,如下面的代码所示。

store.insert(3,new PersonRecord({

    name:'other',

    sex:0

}));

store.insert(3,[new PersonRecord({

    name:'other1',

    sex:0

}), newPersonRecord({

    name:'other2',

    sex:0

})]);

删除操作可以使用remove()removeAll()函数,它们分别可以删除指定的record和清空整个store中的数据,如下面的代码所示。

store.remove(store.getAt(0));

store.removeAll();

store中没有专门提供修改某一行record的操作,我们需要先从store中获取一个record。对这个record内部数据的修改会直接反映到store上,如下面的代码所示。

store.getAt(0).set('name','xxxx');

修改record的内部数据之后有两种选择:执行rejectChanges()撤销所有修改,将修改过的record恢复到原来的状态;执行commitChanges()提交数据修改。在执行撤销和提交操作之前,可以使用getModifiedRecords()获得store中修改过的record数组。

与修改数据相关的参数是pruneModifiedRecords,如果将它设置为true,当每次执行删除或reload操作时,都会清空所有修改。这样,在每次执行删除或reload操作之后,getModifiedRecords()返回的就是一个空数组,否则仍然会得到上次修改过的record记录。

10.4.5 加载及显示数据

store创建好后,需要调用load()函数加载数据,加载成功后才能对store中的数据进行操作。load()调用的完整过程如下面的代码所示。

store.load({

    params:{start:0,limit:20},

    callback:function(records, options, success){

        Ext.Msg.alert('info','加载完毕');

    },

    scope:store,

    add:true

});

q   params是在store加载时发送的附加参数。

q   callback是加载完毕时执行的回调函数,它包含3个参数:records参数表示获得的数据,options表示执行load()时传递的参数,success表示是否加载成功。

q   Scope用来指定回调函数执行时的作用域。

q   Addtrue时,load()得到的数据会添加在原来的store数据的末尾,否则会先清除之前的数据,再将得到的数据添加到store中。

一般来说,为了对store中的数据进行初始化,load()函数只需要执行一次。如果用params参数指定了需要使用的参数,以后再次执行reload()重新加载数据时,store会自动使用上次load()中包含的params参数内容。

如果有一些需要固定传递的参数,也可以使用baseParams参数执行,它是一个JSON对象,里面的数据会作为参数发送给后台处理,如下面的代码所示。

store.baseParams.start= 0;

store.baseParams.limit= 20;

store加载数据之后,有时不需要把所有数据都显示出来,这时可以使用函数filterfilterBystore中的数据进行过滤,只显示符合条件的部分,如下面的代码所示。

filter( Stringfield, String/RegExp value, [Boolean anyMatch],
[Boolean caseSensitive] ) : void

filter()函数的用法与之前谈到的find()相似,如下面的代码所示。

store.filter('name','boy');

对应的filterBy()findBy()类似,也可以在自定义的函数中实现各种复杂判断,如下面的代码所示。

store.filterBy(function(record){

    returnrecord.get('name') == 'girl' && record.get('sex') == 1;

});

如果想取消过滤并显示所有数据,那么可以调用clearFilter()函数,如下面的代码所示。

store.clearFilter();

如果想知道store上是否设置了过滤器,可以通过isFiltered()函数进行判断。

10.4.6 其他功能

除了上面提到的数据获取、排序、更新、显示等功能外,store还提供了其他一些功能函数。

collect( StringdataIndex, [Boolean allowNull], [Boolean bypassFilter] ) : Array

collect函数获得指定的dataIndex对应的那一列的数据,当allowNull参数为true时,返回的结果中可能会包含nullundefined或空字符串,否则collect函数会自动将这些空数据过滤掉。当bypassFilter参数为true时,collect的结果不会受查询条件的影响,无论查询条件是什么都会忽略掉,返回的信息是所有的数据,如下面的代码所示。

alert(store.collect('name'));

这样会获得所有name列的值,示例中返回的是包含了'boy''girl'的数组。

getTotalCount()用于在翻页时获得后台传递过来的数据总数。如果没有设置翻页,get- TotalCount()的结果与getCount()相同,都是返回当前的数据总数,如下面的代码所示。

alert(store.getTotalCount());

indexOf(Ext.data.Record record)indexOfId(String id)函数根据recordrecordid获得record对应的行号,如下面的代码所示。

alert(store.indexOf(store.getAt(1)));

alert(store.indexOfId(1001));

loadData(object data, [Boolean append])从本地JavaScript变量中读取数据,appendtrue时,将读取的数据附加到原数据后,否则执行整体更新,如下面的代码所示。

store.loadData(data,true);

Sum(String property, Number start, Number end):Number用于计算某一个列从startend的总和,如下面的代码所示。

alert(store.sum('sex'));

如果省略参数startend,就计算全部数据的总和。

store还提供了一系列事件(见表10-1),让我们可以为对应操作设定操作函数。

10-1store提供的事件

事件名

参  数

add

( Store this, Ext.data.Record[] records, Number index )

beforelaod

( Store this, Object options )

clear

( Store this )

datachanged

( Store this )

load

( Store this, Ext.data.Record[] records, Object options )

loadexception

()

metachange

( Store this, Object meta. )

remove

( Store this, Ext.data.Record record, Number index )

update

( Store this, Ext.data.Record record, String operation )

至此,storerecord等组件已经讲解完毕,下面我们主要讨论一下常用的proxyreader组件。

10.5 常用proxy

10.5.1MemoryProxy

MemoryProxy只能从JavaScript对象获得数据,可以直接把数组,或JSONXML格式的数据交给它处理,如下面的代码所示。

var proxy = new Ext.data.MemoryProxy([

    ['id1','name1','descn1'],

    ['id2','name2','descn2']

]);              

10.5.2HttpProxy

HttpProxy使用HTTP协议,通过Ajax去后台取数据,构造它时需要设置url:'xxx.jsp'参数。这里的url可以替换成任何一个合法的网址,这样HttpProxy才知道去哪里获取数据,如下面的代码所示。

var proxy = new Ext.data.HttpProxy({url:'xxx.jsp'});              

后台需要返回EXT所需要的JSON格式的数据,下面的内容就是后台使用JSP的一个范例,如下面的代码所示。

response.setContentType("application/x-json");

Writer out =response.getWriter();

out.print("[" +

        "['id1','name1','descn1']" +

        "['id2','name2','descn2']" +

    "]");              

请注意,这里的HttpProxy不支持跨域,它只能从同一域中获得数据。如果想跨域,请参考下面的ScriptTagProxy

10.5.3ScriptTagProxy

ScriptTagProxy的用法几乎和HttpProxy一样,如下面的代码所示。

var proxy = new Ext.data.ScriptTagProxy({url:'xxx.jsp'});              

从这里也看不出来它是如何支持跨域的,我们还需要在后台进行相应的处理,如下面的代码所示。

String cb =request.getParameter("callback");

response.setContentType("text/javascript");

Writer out =response.getWriter();

out.write(cb+ "(");

out.print("[" +

        "['id1','name1','descn1']" +

        "['id2','name2','descn2']" +

    "]");

out.write(");");

其中的关键就在于从请求中获得的callback参数,这个参数叫做回调函数。ScriptTag- Proxy会在当前的HTML页面里添加一个<scripttype="text/javascript"src="xxx.jsp"> </script>标签,然后把后台返回的内容添加到这个标签中,这样就可以解决跨域访问数据的问题。为了让后台返回的内容可以在动态生成的标签中运行,EXT会生成一个名为callback的回调函数,并把回调函数的名称传递给后台,由后台生成callback(data)形式的响应内容,然后返回给前台自动运行。

虽然上述处理过程比较难理解,但是我们只需要了解ScriptTagProxy的用法就足够了。如果还想进一步了解ScriptTagProxy的运行过程,可以使用Firebug查看动态生成的HTML以及响应的JSON内容。

最后我们来分析一下EXTAPI文档中提供的示例,这段后台代码会自动判断请求的类型,返回支持ScriptTagProxyHttpProxy的数据,如代码清单10-2所示。

代码清单10-2 在后台同时支持HttpProxyScriptTagProxy

boolean scriptTag = false;

String cb =request.getParameter("callback");

if (cb != null) {

    scriptTag= true;

    response.setContentType("text/javascript");

} else {

    response.setContentType("application/x-json");

}

Writer out =response.getWriter();

if (scriptTag) {

    out.write(cb+ "(");

}

out.print(dataBlock.toJsonString());

if (scriptTag) {

    out.write(");");

}              

代码中通过判断请求中是否包含callback参数来决定返回何种数据类型。如果包含,就返回ScriptTagProxy需要的数据;否则,就当作HttpProxy处理。

10.6 常用Reader

10.6.1ArrayReader

proxy中读取的数据需要进行解析,这些数据转换成Record数组后才能提供给Ext.data. Store使用。

ArrayReader的作用是从二维数组里依次读取数据,然后生成对应的Record。默认情况下是按列顺序读取数组中的数据,不过你也可以考虑用mapping指定record与原始数组对应的列号。ArrayReader的用法很简单,但缺点是不支持分页。使用二维数组的方式如下面的代码所示。

var data = [

    ['id1','name1','descn1'],

    ['id2','name2','descn2']

];

对应的ArrayReader如下面的代码所示。

var reader = new Ext.data.ArrayReader({

    id:1

},[

    {name:'name',mapping:1},

    {name:'descn',mapping:2},

    {name:'id',mapping:0},

]);

我们演示的是字段顺序不一致的情况,如果字段顺序和列顺序一致,就不用额外配置mapping

10.6.2JsonReader

JavaScript中,JSON是一种非常重要的数据格式,key:value的形式比XML那种复杂的标签结构更容易理解,代码量也更小,很多人倾向于使用它作为EXT的数据交换格式。为Json- Reader准备的JSON数据如下面的代码所示。

var data = {

    id:0,

    totalProperty:2,

    successProperty:true,

    root:[

        {id:'id1',name:'name1',descn:'descn1'},

        {id:'id2',name:'name2',descn:'descn2'}

    ]

};

与数组相比,JSON的最大优点就是支持分页,我们可以使用totalProperty参数表示数据的总量。successProperty参数是可选的,可以用它判断当前请求是否执行成功,进而判断是否进行数据加载。在不希望JsonReader处理响应数据时,可以把successProperty设置成false

现在来讨论一下JsonReader,看看它是如何与上面的JSON数据对应的,如下面的代码所示。

var reader = new Ext.data.JsonReader({

    successProperty: "successproperty",

    totalProperty: "totalProperty",

    root: "root",

    id: "id"

}, [

    {name:'id',mapping:'id'},

    {name:'name',mapping:'name'},

    {name:'descn',mapping:'descn'}

]);

上例中的对应方式不够简洁,因为namemapping部分的内容是相同的,其实这里的mapping可以省略,默认会用name参数从JSON中获得对应的数据。如果不想与JSON里的名字一样,也可以使用mapping修改。不过,mapping在这里还有其他用途,如代码清单10-3所示。

代码清单10-3 为JsonReader设置mapping进行数据映射

var data = {

    id:0,

    totalProperty:2,

    successProperty:true,

    root:[

        {id:'id1',name:'name1',descn:'descn1',person:{

            id:1,name:'man',sex:'male'

        }},

        {id:'id2',name:'name2',descn:'descn2',person:{

            id:2,name:'woman',sex:'female'

        }}

    ]

};

var reader = new Ext.data.JsonReader({

    successProperty: "successproperty",

    totalProperty: "totalProperty",

    root: "root",

    id: "id"

}, [

    'id','name','descn',

    {name:'person_name',mapping:'person.name'},

    {name:'person_sex',mapping:'person.sex'}

]);

在上面的代码中,我们使用JSON支持更复杂的嵌套结构,其中的person对象自身就拥有id namesex等属性。在JsonReader中可以用mapping把这些嵌套的内部属性映射出来,赋予对应的record,而其他字段都不变。

10.6.3XmlReader

XML是非常通用的数据传输格式,XmlReader使用的XML格式的数据如代码清单10-4所示。

代码清单10-4XmlReader使用的XML格式的数据

<?xml version="1.0" encoding="utf-8"?>

<dataset>

    <id>1</id>

    <totalRecords>2</totalRecords>

    <success>true</success>

    <record>

        <id>1</id>

        <name>name1</name>

        <descn>descn1</descn>

    </record>

    <record>

        <id>2</id>

        <name>name2</name>

        <descn>descn2</descn>

    </record>

</dataset>

这里一定要用dataset作为XML根元素。再让我们看一下如何对XmlReader进行配置,从而读取上面示例中的XML数据,如下面的代码所示。

var reader = new Ext.data.XmlReader({

   totalRecords: 'totalRecords',

   success: 'success'

   record: 'record',

   id: "id"

}, ['id','name','descn']);              

XmlReader使用的参数与之前介绍的JsonReader有些不同,我们可以看到这里用到了totalRecordsrecord两个参数,其中totalRecords用来指定从’totalRecords’标签里获得后台数据总数,record则表示XML中放在record标签里的数据是我们需要显示的结果数据。其他两个参数successid的含义和JsonReader中对应的参数相似,分别用来判断操是否成功和这次返回的id。因为XML中的标签和reader里需要的名字是相同的,所以简化了配置,将[{name:’id’},{name:’name’},{name:’descn’}]直接写成了[‘id’,’name’,’descn’]

因为XmlReader不能将JavaScript中的字符串自动解析成XML格式的数据,因此我们需要利用其他方法进行演示。参考localXHR.js中构造XML的方式,我们有了下面的解决方案,如代码清单10-5所示。

代码清单10-5 通过本地字符串构造XML对象

var data = "<?xmlversion='1.0' encoding='utf-8'?>" +

    "<dataset>" +

        "<id>1</id>" +

        "<totalRecords>2</totalRecords>" +

        "<success>true</success>" +

        "<record>" +

            "<id>1</id>" +

            "<name>name1</name>" +

            "<descn>descn1</descn>" +

        "</record>" +

        "<record>" +

            "<id>2</id>" +

            "<name>name2</name>" +

            "<descn>descn2</descn>" +

        "</record>" +

    "</dataset>";

var xdoc;

if(typeof(DOMParser) == 'undefined'){

    xdoc= new ActiveXObject("Microsoft.XMLDOM");

    xdoc.async="false";

    xdoc.loadXML(data);

}else{

    var domParser = new DOMParser();

    xdoc= domParser.parseFromString(data, 'application/xml');

    domParser= null;

}

var proxy = new Ext.data.MemoryProxy(xdoc);

var reader = new Ext.data.XmlReader({

    totalRecords: 'totalRecords',

    success: 'success',

    record: 'record',

    id:"id"

}, ['id','name','descn']);

var ds = new Ext.data.Store({

    proxy:proxy,

    reader:reader

});

10.7 高级store

实际开发时,并不需要每次都对proxyreaderstore这三个对象进行配置,EXT为我们提供了几种可选择的整合方案。

q   SimpleStore = Store + MemoryProxy + ArrayReader

  var ds = Ext.data.SimpleStore({

      data:[

            ['id1','name1','descn1'],

            ['id2','name2','descn2']

      ],

      fields:['id','name','descn']

  });

SimpleStore是专为简化读取本地数组而设计的,设置上MemoryProxy需要的dataArrayReader需要的fields就可以使用了。

q   JsonStore = Store + HttpProxy + JsonReader

  var ds = Ext.data.JsonStore({

      url: 'xxx.jsp',

      root: 'root',

      fields:['id','name','descn']

  });

JsonStoreJsonReaderHttpProxy整合在一起,提供了一种从后台读取JSON信息的简便方法,大多数情况下可以考虑直接使用它从后台读取数据。

q   Ext.data.GroupingStore对数据进行分组

Ext.data.GroupingStore继承自Ext.data.Store,它的主要功能是可以对内部的数据进行分组,我们可以在创建Ext.data.GroupingStore时指定根据某个字段进行分组,也可以在创建实例后调用它的groupBy()函数对内部数据重新分组,如下面的代码所示。

    vards = new Ext.data.GroupingStore({

        data:[

            ['id1','name1','female','descn1'],

            ['id2','name2','male','descn2'],

            ['id3','name3','female','descn3'],

            ['id4','name4','male','descn4'],

            ['id5','name5','female','descn5']

        ],

        reader:new Ext.data.ArrayReader({

            fields:['id','name','sex','descn']

        }),

        groupField:'sex',

        groupOnSort:true

    });

上例中,我们使用groupField作为参数,为Ext.data.Grouping设置了分组字段,另外还设置了groupOnSort参数,这个参数可以保证只有在进行分组时才会对Ext.data.Grouping- Store内部的数据进行排序。如果采用默认值,就需要手工指定sortInfo参数,从而指定默认的排序字段和排序方式,否则就会出现错误。

创建Ext.data.GroupingStore的实例之后,我们还可以调用groupBy()函数重新对数据进行分组。因为我们设置了groupOnSort:true,所以在重新分组时,EXT会使用分组的字段对内部数据进行排序。如果不希望对数据进行分组,也可以调用clearGrouping()函数清除分组信息,如下面的代码所示。

    ds.groupBy('id');

    ds.clearGrouping();

10.8EXT中的Ajax

EXT与后台交换数据时,很大程度上依赖于底层实现的Ajax。所谓底层实现,就是说很可能就是我们之前提到的 PrototypejQueryYUI中提供的Ajax功能。为了统一接口,EXT在它们的基础上进行了封装,让我们可以用同一种写法游走于各种不同的底层实现之间。

10.8.1 最容易看到的Ext.Ajax

Ext.Ajax的基本用法如下所示。

Ext.Ajax.request({

    url:'07-01.txt',

    success:function(response) {

        Ext.Msg.alert('成功', response.responseText);

    },

    failure:function(response) {

        Ext.Msg.alert('失败', response.responseText);

    },

    params:{ name: 'value' }

});

这里调用的是Ext.Ajaxrequest函数,它的参数是一个JSON对象,具体如下所示。

q   url参数表示将要访问的后台网址。

q   success参数表示响应成功后的回调函数。

上例中我们直接从response取得返回的字符串,用Ext.Msg.alert显示出来。

q   failure参数表示响应失败后的回调函数。

注意,这里的响应失败并不是指数据库操作之类的业务性失败,而是指HTTP返回404500错误,请不要把HTTP响应错误与业务错误混淆在一起。

q   params参数表示请求时发送到后台的参数,既可以使用JSON对象,也可以直接使用"name=value"形式的字符串。

上面的示例可以在10.store/07-01.html中找到。

Ext.Ajax直接继承自Ext.data.Connection,不同的是,它是一个单例,不需要用new创建实例,可以直接使用。在使用Ext.data.Connection前需要先创建实例,因为Ext.data. Connection是为了给Ext.data中的各种proxy提供Ajax功能,分配不同的实例更有利于分别管理。Ext.Ajax为用户提供了一个简易的调用接口,实际使用时,可以根据自己的需要进行选择。

10.8.2Ext.lib.Ajax是更底层的封装

其实Ext.AjaxExt.data.Connection的内部功能实现都是依靠Ext.lib.Ajax来完成的,在Ext.lib.Ajax下面就是各种底层库的Ajax了。

如果使用Ext.lib.Ajax实现以上的功能,就需要写成下面的形式,如下面的代码所示。

Ext.lib.Ajax.request(

    'POST',

    '07-01txt',

    {success:function(response){

        Ext.Msg.alert('成功', response.responseText);

    },failure:function(){

        Ext.Msg.alert('失败', response.responseText);

    }},

    'data='+ encodeURIComponent(Ext.encode({name:'value'}))

);

我们可以看到,使用Ext.lib.Ajax时需要传递4个参数,分别为methodurlcallbackparams。它们的含义与Ext.Ajax中的参数都是一一对应的,唯一没有提到过的method参数表示请求HTTP的方法,它也可以在Ext.Ajax中使用method:'POST'的方式设置。

相对于Ext.Ajax来说,Ext.lib.Ajax有如下几个缺点。

q   参数的顺序被定死了,第一个参数是method,第二个参数是url,第三个参数是回调函数callback,第四个参数是params。这样既不容易记忆,也无法省略其中某个不需要的参数。Ext.Ajax中用JSON对象来定义参数,使用起来更灵活。

q   params部分,Ext.lib.Ajax必须使用字符串形式,显得有些笨重。Ext.Ajax则可以在JSON对象和字符串之间随意选择,非常灵活。

比与Ext.Ajax相比,Ext.lib.Ajax的唯一优势就是它可以在EXT 1.x中使用。如果你使用的是EXT 2.0或更高的版本,那么就放心大胆地使用Ext.Ajax吧,它会带给你更多的惊喜。

该示例在10.store/07-02.html中。

10.9 关于scopecreateDelegate()

关于JavaScriptthis的使用,这是一个由来已久的问题了。我们这里就不介绍它的发展历史了,只结合具体的例子,告诉大家可能会遇到什么问题,在遇到这些问题时EXT是如何解决的。在使用EXT时,最常碰到的就是使用Ajax回调函数时出现的问题,如下面的代码所示。

<input type="text" name="text" id="text">

<inputtype="button" name="button" id="button"value="button">

现在的HTML 页面中有一个text输入框和一个按钮。我们希望按下这个按钮之后,能用Ajax去后台读取数据,然后把后台响应的数据放到text中,实现过程如代码清单10-6所示。

代码清单10-6Ajax中使用回调函数

function doSuccess(response) {

    text.dom.value= response.responseText;

}

Ext.onReady(function(){

    Ext.get('button').on('click',function(){

        vartext = Ext.get('text');

        Ext.lib.Ajax.request(

            'POST',

            '08.txt',

            {success:doSuccess},

            'param='+ encodeURIComponent(text.dom.value)

        );

    });

});

在上面的代码中,Ajax已经用Ext.get('text')获得了text,以为后面可以直接使用,没想到回调函数success不会按照你写的顺序去执行。当然,也不会像你所想的那样使用局部变量text。实际上,如果什么都不做,仅仅只是使用回调函数,你不得不再次使用Ext.get('text')重新获得元素,否则浏览器就会报text未定义的错误。

在此使用Ext.get('text')重新获取对象还比较简单,在有些情况下不容易获得需要处理的对象,我们要在发送Ajax请求之前获取回调函数中需要操作的对象,有两种方法可供选择:scopecreateDelegate

q   Ajax设置scope

  function doSuccess(response) {

      this.dom.value = response.responseText;

  }

  Ext.lib.Ajax.request(

      'POST',

      '08.txt',

      {success:doSuccess,scope:text},

      'param=' + encodeURIComponent(text.dom.value)

  );              

Ajaxcallback参数部分添加一个scope:text,把回调函数的scope指向text,它的作用就是把doSuccess函数里的this指向text对象。然后再把doSuccess里改成this.dom. value,这样就可以了。如果想再次在回调函数里用某个对象,必须配上scope,这样就能在回调函数中使用this对它进行操作了。

q   success添加createDelegate()

  function doSuccess(response) {

      this.dom.value = response.responseText;

  }

  Ext.lib.Ajax.request(

      'POST',

      '08.txt',

      {success:doSuccess.createDelegate(text)},

      'param=' + encodeURIComponent(text.dom.value)

  );

createDelegate只能在function上调用,它把函数里的this强行指向我们需要的对象,然后我们就可以在回调函数doSuccess里直接通过this来引用createDelegate()中指定的这个对象了。它可以作为解决this问题的一个备选方案。

如果让我选择,我会尽量选择scope,因为createDelegate是要对原来的函数进行封装,重新生成function对象。简单环境下,scope就够用了,倒是createDelegate还有其他功能,比如修改调用参数等。

示例在10.store/08.html中。

10.10DWREXT整合

据不完全统计,从事Ajax开发的Java程序员有一大半都使用DWR。我们下面来介绍一下如何在EXT中使用DWR与后台交互。

10.10.1 在EXT中直接使用DWR

因为DWR在前台的表现形式和普通的JavaScript完全一样,所以我们不需要特地去做些什么,直接使用EXT调用DWR生成的JavaScript函数即可。以Grid为例,比如现在我们要显示一个通讯录的信息,后台记录的数据有:idnamesexemailteladdTimedescn。编写对应的POJO,代码如下所示。

public class Info {

    longid;

    Stringname;

    intsex;

    Stringemail;

    Stringtel;

    DateaddTime;

    Stringdescn;

}

然后编写操作POJOmanager类,代码如下所示。

public class InfoManager {

    privateList infoList = new ArrayList();

    publicList getResult() {

        returninfoList;

    }

}

代码部分有些删减,我们只保留了其中的关键部分,就这样把这两个类配置到dwr.xml中,让前台可以对这些类进行调用。

下面是EXTDWR交互的关键部分,我们要对JavaScript部分做如下修改,如代码清单10-7所示。

代码清单10-7 使用EXT调用DWR

var cm = new Ext.grid.ColumnModel([

    {header:'编号',dataIndex:'id'},

    {header:'名称',dataIndex:'name'},

    {header:'性别',dataIndex:'sex'},

    {header:'邮箱',dataIndex:'email'},

    {header:'电话',dataIndex:'tel'},

    {header:'添加时间',dataIndex:'addTime'},

    {header:'备注',dataIndex:'descn'}

]);

var store = newExt.data.JsonStore({

    fields:["id","name","sex",'email','tel','addTime','descn']

});

// 调用DWR取得数据

infoManager.getResult(function(data){

    store.loadData(data);

});

var grid = newExt.grid.GridPanel({

    renderTo:'grid',

    store:store,

    cm:cm

});

注意,执行infoManager.getResult()函数时,DWR就会使用Ajax去后台取数据了,操作成功后调用我们定义的匿名回调函数。在这里我们只做一件事,那就是将返回的data直接注入到ds中。

DWR返回的data可以被JsonStore直接读取,我们需要设置对应的fields参数,以告诉JsonReader需要哪些属性。

在这里,EXTDWR两者之间没有任何关系,将它们任何一方替换掉都可以。实际上它们只是在一起运行,并没有整合。我们给出的这个示例也是说明了一种松耦合的可能性,实际操作中完全可以使用这种方式。

10.10.2DWRProxy

要结合使用EXTDWR,不需要对后台程序进行任何修改,可以直接让前后台数据进行交互。不过还要考虑很多细节,比如Grid分页、刷新、排序、搜索等常见的操作。EXT的官方网站上已经有人放上了DWRProxy,借助它可以让DWREXT连接得更加紧密。不过,需要在后台添加DWRProxy所需要的Java类,这可能不是最好的解决方案。但我们相信,通过对它的内在实现的讨论,我们可以有更多的选择和想象空间。

注意     这个DWRProxy.js一定要放在ext-base.jsext-all.js后面,否则会出错。

我们现在就用DWRProxy来实现一个分页的示例。除了准备好插件DWRProxy.js外,还要在后台准备一个专门用于分页的封装类。因为不仅要告诉前台显示哪些数据,还要告诉前台一共有多少条数据。现在我们来重点看一下ListRange.java,如下面的代码所示。

public classListRange {

    Object[]data;

    inttotalSize;

}

其实ListRange非常简单,只有两个属性:提供数据的data和提供数据总量的totalSize。再看一下InfoManager.java,为了实现分页,我们专门编写了一个getItems方法,代码如下所示。

public ListRange getItems(Map conditions) {

    intstart = 0;

    intpageSize = 10;

    intpageNo = (start / pageSize) + 1;

    try{

        start= Integer.parseInt(conditions.get("start").toString());

        pageSize= Integer.parseInt(conditions.get("limit").toString());

        pageNo= (start / pageSize) + 1;

    }catch (Exception ex) {

        ex.printStackTrace();

    }

    Listlist = infoList.subList(start, start + pageSize);

    returnnew ListRange(list.toArray(), infoList.size());

}

getItems()的参数是Map,我们从中获得需要的参数,比如startlimit。不过HTTP里的参数都是字符串,而我们需要的是数字,所以要对类型进行相应的转换。根据startlimit两个属性从全部数据中截取一部分,放进新建的ListRange中,然后把生成的ListRange返回给前台,于是一切都解决了。

重头戏要上演了,我们就要使用传说中的Ext.data.DWRProxy了,还有Ext.data.List- RangeReader。通过这两个扩展,EXT完全可以支持DWR的数据传输协议。实际上,这正是EXT要把数据和显示分离设计的原因,这样你只需要添加自定义的proxyreader,不需要修改EXT的其他部分,就可以实现从特定途径获取数据的功能。后台还是DWR,所以至少在Grid部分,我们可以很好地使用它们的结合,主要代码如下所示。

var store = new Ext.data.Store({

    proxy: new Ext.data.DWRProxy(infoManager.getItems, true),

    reader:new Ext.data.ListRangeReader({

        totalProperty:'totalSize',

        root:'data',

        id:'id'

    },info),

    remoteSort:true

});

与我们上面说的一样,我们修改了proxy,也修改了reader,其他地方都不需要进行修改,Grid已经可以正常运行了。需要提醒的是DWRProxy的用法,其中包括两个参数:第一个是dwr- Call,它把一个DWR函数放进去,它对应的是后台的getItems方法;第二个参数是paging- AndSort,这个参数控制DWR是否需要分页和排序。

ListRangeReader部分与后台的ListRange.java对应。totalProperty表示后台数据总数,我们通过它指定从ListRange中读取totalSize属性的值来作为后台数据总数。还需要指定root参数,以告诉它在ListRange中的数据变量的名称为data,随后DWRProxy会从ListRange中的data属性中获取数据并显示到页面上。如果不想使用我们提供的ListRange.java,也可以自己创建一个类,只要把totalPropertydata两个属性与之对应即可。

10.10.3DWRTreeLoader

我们现在来尝试一下让树形也支持DWR。有了前面的基础,整合DWRtree就更简单了。在后台,我们需要树形节点对应的TreeNode.java。目前,只要idtextleaf三项就可以了。

public class TreeNode {

    Stringid;

    Stringtext;

    booleanleaf;

}

id是节点的唯一标记,知道了id就能知道是在触发哪个节点了。text是显示的标题,leaf比较重要,它用来标记这个节点是不是叶子。

这里还是用异步树,TreeNodeManager.java里的getTree()方法将获得一个节点的id作为参数,然后返回这个节点下的所有子节点。我们这里没有限制生成的树形的深度,你可以根据自己的需要进行设置。TreeNodeManager.java的代码如下所示。

public List getTree(String id) {

    Listlist = new ArrayList();

    Stringseed1 = id + 1;

    Stringseed2 = id + 2;

    Stringseed3 = id + 3;

    list.add(newTreeNode(seed1, "" + seed1, false));

    list.add(newTreeNode(seed2, "" + seed2, false));

    list.add(newTreeNode(seed3, "" + seed3, true));

    returnlist;

}

上面的代码并不复杂,它实现的效果与在Java中使用List或数组是相同的,因为返回给前台的数据都是JSON格式的。前台使用JavaScript处理返回信息的部分更简单,先引入DWRTree- Loader.js,然后把TreeLoader替换成DWRTreeLoder即可,如下面的代码所示。

var tree = new Ext.tree.TreePanel('tree', {

    loader:new Ext.tree.DWRTreeLoader({dataUrl: treeNodeManager.getTree})

});

参数依然是dataUrl,它的值treeNodeManager.getTree代表的是一个DWR函数,我们不需要对它进行深入研究,它的内部会自动处理数据之间的对应关系。DWR有时真的很方便。

10.10.4DWRProxyComboBox

DWRProxy既然可以用在Ext.data.Store中,那么它也可以为ComboBox服务,如代码清单10-8所示。

代码清单10-8DWRProxyComboBox整合

var info = Ext.data.Record.create([

    {name:'id', type: 'int'},

    {name:'name', type: 'string'}

]);

var store = newExt.data.Store({

    proxy:new Ext.data.DWRProxy(infoManager.getItems, true),

    reader:new Ext.data.ListRangeReader({

        totalProperty:'totalSize',

        root:'data',

        id:'id'

    },info)

});

var combo = newExt.form.ComboBox({

    store:store,

    displayField:'name',

    valueField:'id',

    triggerAction:'all',

    typeAhead:true,

    mode:'remote',

    emptyText:'请选择',

    selectOnFocus:true

});

combo.render('combo');

我们既可以用mode:'remote'triggerAction:'all'在第一次选择时读取数据,也可以设置mode:'local',然后手工操作store.load()并读取数据。

DWR要比Json-lib方便得多,而且DWR返回的数据可以直接作为JSON使用,使用Json-lib时还要面对无休无止的循环引用。

这次的示例稍微复杂一些,因为包括依赖jar包、classXMLJSP,所以示例单独放在10.store/dwr2/下,请将它们复制到tomcatwebapps下,然后再使用浏览器访问。

10.11localXHR支持本地使用Ajax

Ajax是不能在本地文件系统中使用的,必须把数据放到服务器上。无论是IISApache Tomcat,还是你熟悉的其他服务器,只要支持HTTP协议,就可以使用EXT中的Ajax

至于本地为何不能用Ajax,主要是因为Ajax要判断HTTP响应返回的状态,只有status=200时才认为这次请求是成功的。所以,localXHR做的就是强行修改响应状态,让Ajax可以继续下去。

下面我们来分析一下localXHR的源代码。

q   加入了一个forceActiveX属性,默认是false,它用来控制是否强制使用activexactivex是在IE下专用的。

q   修改createXhrObject函数,只是在最开始处加了一条判断语句,如下所示。

  if(Ext.isIE7 && !!this.forceActiveX){throw("IE7forceActiveX");}              

q   增加了getHttpStatus函数,这是为了处理HTTP的响应状态,如代码清单10-9所示。

代码清单10-9 处理HTTP响应状态

getHttpStatus: function(reqObj){

    var statObj = {

        status:0

        ,statusText:''

        ,isError:false

        ,isLocal:false

        ,isOK:false

    };

    try {

        if(!reqObj)throw('noobj');

        statObj.status= reqObj.status || 0;

        statObj.isLocal= !reqObj.status && location.protocol == "file:" ||

                           Ext.isSafari&& reqObj.status == undefined;

        statObj.statusText= reqObj.statusText || '';

        statObj.isOK= (statObj.isLocal ||

                        (statObj.status> 199 && statObj.status < 300) ||

                         statObj.status== 304);

    } catch(e){

        //status may not avail/valid yet.

        statObj.isError= true;

    }

    return statObj;

},

它为状态增添了更多语义,status表示状态值,statusText表示状态描述,isError表示是否有错误,isLocal表示是否在本地进行Ajax访问,isOK表示操作是否成功。

判断isLocal是否为本地的有两种方法:reqObj没有status,而且请求协议是file:;浏览器是Safari,而且reqObj.status没有定义。

statObj中的isOK属性用来判断此次请求是否成功。判断请求是否成功的条件很多,例如:isLocal的属性为true、响应状态值在199~300之间、响应状态值是304等。如果处理过程中出现了异常,就会将isError属性设置为true,最后会把配置好的statObj对象返回,等待下一个步骤的处理。

localXHR.jshandleTransactionResponse函数进行了简化。因为增加的getHttpStatus函数很好地封装了与请求相关的各种状态信息,所以在handleTransactionResponse函数中我们不会看到让人头晕目眩的响应状态代码。取而代之的是isErrorisOK这些更容易理解的属性,localXHR.js直接使用这些属性来处理响应。

createResponseObject函数被大大强化了。其实前半部分都是一样的,localXHR.js中对isLocal做了大量的处理,响应中的responseText可以从连接中获得。如果需要XML,它就使用ActiveXObject("Microsoft.XMLDOM")new DOMParser()responseText解析成XML放到response里,响应状态也是重新计算的,这样就能让Ajax正常调用了。

最后处理的是asyncRequest函数,如果在异步请求时出现异常,就调用handleTransac- tionResponse返回响应,然后根据各种情况稍微修改header属性。

我们来看看下面这行代码:

Ext.lib.Ajax.forceActiveX= (document.location.protocol == 'file:');              

如果协议是file:,就强制使用activex

10.12 本章小结

本章系统地讨论了Ext.data包中的各个类的功能和使用方式,还涉及如何将EXTDWR通过自定义的proxy相结合的示例。我们介绍了如何使用Ext.data.Connection与后台进行数据交互,还专门介绍了它的子类Ext.Ajax,并讨论了EXTAjax的应用以及在回调函数中使用scopecreateDelegate()解决this的问题。

接着详细介绍了类Ext.data.RecordExt.data.Store的功能和使用方法,这两个类结合起来形成了Ext.data中的主体数据模型,很多组件(包括GridComboBox)都是建立在它们之上的。除此之外,还讨论了常用的proxyreaderstoreSimpleStoreJsonStore,以及它们的应用场景。

最后我们介绍了扩展插件localXHR.js,它可以解决EXTAjax无法访问本地文件的问题。

q   Ext.data.Connection

q   Ext.data.Record

q   Ext.data.Store

q   常用proxy

q   常用reader

q   高级store

q   EXT中的Ajax

q   关于scopecreateDelegate()

q   DWREXT整合

10.1Ext.data简介

Ext.data在命名空间中定义了一系列storereaderproxyGridComboxBox都是以Ext.data为媒介获取数据的,它包含异步加载、类型转换、分页等功能。Ext.data默认支持ArrayJSONXML等数据格式,可以通过MemoryHTTPScriptTag等方式获得这些格式的数据。如果要实现新的协议和新的数据结构,只需要扩展readerproxy即可。DWRProxy就实现了自身的proxyreader,让EXT可以直接从DWR获得数据。

10.2Ext.data.Connection

Ext.data.Connection是对Ext.lib.Ajax的封装,它提供了配置使用Ajax的通用方式,它在内部通过Ext.lib.Ajax实现与后台的异步调用。与底层的Ext.lib.Ajax相比,Ext.data. Connection提供了更简洁的配置方式,使用起来更方便。

Ext.data.Connection主要用于在Ext.data.HttpProxyExt.data.ScriptTagProxy中执行与后台交互的任务,它会从指定的URL获得数据,并把后台返回的数据交给HttpProxyScriptTagProxy处理,Ext.data.Connection的使用方式如代码清单10-1所示。

代码清单10-1 使用Ext.data.Connection

var conn = newExt.data.Connection({

    autoAbort:false,

    defaultHeaders:{

        referer:'http://localhost:8080/'

    },

    disableCaching: false,

    extraParams: {

        name:'name'

    },

    method: 'GET',

    timeout: 300,

    url: '01-01.txt'

});

在使用Ext.data.Connection之前,都要像上面这样创建一个新的Ext.Connection实例。我们可以在构造方法里配置对应的参数,比如autoAbort表示链接是否会自动断开、default- Headers参数表示请求的默认首部信息、disableCaching参数表示请求是否会禁用缓存、extraParams参数代表请求的额外参数、method参数表示请求方法、timeout参数表示连接的超时时间、url参数表示请求访问的网址等。

在创建了conn之后,可以调用request()函数发送请求,处理返回的结果,如下面的代码所示。

conn.request({

    success:function(response) {

        Ext.Msg.alert('info',response.responseText);

    },

    failure:function() {

        Ext.Msg.alert('warn','failure');

    }

});

Request()函数中可以设置successfailure两个回调函数,分别在请求成功和请求失败时调用。请求成功时,success函数的参数就是后台返回的信息。

我们再来看一下request函数中的其他参数。

q   url:String:请求url

q   params:Object/String/Function:请求传递的参数。

q   method:String:请求方法,通常为GETPOST

q   callback:Function:请求完成后的回调函数,无论是成功还是失败,都会执行。

q   success:Function:请求成功时的回调函数。

q   failure:Function:请求失败时的回调函数

q   scope:Object:回调函数的作用域。

q   form:Object/String:绑定的form表单。

q   isUpload:Boolean:是否执行文件上传。

q   headers:Object:请求首部信息。

q   xmlData:ObjectXML文档对象,可以通过URL附加参数的方式发起请求。

q   disableCaching:Boolean:是否禁用缓存,默认为禁用。

Ext.data.Connection还提供了abort([Number transactionId])函数,当同时有多个请求发生时,根据指定的事务id放弃其中的某一个请求。如果不指定事务id,就会放弃最后一个请求。isLoading([NumbertransactionId])函数的用法与abort()类似,可以根据事务id判断对应的请求是否完成。如果未指定事务id,就判断最后一个请求是否完成。

10.3Ext.data.Record

Ext.data.Record就是一个设定了内部数据类型的对象,它是Ext.data.Store的最基本组成部分。如果把Ext.data.Store看作是一张二维表,那么它的每一行就对应一个Ext.data. Record实例。

Ext.data.Record的主要功能是保存数据,并且在内部数据发生改变时记录修改的状态,它还可以保留修改之前的原始值。

我们使用Ext.data.Record时通常都是由create()函数开始,首先用create()函数创建一个自定义的Record类型,如下面的代码所示。

var PersonRecord= Ext.data.Record.create([

    {name:'name', type: 'string'},

    {name:'sex', type: 'int'}

]);

PersonRecord就是我们定义的新类型,包含字符串类型的name和整数类型的sex两个属性,然后我们使用new关键字创建PersonRecord的实例,如下面的代码所示。

var boy = new PersonRecord({

    name:'boy',

    sex:0

});

创建对象时,可以直接通过构造方法为对象赋予初始值,将'boy'赋值给name0赋值给sex

现在,我们得到了PersonRecord的实例boy,如何才能得到它的属性呢?以下三种方式都可以获得boyname属性的数据,如下面的代码所示。

alert(boy.data.name);

alert(boy.data['name']);

alert(boy.get('name'));

这里涉及Ext.data.Recorddata属性,这是定义在Ext.data.Record中的一个公共属性,用于保存当前record对象的所有数据。它是一个JSON对象,可以直接从它里面获得需要的数据。可以通过Ext.data.Recordget()函数方便地从data属性中获得指定的属性值。

如果我们需要修改boy中的数据,请不要使用以下方式直接操作data,如下面的代码所示。

    boy.data.name= 'boy name';

    boy.data['name']= 'boy name';

而应该使用set()函数,如下面的代码所示。

    boy.set('name','body name');

set()函数会判断属性值是否发生了改变,如果改变了,就要将当前对象的dirty属性设置为true,并将修改之前的原始值放入modified对象中,供其他函数使用。如果直接操作data中的值,record就无法记录属性数据的修改情况。

Record的属性数据被修改后,我们可以执行如下几种操作。

q   commit()(提交):这个函数的效果是设置dirtyfalse,并删除modified中保存的原始数据。

q   reject()(撤销):这个函数的效果是将data中已经修改了的属性值都恢复成modified中保存的原始数据,然后设置dirtyfalse,并删除保存原始数据的modified对象。

q   getChanges()获得修改的部分:这个函数会把data中经过修改的属性和数据放在一个JSON对象里并返回。例如上例中,getChanges()返回的结果是{name:’body name’}

q   我们还可以调用isModified()判断当前record中的数据是否被修改。

      Ext.data.Record还提供了用于复制record实例的函数copy()

  varcopyBoy = boy.copy();

这样我们就得到了boy的一个副本,它里面包含了boydata数据,但copy()函数不会复制dirtymodified等额外的属性值。

Ext.data.Record中其他的参数大多与Ext.data.Store有关,请参考与Ext.data.Store相关的讨论。

10.4Ext.data.Store

Ext.data.StoreEXT中用来进行数据交换和数据交互的标准中间件,无论是Grid还是ComboBox,都是通过它实现数据读取、类型转换、排序分页和搜索等操作的。

Ext.data.Store中有一个Ext.data.Record数组,所有数据都存放在这些Ext.data. Record实例中,为后面的读取和修改操作做准备。

10.4.1 基本应用

在使用之前,首先要创建一个Ext.data.Store的实例,如下面的代码所示。

var data = [

    ['boy',0],

    ['girl',1]

];

var store = newExt.data.Store({

    proxy:new Ext.data.MemoryProxy(data),

    reader:new Ext.data.ArrayReader({}, PersonRecord)

});

store.load();

每个store最少需要两个组件的支持,分别是proxyreaderproxy用于从某个途径读取原始数据,reader用于将原始数据转换成Record实例。

这里我们使用的是Ext.data.MemoryProxyExt.data.ArrayReader,将data数组中的数据转换成对应的几个PersonRecord实例,然后放入store中。store创建完毕之后,执行store.load()实现这个转换过程。

经过转换之后,store里的数据就可以提供给GridComboBox使用了,这就是Ext.data. Store的最基本用法。

10.4.2 对数据进行排序

Ext.data.Store提供了一系列属性和函数,利用它们对数据进行排序操作。

可以在创建Ext.data.Store时使用sortInfo参数指定排序的字段和排序方式,如下面的代码所示。

var store = newExt.data.Store({

    proxy:new Ext.data.MemoryProxy(data),

    reader:new Ext.data.ArrayReader({}, PersonRecord),

    sortInfo:{field: 'name', direction: 'DESC'}

});

这样,在store加载数据之后,就会自动根据name字段进行降序排列。对store使用store.setDefaultSort('name','DESC');也会达到同样效果。

也可以在任何时候调用sort()函数,比如store.sort('name','DESC');,对store中的数据进行排序。

如果我们希望获得store的排序信息,可以调用getSortState()函数,返回的是类似{field: "name", direction: " DESC"}JSON对象。

与排序相关的参数还有remoteSort,这个参数是用来实现后台排序功能的。当设置为remoteSort:true时,store会在向后台请求数据时自动加入sortdir两个参数,分别对应排序的字段和排序的方式,由后台获取并处理这两个参数,在后台对所需数据进行排序操作。remoteSort:true也会导致每次执行sort()时都要去后台重新加载数据,而不能只对本地数据进行排序。

详细的用法可以参考第2章。

10.4.3 从store中获取数据

store中获取数据有很多种途径,可以依据不同的要求选择不同的函数。最直接的方法是根据recordstore中的行号获得对应的record,得到了record就可以使用get()函数获得里面的数据了,如下面的代码所示。

store.getAt(0).get('name')

通过这种方式,我们可以遍历store中所有的record,依次得到它们的数据,如下面的代码所示。

for (var i = 0;i < store.getCount(); i++) {

    varrecord = store.getAt(i);

    alert(record.get('name'));

}

Store.getCount()返回的是store中的所有数据记录,然后使用for循环遍历整个store,从而得到每条记录。

除了使用getCount()的方法外,还可以使用each()函数,如下面的代码所示。

store.each(function(record){

    alert(record.get('name'));

});

Each()可以接受一个函数作为参数,遍历内部record,并将每个record作为参数传递给function()处理。如果希望停止遍历,可以让function()返回false

也可以使用getRange()函数连续获得多个record,只需要指定开始和结束位置的索引值,如下面的代码所示。

var records =store.getRange(0, 1);

for (var i = 0;i < records.length; i++) {

    varrecord = records[i];

    alert(record.get('name'));

}

如果确实不知道recordid,也可以根据record本身的idstore中获得对应的record,如下面的代码所示。

store.getById(1001).get('name')

EXT还提供了函数find()findBy(),可以利用它们对store中的数据进行搜索,如下面的代码所示。

find( Stringproperty, String/RegExp value, [Number startIndex], [Boolean anyMatch],

[BooleancaseSensitive] )

在这5个参数中,只有前两个是必须的。第一个参数property代表搜索的字段名;第二个参数value是匹配用字符串或正则表达式;第三个参数startIndex表示从第几行开始搜索,第四个参数anyMatchtrue时,不必从头开始匹配;第五个参数caseSensitivetrue时,会区分大小写。

如下面的代码所示:

var index =store.find('name','g');

alert(store.getAt(index).get('name'));

find()函数对应的findBy()函数的定义格式如下:

findBy( Functionfn, [Object scope], [Number startIndex] ) : Number

findBy()函数允许用户使用自定义函数对内部数据进行搜索。fn返回true时,表示查找成功,于是停止遍历并返回行号。fn返回false时,表示查找失败(即未找到),继续遍历,如下面的代码所示。

index =store.findBy(function(record, id) {

    returnrecord.get('name') == 'girl' && record.get('sex') == 1;

});

alert(store.getAt(index).get('name'));

通过findBy()函数,我们可以同时判断record中的多个字段,在函数中实现复杂逻辑。

我们还可以使用queryqueryBy函数对store中的数据进行查询。与findfindBy不同的是,queryqueryBy返回的是一个MixCollection对象,里面包含了搜索得到的数据,如下面的代码所示。

alert(store.query('name','boy'));

    alert(store.queryBy(function(record){

        returnrecord.get('name') == 'girl' && record.get('sex') == 1;

    }));

10.4.4 更新store中的数据

可以使用add(Ext.data.Record[] records)store末尾添加一个或多个record,使用的参数可以是一个record实例,如下面的代码所示。

store.add(newPersonRecord({

    name:'other',

    sex:0

}));

Add()的也可以添加一个record数组,如下面的代码所示:

store.add([newPersonRecord({

    name:'other1',

    sex:0

}), newPersonRecord({

    name:'other2',

    sex:0

})]);

Add()函数每次都会将新数据添加到store的末尾,这就有可能破坏store原有的排序方式。如果希望根据store原来的排序方式将新数据插入到对应的位置,可以使用addSorted()函数。它会在添加新数据之后立即对store进行排序,这样就可以保证store中的数据有序地显示,如下面的代码所示。

store.addSorted(newPersonRecord({

    name:'lili',

    sex:1

}));

store会根据排序信息查找这条record应该插入的索引位置,然后根据得到的索引位置插入数据,从而实现对整体进行排序。这个函数需要预先为store设置本地排序,否则会不起作用。

如果希望自己指定数据插入的索引位置,可以使用insert()函数。它的第一个参数表示插入数据的索引位置,可以使用record实例或record实例的数组作为参数,插入之后,后面的数据自动后移,如下面的代码所示。

store.insert(3,new PersonRecord({

    name:'other',

    sex:0

}));

store.insert(3,[new PersonRecord({

    name:'other1',

    sex:0

}), new PersonRecord({

    name:'other2',

    sex:0

})]);

删除操作可以使用remove()removeAll()函数,它们分别可以删除指定的record和清空整个store中的数据,如下面的代码所示。

store.remove(store.getAt(0));

store.removeAll();

store中没有专门提供修改某一行record的操作,我们需要先从store中获取一个record。对这个record内部数据的修改会直接反映到store上,如下面的代码所示。

store.getAt(0).set('name','xxxx');

修改record的内部数据之后有两种选择:执行rejectChanges()撤销所有修改,将修改过的record恢复到原来的状态;执行commitChanges()提交数据修改。在执行撤销和提交操作之前,可以使用getModifiedRecords()获得store中修改过的record数组。

与修改数据相关的参数是pruneModifiedRecords,如果将它设置为true,当每次执行删除或reload操作时,都会清空所有修改。这样,在每次执行删除或reload操作之后,getModifiedRecords()返回的就是一个空数组,否则仍然会得到上次修改过的record记录。

10.4.5 加载及显示数据

store创建好后,需要调用load()函数加载数据,加载成功后才能对store中的数据进行操作。load()调用的完整过程如下面的代码所示。

store.load({

    params:{start:0,limit:20},

    callback:function(records, options, success){

        Ext.Msg.alert('info','加载完毕');

    },

    scope:store,

    add:true

});

q   params是在store加载时发送的附加参数。

q   callback是加载完毕时执行的回调函数,它包含3个参数:records参数表示获得的数据,options表示执行load()时传递的参数,success表示是否加载成功。

q   Scope用来指定回调函数执行时的作用域。

q   Addtrue时,load()得到的数据会添加在原来的store数据的末尾,否则会先清除之前的数据,再将得到的数据添加到store中。

一般来说,为了对store中的数据进行初始化,load()函数只需要执行一次。如果用params参数指定了需要使用的参数,以后再次执行reload()重新加载数据时,store会自动使用上次load()中包含的params参数内容。

如果有一些需要固定传递的参数,也可以使用baseParams参数执行,它是一个JSON对象,里面的数据会作为参数发送给后台处理,如下面的代码所示。

store.baseParams.start= 0;

store.baseParams.limit= 20;

store加载数据之后,有时不需要把所有数据都显示出来,这时可以使用函数filterfilterBystore中的数据进行过滤,只显示符合条件的部分,如下面的代码所示。

filter( Stringfield, String/RegExp value, [Boolean anyMatch],
[Boolean caseSensitive] ) : void

filter()函数的用法与之前谈到的find()相似,如下面的代码所示。

store.filter('name','boy');

对应的filterBy()findBy()类似,也可以在自定义的函数中实现各种复杂判断,如下面的代码所示。

store.filterBy(function(record){

    returnrecord.get('name') == 'girl' && record.get('sex') == 1;

});

如果想取消过滤并显示所有数据,那么可以调用clearFilter()函数,如下面的代码所示。

store.clearFilter();

如果想知道store上是否设置了过滤器,可以通过isFiltered()函数进行判断。

10.4.6 其他功能

除了上面提到的数据获取、排序、更新、显示等功能外,store还提供了其他一些功能函数。

collect( StringdataIndex, [Boolean allowNull], [Boolean bypassFilter] ) : Array

collect函数获得指定的dataIndex对应的那一列的数据,当allowNull参数为true时,返回的结果中可能会包含nullundefined或空字符串,否则collect函数会自动将这些空数据过滤掉。当bypassFilter参数为true时,collect的结果不会受查询条件的影响,无论查询条件是什么都会忽略掉,返回的信息是所有的数据,如下面的代码所示。

alert(store.collect('name'));

这样会获得所有name列的值,示例中返回的是包含了'boy''girl'的数组。

getTotalCount()用于在翻页时获得后台传递过来的数据总数。如果没有设置翻页,get- TotalCount()的结果与getCount()相同,都是返回当前的数据总数,如下面的代码所示。

alert(store.getTotalCount());

indexOf(Ext.data.Record record)indexOfId(String id)函数根据recordrecordid获得record对应的行号,如下面的代码所示。

alert(store.indexOf(store.getAt(1)));

alert(store.indexOfId(1001));

loadData(object data, [Boolean append])从本地JavaScript变量中读取数据,appendtrue时,将读取的数据附加到原数据后,否则执行整体更新,如下面的代码所示。

store.loadData(data,true);

Sum(String property, Number start, Number end):Number用于计算某一个列从startend的总和,如下面的代码所示。

alert(store.sum('sex'));

如果省略参数startend,就计算全部数据的总和。

store还提供了一系列事件(见表10-1),让我们可以为对应操作设定操作函数。

10-1store提供的事件

事件名

参  数

add

( Store this, Ext.data.Record[] records, Number index )

beforelaod

( Store this, Object options )

clear

( Store this )

datachanged

( Store this )

load

( Store this, Ext.data.Record[] records, Object options )

loadexception

()

metachange

( Store this, Object meta. )

remove

( Store this, Ext.data.Record record, Number index )

update

( Store this, Ext.data.Record record, String operation )

至此,storerecord等组件已经讲解完毕,下面我们主要讨论一下常用的proxyreader组件。

10.5 常用proxy

10.5.1MemoryProxy

MemoryProxy只能从JavaScript对象获得数据,可以直接把数组,或JSONXML格式的数据交给它处理,如下面的代码所示。

var proxy = new Ext.data.MemoryProxy([

    ['id1','name1','descn1'],

    ['id2','name2','descn2']

]);              

10.5.2HttpProxy

HttpProxy使用HTTP协议,通过Ajax去后台取数据,构造它时需要设置url:'xxx.jsp'参数。这里的url可以替换成任何一个合法的网址,这样HttpProxy才知道去哪里获取数据,如下面的代码所示。

var proxy = new Ext.data.HttpProxy({url:'xxx.jsp'});              

后台需要返回EXT所需要的JSON格式的数据,下面的内容就是后台使用JSP的一个范例,如下面的代码所示。

response.setContentType("application/x-json");

Writer out =response.getWriter();

out.print("[" +

        "['id1','name1','descn1']" +

        "['id2','name2','descn2']" +

    "]");              

请注意,这里的HttpProxy不支持跨域,它只能从同一域中获得数据。如果想跨域,请参考下面的ScriptTagProxy

10.5.3ScriptTagProxy

ScriptTagProxy的用法几乎和HttpProxy一样,如下面的代码所示。

var proxy = new Ext.data.ScriptTagProxy({url:'xxx.jsp'});              

从这里也看不出来它是如何支持跨域的,我们还需要在后台进行相应的处理,如下面的代码所示。

String cb =request.getParameter("callback");

response.setContentType("text/javascript");

Writer out =response.getWriter();

out.write(cb+ "(");

out.print("[" +

        "['id1','name1','descn1']" +

        "['id2','name2','descn2']" +

    "]");

out.write(");");

其中的关键就在于从请求中获得的callback参数,这个参数叫做回调函数。ScriptTag- Proxy会在当前的HTML页面里添加一个<scripttype="text/javascript"src="xxx.jsp"> </script>标签,然后把后台返回的内容添加到这个标签中,这样就可以解决跨域访问数据的问题。为了让后台返回的内容可以在动态生成的标签中运行,EXT会生成一个名为callback的回调函数,并把回调函数的名称传递给后台,由后台生成callback(data)形式的响应内容,然后返回给前台自动运行。

虽然上述处理过程比较难理解,但是我们只需要了解ScriptTagProxy的用法就足够了。如果还想进一步了解ScriptTagProxy的运行过程,可以使用Firebug查看动态生成的HTML以及响应的JSON内容。

最后我们来分析一下EXTAPI文档中提供的示例,这段后台代码会自动判断请求的类型,返回支持ScriptTagProxyHttpProxy的数据,如代码清单10-2所示。

代码清单10-2 在后台同时支持HttpProxyScriptTagProxy

boolean scriptTag = false;

String cb =request.getParameter("callback");

if (cb != null) {

    scriptTag= true;

    response.setContentType("text/javascript");

} else {

    response.setContentType("application/x-json");

}

Writer out =response.getWriter();

if (scriptTag) {

    out.write(cb+ "(");

}

out.print(dataBlock.toJsonString());

if (scriptTag) {

    out.write(");");

}              

代码中通过判断请求中是否包含callback参数来决定返回何种数据类型。如果包含,就返回ScriptTagProxy需要的数据;否则,就当作HttpProxy处理。

10.6 常用Reader

10.6.1ArrayReader

proxy中读取的数据需要进行解析,这些数据转换成Record数组后才能提供给Ext.data. Store使用。

ArrayReader的作用是从二维数组里依次读取数据,然后生成对应的Record。默认情况下是按列顺序读取数组中的数据,不过你也可以考虑用mapping指定record与原始数组对应的列号。ArrayReader的用法很简单,但缺点是不支持分页。使用二维数组的方式如下面的代码所示。

var data = [

    ['id1','name1','descn1'],

    ['id2','name2','descn2']

];

对应的ArrayReader如下面的代码所示。

var reader = new Ext.data.ArrayReader({

    id:1

},[

    {name:'name',mapping:1},

    {name:'descn',mapping:2},

    {name:'id',mapping:0},

]);

我们演示的是字段顺序不一致的情况,如果字段顺序和列顺序一致,就不用额外配置mapping

10.6.2JsonReader

JavaScript中,JSON是一种非常重要的数据格式,key:value的形式比XML那种复杂的标签结构更容易理解,代码量也更小,很多人倾向于使用它作为EXT的数据交换格式。为Json- Reader准备的JSON数据如下面的代码所示。

var data = {

    id:0,

    totalProperty:2,

    successProperty:true,

    root:[

        {id:'id1',name:'name1',descn:'descn1'},

        {id:'id2',name:'name2',descn:'descn2'}

    ]

};

与数组相比,JSON的最大优点就是支持分页,我们可以使用totalProperty参数表示数据的总量。successProperty参数是可选的,可以用它判断当前请求是否执行成功,进而判断是否进行数据加载。在不希望JsonReader处理响应数据时,可以把successProperty设置成false

现在来讨论一下JsonReader,看看它是如何与上面的JSON数据对应的,如下面的代码所示。

var reader = new Ext.data.JsonReader({

    successProperty: "successproperty",

    totalProperty: "totalProperty",

    root: "root",

    id: "id"

}, [

    {name:'id',mapping:'id'},

    {name:'name',mapping:'name'},

    {name:'descn',mapping:'descn'}

]);

上例中的对应方式不够简洁,因为namemapping部分的内容是相同的,其实这里的mapping可以省略,默认会用name参数从JSON中获得对应的数据。如果不想与JSON里的名字一样,也可以使用mapping修改。不过,mapping在这里还有其他用途,如代码清单10-3所示。

代码清单10-3 为JsonReader设置mapping进行数据映射

var data = {

    id:0,

    totalProperty:2,

    successProperty:true,

    root:[

        {id:'id1',name:'name1',descn:'descn1',person:{

            id:1,name:'man',sex:'male'

        }},

        {id:'id2',name:'name2',descn:'descn2',person:{

            id:2,name:'woman',sex:'female'

        }}

    ]

};

var reader = new Ext.data.JsonReader({

    successProperty: "successproperty",

    totalProperty: "totalProperty",

    root: "root",

    id: "id"

}, [

    'id','name','descn',

    {name:'person_name',mapping:'person.name'},

    {name:'person_sex',mapping:'person.sex'}

]);

在上面的代码中,我们使用JSON支持更复杂的嵌套结构,其中的person对象自身就拥有id namesex等属性。在JsonReader中可以用mapping把这些嵌套的内部属性映射出来,赋予对应的record,而其他字段都不变。

10.6.3XmlReader

XML是非常通用的数据传输格式,XmlReader使用的XML格式的数据如代码清单10-4所示。

代码清单10-4XmlReader使用的XML格式的数据

<?xml version="1.0" encoding="utf-8"?>

<dataset>

    <id>1</id>

    <totalRecords>2</totalRecords>

    <success>true</success>

    <record>

        <id>1</id>

        <name>name1</name>

        <descn>descn1</descn>

    </record>

    <record>

        <id>2</id>

        <name>name2</name>

        <descn>descn2</descn>

    </record>

</dataset>

这里一定要用dataset作为XML根元素。再让我们看一下如何对XmlReader进行配置,从而读取上面示例中的XML数据,如下面的代码所示。

var reader = new Ext.data.XmlReader({

   totalRecords: 'totalRecords',

   success: 'success'

   record: 'record',

   id: "id"

}, ['id','name','descn']);              

XmlReader使用的参数与之前介绍的JsonReader有些不同,我们可以看到这里用到了totalRecordsrecord两个参数,其中totalRecords用来指定从’totalRecords’标签里获得后台数据总数,record则表示XML中放在record标签里的数据是我们需要显示的结果数据。其他两个参数successid的含义和JsonReader中对应的参数相似,分别用来判断操是否成功和这次返回的id。因为XML中的标签和reader里需要的名字是相同的,所以简化了配置,将[{name:’id’},{name:’name’},{name:’descn’}]直接写成了[‘id’,’name’,’descn’]

因为XmlReader不能将JavaScript中的字符串自动解析成XML格式的数据,因此我们需要利用其他方法进行演示。参考localXHR.js中构造XML的方式,我们有了下面的解决方案,如代码清单10-5所示。

代码清单10-5 通过本地字符串构造XML对象

var data = "<?xmlversion='1.0' encoding='utf-8'?>" +

    "<dataset>" +

        "<id>1</id>" +

        "<totalRecords>2</totalRecords>" +

        "<success>true</success>" +

        "<record>" +

            "<id>1</id>" +

            "<name>name1</name>" +

            "<descn>descn1</descn>" +

        "</record>" +

        "<record>" +

            "<id>2</id>" +

            "<name>name2</name>" +

            "<descn>descn2</descn>" +

        "</record>" +

    "</dataset>";

var xdoc;

if(typeof(DOMParser) == 'undefined'){

    xdoc= new ActiveXObject("Microsoft.XMLDOM");

    xdoc.async="false";

    xdoc.loadXML(data);

}else{

    var domParser = new DOMParser();

    xdoc= domParser.parseFromString(data, 'application/xml');

    domParser= null;

}

var proxy = new Ext.data.MemoryProxy(xdoc);

var reader = new Ext.data.XmlReader({

    totalRecords: 'totalRecords',

    success: 'success',

    record: 'record',

    id:"id"

}, ['id','name','descn']);

var ds = new Ext.data.Store({

    proxy:proxy,

    reader:reader

});

10.7 高级store

实际开发时,并不需要每次都对proxyreaderstore这三个对象进行配置,EXT为我们提供了几种可选择的整合方案。

q   SimpleStore = Store + MemoryProxy + ArrayReader

  var ds = Ext.data.SimpleStore({

      data:[

            ['id1','name1','descn1'],

            ['id2','name2','descn2']

      ],

      fields:['id','name','descn']

  });

SimpleStore是专为简化读取本地数组而设计的,设置上MemoryProxy需要的dataArrayReader需要的fields就可以使用了。

q   JsonStore = Store + HttpProxy + JsonReader

  var ds = Ext.data.JsonStore({

      url: 'xxx.jsp',

      root: 'root',

      fields:['id','name','descn']

  });

JsonStoreJsonReaderHttpProxy整合在一起,提供了一种从后台读取JSON信息的简便方法,大多数情况下可以考虑直接使用它从后台读取数据。

q   Ext.data.GroupingStore对数据进行分组

Ext.data.GroupingStore继承自Ext.data.Store,它的主要功能是可以对内部的数据进行分组,我们可以在创建Ext.data.GroupingStore时指定根据某个字段进行分组,也可以在创建实例后调用它的groupBy()函数对内部数据重新分组,如下面的代码所示。

    vards = new Ext.data.GroupingStore({

        data:[

            ['id1','name1','female','descn1'],

            ['id2','name2','male','descn2'],

            ['id3','name3','female','descn3'],

            ['id4','name4','male','descn4'],

            ['id5','name5','female','descn5']

        ],

        reader:new Ext.data.ArrayReader({

            fields:['id','name','sex','descn']

        }),

        groupField:'sex',

        groupOnSort:true

    });

上例中,我们使用groupField作为参数,为Ext.data.Grouping设置了分组字段,另外还设置了groupOnSort参数,这个参数可以保证只有在进行分组时才会对Ext.data.Grouping- Store内部的数据进行排序。如果采用默认值,就需要手工指定sortInfo参数,从而指定默认的排序字段和排序方式,否则就会出现错误。

创建Ext.data.GroupingStore的实例之后,我们还可以调用groupBy()函数重新对数据进行分组。因为我们设置了groupOnSort:true,所以在重新分组时,EXT会使用分组的字段对内部数据进行排序。如果不希望对数据进行分组,也可以调用clearGrouping()函数清除分组信息,如下面的代码所示。

    ds.groupBy('id');

    ds.clearGrouping();

10.8EXT中的Ajax

EXT与后台交换数据时,很大程度上依赖于底层实现的Ajax。所谓底层实现,就是说很可能就是我们之前提到的 PrototypejQueryYUI中提供的Ajax功能。为了统一接口,EXT在它们的基础上进行了封装,让我们可以用同一种写法游走于各种不同的底层实现之间。

10.8.1 最容易看到的Ext.Ajax

Ext.Ajax的基本用法如下所示。

Ext.Ajax.request({

    url:'07-01.txt',

    success:function(response) {

        Ext.Msg.alert('成功', response.responseText);

    },

    failure:function(response) {

        Ext.Msg.alert('失败', response.responseText);

    },

    params:{ name: 'value' }

});

这里调用的是Ext.Ajaxrequest函数,它的参数是一个JSON对象,具体如下所示。

q   url参数表示将要访问的后台网址。

q   success参数表示响应成功后的回调函数。

上例中我们直接从response取得返回的字符串,用Ext.Msg.alert显示出来。

q   failure参数表示响应失败后的回调函数。

注意,这里的响应失败并不是指数据库操作之类的业务性失败,而是指HTTP返回404500错误,请不要把HTTP响应错误与业务错误混淆在一起。

q   params参数表示请求时发送到后台的参数,既可以使用JSON对象,也可以直接使用"name=value"形式的字符串。

上面的示例可以在10.store/07-01.html中找到。

Ext.Ajax直接继承自Ext.data.Connection,不同的是,它是一个单例,不需要用new创建实例,可以直接使用。在使用Ext.data.Connection前需要先创建实例,因为Ext.data. Connection是为了给Ext.data中的各种proxy提供Ajax功能,分配不同的实例更有利于分别管理。Ext.Ajax为用户提供了一个简易的调用接口,实际使用时,可以根据自己的需要进行选择。

10.8.2Ext.lib.Ajax是更底层的封装

其实Ext.AjaxExt.data.Connection的内部功能实现都是依靠Ext.lib.Ajax来完成的,在Ext.lib.Ajax下面就是各种底层库的Ajax了。

如果使用Ext.lib.Ajax实现以上的功能,就需要写成下面的形式,如下面的代码所示。

Ext.lib.Ajax.request(

    'POST',

    '07-01txt',

    {success:function(response){

        Ext.Msg.alert('成功', response.responseText);

    },failure:function(){

        Ext.Msg.alert('失败', response.responseText);

    }},

    'data='+ encodeURIComponent(Ext.encode({name:'value'}))

);

我们可以看到,使用Ext.lib.Ajax时需要传递4个参数,分别为methodurlcallbackparams。它们的含义与Ext.Ajax中的参数都是一一对应的,唯一没有提到过的method参数表示请求HTTP的方法,它也可以在Ext.Ajax中使用method:'POST'的方式设置。

相对于Ext.Ajax来说,Ext.lib.Ajax有如下几个缺点。

q   参数的顺序被定死了,第一个参数是method,第二个参数是url,第三个参数是回调函数callback,第四个参数是params。这样既不容易记忆,也无法省略其中某个不需要的参数。Ext.Ajax中用JSON对象来定义参数,使用起来更灵活。

q   params部分,Ext.lib.Ajax必须使用字符串形式,显得有些笨重。Ext.Ajax则可以在JSON对象和字符串之间随意选择,非常灵活。

比与Ext.Ajax相比,Ext.lib.Ajax的唯一优势就是它可以在EXT 1.x中使用。如果你使用的是EXT 2.0或更高的版本,那么就放心大胆地使用Ext.Ajax吧,它会带给你更多的惊喜。

该示例在10.store/07-02.html中。

10.9 关于scopecreateDelegate()

关于JavaScriptthis的使用,这是一个由来已久的问题了。我们这里就不介绍它的发展历史了,只结合具体的例子,告诉大家可能会遇到什么问题,在遇到这些问题时EXT是如何解决的。在使用EXT时,最常碰到的就是使用Ajax回调函数时出现的问题,如下面的代码所示。

<input type="text" name="text" id="text">

<inputtype="button" name="button" id="button"value="button">

现在的HTML 页面中有一个text输入框和一个按钮。我们希望按下这个按钮之后,能用Ajax去后台读取数据,然后把后台响应的数据放到text中,实现过程如代码清单10-6所示。

代码清单10-6Ajax中使用回调函数

function doSuccess(response) {

    text.dom.value= response.responseText;

}

Ext.onReady(function(){

    Ext.get('button').on('click',function(){

        vartext = Ext.get('text');

        Ext.lib.Ajax.request(

            'POST',

            '08.txt',

            {success:doSuccess},

            'param='+ encodeURIComponent(text.dom.value)

        );

    });

});

在上面的代码中,Ajax已经用Ext.get('text')获得了text,以为后面可以直接使用,没想到回调函数success不会按照你写的顺序去执行。当然,也不会像你所想的那样使用局部变量text。实际上,如果什么都不做,仅仅只是使用回调函数,你不得不再次使用Ext.get('text')重新获得元素,否则浏览器就会报text未定义的错误。

在此使用Ext.get('text')重新获取对象还比较简单,在有些情况下不容易获得需要处理的对象,我们要在发送Ajax请求之前获取回调函数中需要操作的对象,有两种方法可供选择:scopecreateDelegate

q   Ajax设置scope

  function doSuccess(response) {

      this.dom.value = response.responseText;

  }

  Ext.lib.Ajax.request(

      'POST',

      '08.txt',

      {success:doSuccess,scope:text},

      'param=' + encodeURIComponent(text.dom.value)

  );              

Ajaxcallback参数部分添加一个scope:text,把回调函数的scope指向text,它的作用就是把doSuccess函数里的this指向text对象。然后再把doSuccess里改成this.dom. value,这样就可以了。如果想再次在回调函数里用某个对象,必须配上scope,这样就能在回调函数中使用this对它进行操作了。

q   success添加createDelegate()

  function doSuccess(response) {

      this.dom.value = response.responseText;

  }

  Ext.lib.Ajax.request(

      'POST',

      '08.txt',

      {success:doSuccess.createDelegate(text)},

      'param=' + encodeURIComponent(text.dom.value)

  );

createDelegate只能在function上调用,它把函数里的this强行指向我们需要的对象,然后我们就可以在回调函数doSuccess里直接通过this来引用createDelegate()中指定的这个对象了。它可以作为解决this问题的一个备选方案。

如果让我选择,我会尽量选择scope,因为createDelegate是要对原来的函数进行封装,重新生成function对象。简单环境下,scope就够用了,倒是createDelegate还有其他功能,比如修改调用参数等。

示例在10.store/08.html中。

10.10DWREXT整合

据不完全统计,从事Ajax开发的Java程序员有一大半都使用DWR。我们下面来介绍一下如何在EXT中使用DWR与后台交互。

10.10.1 在EXT中直接使用DWR

因为DWR在前台的表现形式和普通的JavaScript完全一样,所以我们不需要特地去做些什么,直接使用EXT调用DWR生成的JavaScript函数即可。以Grid为例,比如现在我们要显示一个通讯录的信息,后台记录的数据有:idnamesexemailteladdTimedescn。编写对应的POJO,代码如下所示。

public class Info {

    longid;

    Stringname;

    intsex;

    Stringemail;

    Stringtel;

    DateaddTime;

    Stringdescn;

}

然后编写操作POJOmanager类,代码如下所示。

public class InfoManager {

    privateList infoList = new ArrayList();

    publicList getResult() {

        returninfoList;

    }

}

代码部分有些删减,我们只保留了其中的关键部分,就这样把这两个类配置到dwr.xml中,让前台可以对这些类进行调用。

下面是EXTDWR交互的关键部分,我们要对JavaScript部分做如下修改,如代码清单10-7所示。

代码清单10-7 使用EXT调用DWR

var cm = new Ext.grid.ColumnModel([

    {header:'编号',dataIndex:'id'},

    {header:'名称',dataIndex:'name'},

    {header:'性别',dataIndex:'sex'},

    {header:'邮箱',dataIndex:'email'},

    {header:'电话',dataIndex:'tel'},

    {header:'添加时间',dataIndex:'addTime'},

    {header:'备注',dataIndex:'descn'}

]);

var store = newExt.data.JsonStore({

    fields:["id","name","sex",'email','tel','addTime','descn']

});

// 调用DWR取得数据

infoManager.getResult(function(data){

    store.loadData(data);

});

var grid = newExt.grid.GridPanel({

    renderTo:'grid',

    store:store,

    cm:cm

});

注意,执行infoManager.getResult()函数时,DWR就会使用Ajax去后台取数据了,操作成功后调用我们定义的匿名回调函数。在这里我们只做一件事,那就是将返回的data直接注入到ds中。

DWR返回的data可以被JsonStore直接读取,我们需要设置对应的fields参数,以告诉JsonReader需要哪些属性。

在这里,EXTDWR两者之间没有任何关系,将它们任何一方替换掉都可以。实际上它们只是在一起运行,并没有整合。我们给出的这个示例也是说明了一种松耦合的可能性,实际操作中完全可以使用这种方式。

10.10.2DWRProxy

要结合使用EXTDWR,不需要对后台程序进行任何修改,可以直接让前后台数据进行交互。不过还要考虑很多细节,比如Grid分页、刷新、排序、搜索等常见的操作。EXT的官方网站上已经有人放上了DWRProxy,借助它可以让DWREXT连接得更加紧密。不过,需要在后台添加DWRProxy所需要的Java类,这可能不是最好的解决方案。但我们相信,通过对它的内在实现的讨论,我们可以有更多的选择和想象空间。

注意     这个DWRProxy.js一定要放在ext-base.jsext-all.js后面,否则会出错。

我们现在就用DWRProxy来实现一个分页的示例。除了准备好插件DWRProxy.js外,还要在后台准备一个专门用于分页的封装类。因为不仅要告诉前台显示哪些数据,还要告诉前台一共有多少条数据。现在我们来重点看一下ListRange.java,如下面的代码所示。

public classListRange {

    Object[]data;

    inttotalSize;

}

其实ListRange非常简单,只有两个属性:提供数据的data和提供数据总量的totalSize。再看一下InfoManager.java,为了实现分页,我们专门编写了一个getItems方法,代码如下所示。

public ListRange getItems(Map conditions) {

    intstart = 0;

    intpageSize = 10;

    intpageNo = (start / pageSize) + 1;

    try{

        start= Integer.parseInt(conditions.get("start").toString());

        pageSize= Integer.parseInt(conditions.get("limit").toString());

        pageNo= (start / pageSize) + 1;

    }catch (Exception ex) {

        ex.printStackTrace();

    }

    Listlist = infoList.subList(start, start + pageSize);

    returnnew ListRange(list.toArray(), infoList.size());

}

getItems()的参数是Map,我们从中获得需要的参数,比如startlimit。不过HTTP里的参数都是字符串,而我们需要的是数字,所以要对类型进行相应的转换。根据startlimit两个属性从全部数据中截取一部分,放进新建的ListRange中,然后把生成的ListRange返回给前台,于是一切都解决了。

重头戏要上演了,我们就要使用传说中的Ext.data.DWRProxy了,还有Ext.data.List- RangeReader。通过这两个扩展,EXT完全可以支持DWR的数据传输协议。实际上,这正是EXT要把数据和显示分离设计的原因,这样你只需要添加自定义的proxyreader,不需要修改EXT的其他部分,就可以实现从特定途径获取数据的功能。后台还是DWR,所以至少在Grid部分,我们可以很好地使用它们的结合,主要代码如下所示。

var store = new Ext.data.Store({

    proxy: new Ext.data.DWRProxy(infoManager.getItems, true),

    reader:new Ext.data.ListRangeReader({

        totalProperty:'totalSize',

        root:'data',

        id:'id'

    },info),

    remoteSort:true

});

与我们上面说的一样,我们修改了proxy,也修改了reader,其他地方都不需要进行修改,Grid已经可以正常运行了。需要提醒的是DWRProxy的用法,其中包括两个参数:第一个是dwr- Call,它把一个DWR函数放进去,它对应的是后台的getItems方法;第二个参数是paging- AndSort,这个参数控制DWR是否需要分页和排序。

ListRangeReader部分与后台的ListRange.java对应。totalProperty表示后台数据总数,我们通过它指定从ListRange中读取totalSize属性的值来作为后台数据总数。还需要指定root参数,以告诉它在ListRange中的数据变量的名称为data,随后DWRProxy会从ListRange中的data属性中获取数据并显示到页面上。如果不想使用我们提供的ListRange.java,也可以自己创建一个类,只要把totalPropertydata两个属性与之对应即可。

10.10.3DWRTreeLoader

我们现在来尝试一下让树形也支持DWR。有了前面的基础,整合DWRtree就更简单了。在后台,我们需要树形节点对应的TreeNode.java。目前,只要idtextleaf三项就可以了。

public class TreeNode {

    Stringid;

    Stringtext;

    booleanleaf;

}

id是节点的唯一标记,知道了id就能知道是在触发哪个节点了。text是显示的标题,leaf比较重要,它用来标记这个节点是不是叶子。

这里还是用异步树,TreeNodeManager.java里的getTree()方法将获得一个节点的id作为参数,然后返回这个节点下的所有子节点。我们这里没有限制生成的树形的深度,你可以根据自己的需要进行设置。TreeNodeManager.java的代码如下所示。

public List getTree(String id) {

    Listlist = new ArrayList();

    Stringseed1 = id + 1;

    Stringseed2 = id + 2;

    Stringseed3 = id + 3;

    list.add(newTreeNode(seed1, "" + seed1, false));

    list.add(newTreeNode(seed2, "" + seed2, false));

    list.add(newTreeNode(seed3, "" + seed3, true));

    returnlist;

}

上面的代码并不复杂,它实现的效果与在Java中使用List或数组是相同的,因为返回给前台的数据都是JSON格式的。前台使用JavaScript处理返回信息的部分更简单,先引入DWRTree- Loader.js,然后把TreeLoader替换成DWRTreeLoder即可,如下面的代码所示。

var tree = new Ext.tree.TreePanel('tree', {

    loader:new Ext.tree.DWRTreeLoader({dataUrl: treeNodeManager.getTree})

});

参数依然是dataUrl,它的值treeNodeManager.getTree代表的是一个DWR函数,我们不需要对它进行深入研究,它的内部会自动处理数据之间的对应关系。DWR有时真的很方便。

10.10.4DWRProxyComboBox

DWRProxy既然可以用在Ext.data.Store中,那么它也可以为ComboBox服务,如代码清单10-8所示。

代码清单10-8DWRProxyComboBox整合

var info = Ext.data.Record.create([

    {name:'id', type: 'int'},

    {name:'name', type: 'string'}

]);

var store = newExt.data.Store({

    proxy:new Ext.data.DWRProxy(infoManager.getItems, true),

    reader:new Ext.data.ListRangeReader({

        totalProperty:'totalSize',

        root:'data',

        id:'id'

    },info)

});

var combo = newExt.form.ComboBox({

    store:store,

    displayField:'name',

    valueField:'id',

    triggerAction:'all',

    typeAhead:true,

    mode:'remote',

    emptyText:'请选择',

    selectOnFocus:true

});

combo.render('combo');

我们既可以用mode:'remote'triggerAction:'all'在第一次选择时读取数据,也可以设置mode:'local',然后手工操作store.load()并读取数据。

DWR要比Json-lib方便得多,而且DWR返回的数据可以直接作为JSON使用,使用Json-lib时还要面对无休无止的循环引用。

这次的示例稍微复杂一些,因为包括依赖jar包、classXMLJSP,所以示例单独放在10.store/dwr2/下,请将它们复制到tomcatwebapps下,然后再使用浏览器访问。

10.11localXHR支持本地使用Ajax

Ajax是不能在本地文件系统中使用的,必须把数据放到服务器上。无论是IISApache Tomcat,还是你熟悉的其他服务器,只要支持HTTP协议,就可以使用EXT中的Ajax

至于本地为何不能用Ajax,主要是因为Ajax要判断HTTP响应返回的状态,只有status=200时才认为这次请求是成功的。所以,localXHR做的就是强行修改响应状态,让Ajax可以继续下去。

下面我们来分析一下localXHR的源代码。

q   加入了一个forceActiveX属性,默认是false,它用来控制是否强制使用activexactivex是在IE下专用的。

q   修改createXhrObject函数,只是在最开始处加了一条判断语句,如下所示。

  if(Ext.isIE7 && !!this.forceActiveX){throw("IE7forceActiveX");}              

q   增加了getHttpStatus函数,这是为了处理HTTP的响应状态,如代码清单10-9所示。

代码清单10-9 处理HTTP响应状态

getHttpStatus: function(reqObj){

    var statObj = {

        status:0

        ,statusText:''

        ,isError:false

        ,isLocal:false

        ,isOK:false

    };

    try {

        if(!reqObj)throw('noobj');

        statObj.status= reqObj.status || 0;

        statObj.isLocal= !reqObj.status && location.protocol == "file:" ||

                           Ext.isSafari&& reqObj.status == undefined;

        statObj.statusText= reqObj.statusText || '';

        statObj.isOK= (statObj.isLocal ||

                        (statObj.status> 199 && statObj.status < 300) ||

                         statObj.status== 304);

    } catch(e){

        //status may not avail/valid yet.

        statObj.isError= true;

    }

    return statObj;

},

它为状态增添了更多语义,status表示状态值,statusText表示状态描述,isError表示是否有错误,isLocal表示是否在本地进行Ajax访问,isOK表示操作是否成功。

判断isLocal是否为本地的有两种方法:reqObj没有status,而且请求协议是file:;浏览器是Safari,而且reqObj.status没有定义。

statObj中的isOK属性用来判断此次请求是否成功。判断请求是否成功的条件很多,例如:isLocal的属性为true、响应状态值在199~300之间、响应状态值是304等。如果处理过程中出现了异常,就会将isError属性设置为true,最后会把配置好的statObj对象返回,等待下一个步骤的处理。

localXHR.jshandleTransactionResponse函数进行了简化。因为增加的getHttpStatus函数很好地封装了与请求相关的各种状态信息,所以在handleTransactionResponse函数中我们不会看到让人头晕目眩的响应状态代码。取而代之的是isErrorisOK这些更容易理解的属性,localXHR.js直接使用这些属性来处理响应。

createResponseObject函数被大大强化了。其实前半部分都是一样的,localXHR.js中对isLocal做了大量的处理,响应中的responseText可以从连接中获得。如果需要XML,它就使用ActiveXObject("Microsoft.XMLDOM")new DOMParser()responseText解析成XML放到response里,响应状态也是重新计算的,这样就能让Ajax正常调用了。

最后处理的是asyncRequest函数,如果在异步请求时出现异常,就调用handleTransac- tionResponse返回响应,然后根据各种情况稍微修改header属性。

我们来看看下面这行代码:

Ext.lib.Ajax.forceActiveX= (document.location.protocol == 'file:');              

如果协议是file:,就强制使用activex

10.12 本章小结

本章系统地讨论了Ext.data包中的各个类的功能和使用方式,还涉及如何将EXTDWR通过自定义的proxy相结合的示例。我们介绍了如何使用Ext.data.Connection与后台进行数据交互,还专门介绍了它的子类Ext.Ajax,并讨论了EXTAjax的应用以及在回调函数中使用scopecreateDelegate()解决this的问题。

接着详细介绍了类Ext.data.RecordExt.data.Store的功能和使用方法,这两个类结合起来形成了Ext.data中的主体数据模型,很多组件(包括GridComboBox)都是建立在它们之上的。除此之外,还讨论了常用的proxyreaderstoreSimpleStoreJsonStore,以及它们的应用场景。

最后我们介绍了扩展插件localXHR.js,它可以解决EXTAjax无法访问本地文件的问题。

本章内容

q   Ext.data简介

q   Ext.data.Connection

q   Ext.data.Record

q   Ext.data.Store

q   常用proxy

q   常用reader

q   高级store

q   EXT中的Ajax

q   关于scopecreateDelegate()

q   DWREXT整合

10.1Ext.data简介

Ext.data在命名空间中定义了一系列storereaderproxyGridComboxBox都是以Ext.data为媒介获取数据的,它包含异步加载、类型转换、分页等功能。Ext.data默认支持ArrayJSONXML等数据格式,可以通过MemoryHTTPScriptTag等方式获得这些格式的数据。如果要实现新的协议和新的数据结构,只需要扩展readerproxy即可。DWRProxy就实现了自身的proxyreader,让EXT可以直接从DWR获得数据。

10.2Ext.data.Connection

Ext.data.Connection是对Ext.lib.Ajax的封装,它提供了配置使用Ajax的通用方式,它在内部通过Ext.lib.Ajax实现与后台的异步调用。与底层的Ext.lib.Ajax相比,Ext.data. Connection提供了更简洁的配置方式,使用起来更方便。

Ext.data.Connection主要用于在Ext.data.HttpProxyExt.data.ScriptTagProxy中执行与后台交互的任务,它会从指定的URL获得数据,并把后台返回的数据交给HttpProxyScriptTagProxy处理,Ext.data.Connection的使用方式如代码清单10-1所示。

代码清单10-1 使用Ext.data.Connection

var conn = newExt.data.Connection({

    autoAbort:false,

    defaultHeaders:{

        referer:'http://localhost:8080/'

    },

    disableCaching: false,

    extraParams: {

        name:'name'

    },

    method: 'GET',

    timeout: 300,

    url: '01-01.txt'

});

在使用Ext.data.Connection之前,都要像上面这样创建一个新的Ext.Connection实例。我们可以在构造方法里配置对应的参数,比如autoAbort表示链接是否会自动断开、default- Headers参数表示请求的默认首部信息、disableCaching参数表示请求是否会禁用缓存、extraParams参数代表请求的额外参数、method参数表示请求方法、timeout参数表示连接的超时时间、url参数表示请求访问的网址等。

在创建了conn之后,可以调用request()函数发送请求,处理返回的结果,如下面的代码所示。

conn.request({

    success:function(response) {

        Ext.Msg.alert('info',response.responseText);

    },

    failure:function() {

        Ext.Msg.alert('warn','failure');

    }

});

Request()函数中可以设置successfailure两个回调函数,分别在请求成功和请求失败时调用。请求成功时,success函数的参数就是后台返回的信息。

我们再来看一下request函数中的其他参数。

q   url:String:请求url

q   params:Object/String/Function:请求传递的参数。

q   method:String:请求方法,通常为GETPOST

q   callback:Function:请求完成后的回调函数,无论是成功还是失败,都会执行。

q   success:Function:请求成功时的回调函数。

q   failure:Function:请求失败时的回调函数

q   scope:Object:回调函数的作用域。

q   form:Object/String:绑定的form表单。

q   isUpload:Boolean:是否执行文件上传。

q   headers:Object:请求首部信息。

q   xmlData:ObjectXML文档对象,可以通过URL附加参数的方式发起请求。

q   disableCaching:Boolean:是否禁用缓存,默认为禁用。

Ext.data.Connection还提供了abort([Number transactionId])函数,当同时有多个请求发生时,根据指定的事务id放弃其中的某一个请求。如果不指定事务id,就会放弃最后一个请求。isLoading([NumbertransactionId])函数的用法与abort()类似,可以根据事务id判断对应的请求是否完成。如果未指定事务id,就判断最后一个请求是否完成。

10.3Ext.data.Record

Ext.data.Record就是一个设定了内部数据类型的对象,它是Ext.data.Store的最基本组成部分。如果把Ext.data.Store看作是一张二维表,那么它的每一行就对应一个Ext.data. Record实例。

Ext.data.Record的主要功能是保存数据,并且在内部数据发生改变时记录修改的状态,它还可以保留修改之前的原始值。

我们使用Ext.data.Record时通常都是由create()函数开始,首先用create()函数创建一个自定义的Record类型,如下面的代码所示。

var PersonRecord= Ext.data.Record.create([

    {name:'name', type: 'string'},

    {name:'sex', type: 'int'}

]);

PersonRecord就是我们定义的新类型,包含字符串类型的name和整数类型的sex两个属性,然后我们使用new关键字创建PersonRecord的实例,如下面的代码所示。

var boy = newPersonRecord({

    name:'boy',

    sex:0

});

创建对象时,可以直接通过构造方法为对象赋予初始值,将'boy'赋值给name0赋值给sex

现在,我们得到了PersonRecord的实例boy,如何才能得到它的属性呢?以下三种方式都可以获得boyname属性的数据,如下面的代码所示。

alert(boy.data.name);

alert(boy.data['name']);

alert(boy.get('name'));

这里涉及Ext.data.Recorddata属性,这是定义在Ext.data.Record中的一个公共属性,用于保存当前record对象的所有数据。它是一个JSON对象,可以直接从它里面获得需要的数据。可以通过Ext.data.Recordget()函数方便地从data属性中获得指定的属性值。

如果我们需要修改boy中的数据,请不要使用以下方式直接操作data,如下面的代码所示。

    boy.data.name= 'boy name';

    boy.data['name']= 'boy name';

而应该使用set()函数,如下面的代码所示。

    boy.set('name','body name');

set()函数会判断属性值是否发生了改变,如果改变了,就要将当前对象的dirty属性设置为true,并将修改之前的原始值放入modified对象中,供其他函数使用。如果直接操作data中的值,record就无法记录属性数据的修改情况。

Record的属性数据被修改后,我们可以执行如下几种操作。

q   commit()(提交):这个函数的效果是设置dirtyfalse,并删除modified中保存的原始数据。

q   reject()(撤销):这个函数的效果是将data中已经修改了的属性值都恢复成modified中保存的原始数据,然后设置dirtyfalse,并删除保存原始数据的modified对象。

q   getChanges()获得修改的部分:这个函数会把data中经过修改的属性和数据放在一个JSON对象里并返回。例如上例中,getChanges()返回的结果是{name:’body name’}

q   我们还可以调用isModified()判断当前record中的数据是否被修改。

      Ext.data.Record还提供了用于复制record实例的函数copy()

  varcopyBoy = boy.copy();

这样我们就得到了boy的一个副本,它里面包含了boydata数据,但copy()函数不会复制dirtymodified等额外的属性值。

Ext.data.Record中其他的参数大多与Ext.data.Store有关,请参考与Ext.data.Store相关的讨论。

10.4Ext.data.Store

Ext.data.StoreEXT中用来进行数据交换和数据交互的标准中间件,无论是Grid还是ComboBox,都是通过它实现数据读取、类型转换、排序分页和搜索等操作的。

Ext.data.Store中有一个Ext.data.Record数组,所有数据都存放在这些Ext.data. Record实例中,为后面的读取和修改操作做准备。

10.4.1 基本应用

在使用之前,首先要创建一个Ext.data.Store的实例,如下面的代码所示。

var data = [

    ['boy',0],

    ['girl',1]

];

var store = newExt.data.Store({

    proxy:new Ext.data.MemoryProxy(data),

    reader:new Ext.data.ArrayReader({}, PersonRecord)

});

store.load();

每个store最少需要两个组件的支持,分别是proxyreaderproxy用于从某个途径读取原始数据,reader用于将原始数据转换成Record实例。

这里我们使用的是Ext.data.MemoryProxyExt.data.ArrayReader,将data数组中的数据转换成对应的几个PersonRecord实例,然后放入store中。store创建完毕之后,执行store.load()实现这个转换过程。

经过转换之后,store里的数据就可以提供给GridComboBox使用了,这就是Ext.data. Store的最基本用法。

10.4.2 对数据进行排序

Ext.data.Store提供了一系列属性和函数,利用它们对数据进行排序操作。

可以在创建Ext.data.Store时使用sortInfo参数指定排序的字段和排序方式,如下面的代码所示。

var store = newExt.data.Store({

    proxy:new Ext.data.MemoryProxy(data),

    reader:new Ext.data.ArrayReader({}, PersonRecord),

    sortInfo:{field: 'name', direction: 'DESC'}

});

这样,在store加载数据之后,就会自动根据name字段进行降序排列。对store使用store.setDefaultSort('name','DESC');也会达到同样效果。

也可以在任何时候调用sort()函数,比如store.sort('name','DESC');,对store中的数据进行排序。

如果我们希望获得store的排序信息,可以调用getSortState()函数,返回的是类似{field: "name", direction: " DESC"}JSON对象。

与排序相关的参数还有remoteSort,这个参数是用来实现后台排序功能的。当设置为remoteSort:true时,store会在向后台请求数据时自动加入sortdir两个参数,分别对应排序的字段和排序的方式,由后台获取并处理这两个参数,在后台对所需数据进行排序操作。remoteSort:true也会导致每次执行sort()时都要去后台重新加载数据,而不能只对本地数据进行排序。

详细的用法可以参考第2章。

10.4.3 从store中获取数据

store中获取数据有很多种途径,可以依据不同的要求选择不同的函数。最直接的方法是根据recordstore中的行号获得对应的record,得到了record就可以使用get()函数获得里面的数据了,如下面的代码所示。

store.getAt(0).get('name')

通过这种方式,我们可以遍历store中所有的record,依次得到它们的数据,如下面的代码所示。

for (var i = 0;i < store.getCount(); i++) {

    varrecord = store.getAt(i);

    alert(record.get('name'));

}

Store.getCount()返回的是store中的所有数据记录,然后使用for循环遍历整个store,从而得到每条记录。

除了使用getCount()的方法外,还可以使用each()函数,如下面的代码所示。

store.each(function(record){

    alert(record.get('name'));

});

Each()可以接受一个函数作为参数,遍历内部record,并将每个record作为参数传递给function()处理。如果希望停止遍历,可以让function()返回false

也可以使用getRange()函数连续获得多个record,只需要指定开始和结束位置的索引值,如下面的代码所示。

var records =store.getRange(0, 1);

for (var i = 0;i < records.length; i++) {

    varrecord = records[i];

    alert(record.get('name'));

}

如果确实不知道recordid,也可以根据record本身的idstore中获得对应的record,如下面的代码所示。

store.getById(1001).get('name')

EXT还提供了函数find()findBy(),可以利用它们对store中的数据进行搜索,如下面的代码所示。

find( Stringproperty, String/RegExp value, [Number startIndex], [Boolean anyMatch],

[BooleancaseSensitive] )

在这5个参数中,只有前两个是必须的。第一个参数property代表搜索的字段名;第二个参数value是匹配用字符串或正则表达式;第三个参数startIndex表示从第几行开始搜索,第四个参数anyMatchtrue时,不必从头开始匹配;第五个参数caseSensitivetrue时,会区分大小写。

如下面的代码所示:

var index =store.find('name','g');

alert(store.getAt(index).get('name'));

find()函数对应的findBy()函数的定义格式如下:

findBy( Functionfn, [Object scope], [Number startIndex] ) : Number

findBy()函数允许用户使用自定义函数对内部数据进行搜索。fn返回true时,表示查找成功,于是停止遍历并返回行号。fn返回false时,表示查找失败(即未找到),继续遍历,如下面的代码所示。

index =store.findBy(function(record, id) {

    returnrecord.get('name') == 'girl' && record.get('sex') == 1;

});

alert(store.getAt(index).get('name'));

通过findBy()函数,我们可以同时判断record中的多个字段,在函数中实现复杂逻辑。

我们还可以使用queryqueryBy函数对store中的数据进行查询。与findfindBy不同的是,queryqueryBy返回的是一个MixCollection对象,里面包含了搜索得到的数据,如下面的代码所示。

alert(store.query('name','boy'));

    alert(store.queryBy(function(record){

        returnrecord.get('name') == 'girl' && record.get('sex') == 1;

    }));

10.4.4 更新store中的数据

可以使用add(Ext.data.Record[] records)store末尾添加一个或多个record,使用的参数可以是一个record实例,如下面的代码所示。

store.add(newPersonRecord({

    name:'other',

    sex:0

}));

Add()的也可以添加一个record数组,如下面的代码所示:

store.add([newPersonRecord({

    name:'other1',

    sex:0

}), newPersonRecord({

    name:'other2',

    sex:0

})]);

Add()函数每次都会将新数据添加到store的末尾,这就有可能破坏store原有的排序方式。如果希望根据store原来的排序方式将新数据插入到对应的位置,可以使用addSorted()函数。它会在添加新数据之后立即对store进行排序,这样就可以保证store中的数据有序地显示,如下面的代码所示。

store.addSorted(newPersonRecord({

    name:'lili',

    sex:1

}));

store会根据排序信息查找这条record应该插入的索引位置,然后根据得到的索引位置插入数据,从而实现对整体进行排序。这个函数需要预先为store设置本地排序,否则会不起作用。

如果希望自己指定数据插入的索引位置,可以使用insert()函数。它的第一个参数表示插入数据的索引位置,可以使用record实例或record实例的数组作为参数,插入之后,后面的数据自动后移,如下面的代码所示。

store.insert(3,new PersonRecord({

    name:'other',

    sex:0

}));

store.insert(3,[new PersonRecord({

    name:'other1',

    sex:0

}), newPersonRecord({

    name:'other2',

    sex:0

})]);

删除操作可以使用remove()removeAll()函数,它们分别可以删除指定的record和清空整个store中的数据,如下面的代码所示。

store.remove(store.getAt(0));

store.removeAll();

store中没有专门提供修改某一行record的操作,我们需要先从store中获取一个record。对这个record内部数据的修改会直接反映到store上,如下面的代码所示。

store.getAt(0).set('name','xxxx');

修改record的内部数据之后有两种选择:执行rejectChanges()撤销所有修改,将修改过的record恢复到原来的状态;执行commitChanges()提交数据修改。在执行撤销和提交操作之前,可以使用getModifiedRecords()获得store中修改过的record数组。

与修改数据相关的参数是pruneModifiedRecords,如果将它设置为true,当每次执行删除或reload操作时,都会清空所有修改。这样,在每次执行删除或reload操作之后,getModifiedRecords()返回的就是一个空数组,否则仍然会得到上次修改过的record记录。

10.4.5 加载及显示数据

store创建好后,需要调用load()函数加载数据,加载成功后才能对store中的数据进行操作。load()调用的完整过程如下面的代码所示。

store.load({

    params:{start:0,limit:20},

    callback:function(records, options, success){

        Ext.Msg.alert('info','加载完毕');

    },

    scope:store,

    add:true

});

q   params是在store加载时发送的附加参数。

q   callback是加载完毕时执行的回调函数,它包含3个参数:records参数表示获得的数据,options表示执行load()时传递的参数,success表示是否加载成功。

q   Scope用来指定回调函数执行时的作用域。

q   Addtrue时,load()得到的数据会添加在原来的store数据的末尾,否则会先清除之前的数据,再将得到的数据添加到store中。

一般来说,为了对store中的数据进行初始化,load()函数只需要执行一次。如果用params参数指定了需要使用的参数,以后再次执行reload()重新加载数据时,store会自动使用上次load()中包含的params参数内容。

如果有一些需要固定传递的参数,也可以使用baseParams参数执行,它是一个JSON对象,里面的数据会作为参数发送给后台处理,如下面的代码所示。

store.baseParams.start= 0;

store.baseParams.limit= 20;

store加载数据之后,有时不需要把所有数据都显示出来,这时可以使用函数filterfilterBystore中的数据进行过滤,只显示符合条件的部分,如下面的代码所示。

filter( Stringfield, String/RegExp value, [Boolean anyMatch],
[Boolean caseSensitive] ) : void

filter()函数的用法与之前谈到的find()相似,如下面的代码所示。

store.filter('name','boy');

对应的filterBy()findBy()类似,也可以在自定义的函数中实现各种复杂判断,如下面的代码所示。

store.filterBy(function(record){

    returnrecord.get('name') == 'girl' && record.get('sex') == 1;

});

如果想取消过滤并显示所有数据,那么可以调用clearFilter()函数,如下面的代码所示。

store.clearFilter();

如果想知道store上是否设置了过滤器,可以通过isFiltered()函数进行判断。

10.4.6 其他功能

除了上面提到的数据获取、排序、更新、显示等功能外,store还提供了其他一些功能函数。

collect( StringdataIndex, [Boolean allowNull], [Boolean bypassFilter] ) : Array

collect函数获得指定的dataIndex对应的那一列的数据,当allowNull参数为true时,返回的结果中可能会包含nullundefined或空字符串,否则collect函数会自动将这些空数据过滤掉。当bypassFilter参数为true时,collect的结果不会受查询条件的影响,无论查询条件是什么都会忽略掉,返回的信息是所有的数据,如下面的代码所示。

alert(store.collect('name'));

这样会获得所有name列的值,示例中返回的是包含了'boy''girl'的数组。

getTotalCount()用于在翻页时获得后台传递过来的数据总数。如果没有设置翻页,get- TotalCount()的结果与getCount()相同,都是返回当前的数据总数,如下面的代码所示。

alert(store.getTotalCount());

indexOf(Ext.data.Record record)indexOfId(String id)函数根据recordrecordid获得record对应的行号,如下面的代码所示。

alert(store.indexOf(store.getAt(1)));

alert(store.indexOfId(1001));

loadData(object data, [Boolean append])从本地JavaScript变量中读取数据,appendtrue时,将读取的数据附加到原数据后,否则执行整体更新,如下面的代码所示。

store.loadData(data,true);

Sum(String property, Number start, Number end):Number用于计算某一个列从startend的总和,如下面的代码所示。

alert(store.sum('sex'));

如果省略参数startend,就计算全部数据的总和。

store还提供了一系列事件(见表10-1),让我们可以为对应操作设定操作函数。

10-1store提供的事件

事件名

参  数

add

( Store this, Ext.data.Record[] records, Number index )

beforelaod

( Store this, Object options )

clear

( Store this )

datachanged

( Store this )

load

( Store this, Ext.data.Record[] records, Object options )

loadexception

()

metachange

( Store this, Object meta. )

remove

( Store this, Ext.data.Record record, Number index )

update

( Store this, Ext.data.Record record, String operation )

至此,storerecord等组件已经讲解完毕,下面我们主要讨论一下常用的proxyreader组件。

10.5 常用proxy

10.5.1MemoryProxy

MemoryProxy只能从JavaScript对象获得数据,可以直接把数组,或JSONXML格式的数据交给它处理,如下面的代码所示。

var proxy = new Ext.data.MemoryProxy([

    ['id1','name1','descn1'],

    ['id2','name2','descn2']

]);              

10.5.2HttpProxy

HttpProxy使用HTTP协议,通过Ajax去后台取数据,构造它时需要设置url:'xxx.jsp'参数。这里的url可以替换成任何一个合法的网址,这样HttpProxy才知道去哪里获取数据,如下面的代码所示。

var proxy = new Ext.data.HttpProxy({url:'xxx.jsp'});              

后台需要返回EXT所需要的JSON格式的数据,下面的内容就是后台使用JSP的一个范例,如下面的代码所示。

response.setContentType("application/x-json");

Writer out =response.getWriter();

out.print("[" +

        "['id1','name1','descn1']" +

        "['id2','name2','descn2']" +

    "]");              

请注意,这里的HttpProxy不支持跨域,它只能从同一域中获得数据。如果想跨域,请参考下面的ScriptTagProxy

10.5.3ScriptTagProxy

ScriptTagProxy的用法几乎和HttpProxy一样,如下面的代码所示。

var proxy = new Ext.data.ScriptTagProxy({url:'xxx.jsp'});              

从这里也看不出来它是如何支持跨域的,我们还需要在后台进行相应的处理,如下面的代码所示。

String cb =request.getParameter("callback");

response.setContentType("text/javascript");

Writer out =response.getWriter();

out.write(cb+ "(");

out.print("[" +

        "['id1','name1','descn1']" +

        "['id2','name2','descn2']" +

    "]");

out.write(");");

其中的关键就在于从请求中获得的callback参数,这个参数叫做回调函数。ScriptTag- Proxy会在当前的HTML页面里添加一个<scripttype="text/javascript"src="xxx.jsp"> </script>标签,然后把后台返回的内容添加到这个标签中,这样就可以解决跨域访问数据的问题。为了让后台返回的内容可以在动态生成的标签中运行,EXT会生成一个名为callback的回调函数,并把回调函数的名称传递给后台,由后台生成callback(data)形式的响应内容,然后返回给前台自动运行。

虽然上述处理过程比较难理解,但是我们只需要了解ScriptTagProxy的用法就足够了。如果还想进一步了解ScriptTagProxy的运行过程,可以使用Firebug查看动态生成的HTML以及响应的JSON内容。

最后我们来分析一下EXTAPI文档中提供的示例,这段后台代码会自动判断请求的类型,返回支持ScriptTagProxyHttpProxy的数据,如代码清单10-2所示。

代码清单10-2 在后台同时支持HttpProxyScriptTagProxy

boolean scriptTag = false;

String cb =request.getParameter("callback");

if (cb != null) {

    scriptTag= true;

    response.setContentType("text/javascript");

} else {

    response.setContentType("application/x-json");

}

Writer out =response.getWriter();

if (scriptTag) {

    out.write(cb+ "(");

}

out.print(dataBlock.toJsonString());

if (scriptTag) {

    out.write(");");

}              

代码中通过判断请求中是否包含callback参数来决定返回何种数据类型。如果包含,就返回ScriptTagProxy需要的数据;否则,就当作HttpProxy处理。

10.6 常用Reader

10.6.1ArrayReader

proxy中读取的数据需要进行解析,这些数据转换成Record数组后才能提供给Ext.data. Store使用。

ArrayReader的作用是从二维数组里依次读取数据,然后生成对应的Record。默认情况下是按列顺序读取数组中的数据,不过你也可以考虑用mapping指定record与原始数组对应的列号。ArrayReader的用法很简单,但缺点是不支持分页。使用二维数组的方式如下面的代码所示。

var data = [

    ['id1','name1','descn1'],

    ['id2','name2','descn2']

];

对应的ArrayReader如下面的代码所示。

var reader = new Ext.data.ArrayReader({

    id:1

},[

    {name:'name',mapping:1},

    {name:'descn',mapping:2},

    {name:'id',mapping:0},

]);

我们演示的是字段顺序不一致的情况,如果字段顺序和列顺序一致,就不用额外配置mapping

10.6.2JsonReader

JavaScript中,JSON是一种非常重要的数据格式,key:value的形式比XML那种复杂的标签结构更容易理解,代码量也更小,很多人倾向于使用它作为EXT的数据交换格式。为Json- Reader准备的JSON数据如下面的代码所示。

var data = {

    id:0,

    totalProperty:2,

    successProperty:true,

    root:[

        {id:'id1',name:'name1',descn:'descn1'},

        {id:'id2',name:'name2',descn:'descn2'}

    ]

};

与数组相比,JSON的最大优点就是支持分页,我们可以使用totalProperty参数表示数据的总量。successProperty参数是可选的,可以用它判断当前请求是否执行成功,进而判断是否进行数据加载。在不希望JsonReader处理响应数据时,可以把successProperty设置成false

现在来讨论一下JsonReader,看看它是如何与上面的JSON数据对应的,如下面的代码所示。

var reader = new Ext.data.JsonReader({

    successProperty: "successproperty",

    totalProperty: "totalProperty",

    root: "root",

    id: "id"

}, [

    {name:'id',mapping:'id'},

    {name:'name',mapping:'name'},

    {name:'descn',mapping:'descn'}

]);

上例中的对应方式不够简洁,因为namemapping部分的内容是相同的,其实这里的mapping可以省略,默认会用name参数从JSON中获得对应的数据。如果不想与JSON里的名字一样,也可以使用mapping修改。不过,mapping在这里还有其他用途,如代码清单10-3所示。

代码清单10-3 为JsonReader设置mapping进行数据映射

var data = {

    id:0,

    totalProperty:2,

    successProperty:true,

    root:[

        {id:'id1',name:'name1',descn:'descn1',person:{

            id:1,name:'man',sex:'male'

        }},

        {id:'id2',name:'name2',descn:'descn2',person:{

            id:2,name:'woman',sex:'female'

        }}

    ]

};

var reader = new Ext.data.JsonReader({

    successProperty: "successproperty",

    totalProperty: "totalProperty",

    root: "root",

    id: "id"

}, [

    'id','name','descn',

    {name:'person_name',mapping:'person.name'},

    {name:'person_sex',mapping:'person.sex'}

]);

在上面的代码中,我们使用JSON支持更复杂的嵌套结构,其中的person对象自身就拥有id namesex等属性。在JsonReader中可以用mapping把这些嵌套的内部属性映射出来,赋予对应的record,而其他字段都不变。

10.6.3XmlReader

XML是非常通用的数据传输格式,XmlReader使用的XML格式的数据如代码清单10-4所示。

代码清单10-4XmlReader使用的XML格式的数据

<?xml version="1.0" encoding="utf-8"?>

<dataset>

    <id>1</id>

    <totalRecords>2</totalRecords>

    <success>true</success>

    <record>

        <id>1</id>

        <name>name1</name>

        <descn>descn1</descn>

    </record>

    <record>

        <id>2</id>

        <name>name2</name>

        <descn>descn2</descn>

    </record>

</dataset>

这里一定要用dataset作为XML根元素。再让我们看一下如何对XmlReader进行配置,从而读取上面示例中的XML数据,如下面的代码所示。

var reader = new Ext.data.XmlReader({

   totalRecords: 'totalRecords',

   success: 'success'

   record: 'record',

   id: "id"

}, ['id','name','descn']);              

XmlReader使用的参数与之前介绍的JsonReader有些不同,我们可以看到这里用到了totalRecordsrecord两个参数,其中totalRecords用来指定从’totalRecords’标签里获得后台数据总数,record则表示XML中放在record标签里的数据是我们需要显示的结果数据。其他两个参数successid的含义和JsonReader中对应的参数相似,分别用来判断操是否成功和这次返回的id。因为XML中的标签和reader里需要的名字是相同的,所以简化了配置,将[{name:’id’},{name:’name’},{name:’descn’}]直接写成了[‘id’,’name’,’descn’]

因为XmlReader不能将JavaScript中的字符串自动解析成XML格式的数据,因此我们需要利用其他方法进行演示。参考localXHR.js中构造XML的方式,我们有了下面的解决方案,如代码清单10-5所示。

代码清单10-5 通过本地字符串构造XML对象

var data = "<?xmlversion='1.0' encoding='utf-8'?>" +

    "<dataset>" +

        "<id>1</id>" +

        "<totalRecords>2</totalRecords>" +

        "<success>true</success>" +

        "<record>" +

            "<id>1</id>" +

            "<name>name1</name>" +

            "<descn>descn1</descn>" +

        "</record>" +

        "<record>" +

            "<id>2</id>" +

            "<name>name2</name>" +

            "<descn>descn2</descn>" +

        "</record>" +

    "</dataset>";

var xdoc;

if(typeof(DOMParser) == 'undefined'){

    xdoc= new ActiveXObject("Microsoft.XMLDOM");

    xdoc.async="false";

    xdoc.loadXML(data);

}else{

    var domParser = new DOMParser();

    xdoc= domParser.parseFromString(data, 'application/xml');

    domParser= null;

}

var proxy = new Ext.data.MemoryProxy(xdoc);

var reader = new Ext.data.XmlReader({

    totalRecords: 'totalRecords',

    success: 'success',

    record: 'record',

    id:"id"

}, ['id','name','descn']);

var ds = new Ext.data.Store({

    proxy:proxy,

    reader:reader

});

10.7 高级store

实际开发时,并不需要每次都对proxyreaderstore这三个对象进行配置,EXT为我们提供了几种可选择的整合方案。

q   SimpleStore = Store + MemoryProxy + ArrayReader

  var ds = Ext.data.SimpleStore({

      data:[

            ['id1','name1','descn1'],

            ['id2','name2','descn2']

      ],

      fields:['id','name','descn']

  });

SimpleStore是专为简化读取本地数组而设计的,设置上MemoryProxy需要的dataArrayReader需要的fields就可以使用了。

q   JsonStore = Store + HttpProxy + JsonReader

  var ds = Ext.data.JsonStore({

      url: 'xxx.jsp',

      root: 'root',

      fields:['id','name','descn']

  });

JsonStoreJsonReaderHttpProxy整合在一起,提供了一种从后台读取JSON信息的简便方法,大多数情况下可以考虑直接使用它从后台读取数据。

q   Ext.data.GroupingStore对数据进行分组

Ext.data.GroupingStore继承自Ext.data.Store,它的主要功能是可以对内部的数据进行分组,我们可以在创建Ext.data.GroupingStore时指定根据某个字段进行分组,也可以在创建实例后调用它的groupBy()函数对内部数据重新分组,如下面的代码所示。

    vards = new Ext.data.GroupingStore({

        data:[

            ['id1','name1','female','descn1'],

            ['id2','name2','male','descn2'],

            ['id3','name3','female','descn3'],

            ['id4','name4','male','descn4'],

            ['id5','name5','female','descn5']

        ],

        reader:new Ext.data.ArrayReader({

            fields:['id','name','sex','descn']

        }),

        groupField:'sex',

        groupOnSort:true

    });

上例中,我们使用groupField作为参数,为Ext.data.Grouping设置了分组字段,另外还设置了groupOnSort参数,这个参数可以保证只有在进行分组时才会对Ext.data.Grouping- Store内部的数据进行排序。如果采用默认值,就需要手工指定sortInfo参数,从而指定默认的排序字段和排序方式,否则就会出现错误。

创建Ext.data.GroupingStore的实例之后,我们还可以调用groupBy()函数重新对数据进行分组。因为我们设置了groupOnSort:true,所以在重新分组时,EXT会使用分组的字段对内部数据进行排序。如果不希望对数据进行分组,也可以调用clearGrouping()函数清除分组信息,如下面的代码所示。

    ds.groupBy('id');

    ds.clearGrouping();

10.8EXT中的Ajax

EXT与后台交换数据时,很大程度上依赖于底层实现的Ajax。所谓底层实现,就是说很可能就是我们之前提到的 PrototypejQueryYUI中提供的Ajax功能。为了统一接口,EXT在它们的基础上进行了封装,让我们可以用同一种写法游走于各种不同的底层实现之间。

10.8.1 最容易看到的Ext.Ajax

Ext.Ajax的基本用法如下所示。

Ext.Ajax.request({

    url:'07-01.txt',

    success:function(response) {

        Ext.Msg.alert('成功', response.responseText);

    },

    failure:function(response) {

        Ext.Msg.alert('失败', response.responseText);

    },

    params:{ name: 'value' }

});

这里调用的是Ext.Ajaxrequest函数,它的参数是一个JSON对象,具体如下所示。

q   url参数表示将要访问的后台网址。

q   success参数表示响应成功后的回调函数。

上例中我们直接从response取得返回的字符串,用Ext.Msg.alert显示出来。

q   failure参数表示响应失败后的回调函数。

注意,这里的响应失败并不是指数据库操作之类的业务性失败,而是指HTTP返回404500错误,请不要把HTTP响应错误与业务错误混淆在一起。

q   params参数表示请求时发送到后台的参数,既可以使用JSON对象,也可以直接使用"name=value"形式的字符串。

上面的示例可以在10.store/07-01.html中找到。

Ext.Ajax直接继承自Ext.data.Connection,不同的是,它是一个单例,不需要用new创建实例,可以直接使用。在使用Ext.data.Connection前需要先创建实例,因为Ext.data. Connection是为了给Ext.data中的各种proxy提供Ajax功能,分配不同的实例更有利于分别管理。Ext.Ajax为用户提供了一个简易的调用接口,实际使用时,可以根据自己的需要进行选择。

10.8.2Ext.lib.Ajax是更底层的封装

其实Ext.AjaxExt.data.Connection的内部功能实现都是依靠Ext.lib.Ajax来完成的,在Ext.lib.Ajax下面就是各种底层库的Ajax了。

如果使用Ext.lib.Ajax实现以上的功能,就需要写成下面的形式,如下面的代码所示。

Ext.lib.Ajax.request(

    'POST',

    '07-01txt',

    {success:function(response){

        Ext.Msg.alert('成功', response.responseText);

    },failure:function(){

        Ext.Msg.alert('失败', response.responseText);

    }},

    'data='+ encodeURIComponent(Ext.encode({name:'value'}))

);

我们可以看到,使用Ext.lib.Ajax时需要传递4个参数,分别为methodurlcallbackparams。它们的含义与Ext.Ajax中的参数都是一一对应的,唯一没有提到过的method参数表示请求HTTP的方法,它也可以在Ext.Ajax中使用method:'POST'的方式设置。

相对于Ext.Ajax来说,Ext.lib.Ajax有如下几个缺点。

q   参数的顺序被定死了,第一个参数是method,第二个参数是url,第三个参数是回调函数callback,第四个参数是params。这样既不容易记忆,也无法省略其中某个不需要的参数。Ext.Ajax中用JSON对象来定义参数,使用起来更灵活。

q   params部分,Ext.lib.Ajax必须使用字符串形式,显得有些笨重。Ext.Ajax则可以在JSON对象和字符串之间随意选择,非常灵活。

比与Ext.Ajax相比,Ext.lib.Ajax的唯一优势就是它可以在EXT 1.x中使用。如果你使用的是EXT 2.0或更高的版本,那么就放心大胆地使用Ext.Ajax吧,它会带给你更多的惊喜。

该示例在10.store/07-02.html中。

10.9 关于scopecreateDelegate()

关于JavaScriptthis的使用,这是一个由来已久的问题了。我们这里就不介绍它的发展历史了,只结合具体的例子,告诉大家可能会遇到什么问题,在遇到这些问题时EXT是如何解决的。在使用EXT时,最常碰到的就是使用Ajax回调函数时出现的问题,如下面的代码所示。

<input type="text" name="text" id="text">

<inputtype="button" name="button" id="button"value="button">

现在的HTML 页面中有一个text输入框和一个按钮。我们希望按下这个按钮之后,能用Ajax去后台读取数据,然后把后台响应的数据放到text中,实现过程如代码清单10-6所示。

代码清单10-6Ajax中使用回调函数

function doSuccess(response) {

    text.dom.value= response.responseText;

}

Ext.onReady(function(){

    Ext.get('button').on('click',function(){

        vartext = Ext.get('text');

        Ext.lib.Ajax.request(

            'POST',

            '08.txt',

            {success:doSuccess},

            'param='+ encodeURIComponent(text.dom.value)

        );

    });

});

在上面的代码中,Ajax已经用Ext.get('text')获得了text,以为后面可以直接使用,没想到回调函数success不会按照你写的顺序去执行。当然,也不会像你所想的那样使用局部变量text。实际上,如果什么都不做,仅仅只是使用回调函数,你不得不再次使用Ext.get('text')重新获得元素,否则浏览器就会报text未定义的错误。

在此使用Ext.get('text')重新获取对象还比较简单,在有些情况下不容易获得需要处理的对象,我们要在发送Ajax请求之前获取回调函数中需要操作的对象,有两种方法可供选择:scopecreateDelegate

q   Ajax设置scope

  function doSuccess(response) {

      this.dom.value = response.responseText;

  }

  Ext.lib.Ajax.request(

      'POST',

      '08.txt',

      {success:doSuccess,scope:text},

      'param=' + encodeURIComponent(text.dom.value)

  );              

Ajaxcallback参数部分添加一个scope:text,把回调函数的scope指向text,它的作用就是把doSuccess函数里的this指向text对象。然后再把doSuccess里改成this.dom. value,这样就可以了。如果想再次在回调函数里用某个对象,必须配上scope,这样就能在回调函数中使用this对它进行操作了。

q   success添加createDelegate()

  function doSuccess(response) {

      this.dom.value = response.responseText;

  }

  Ext.lib.Ajax.request(

      'POST',

      '08.txt',

      {success:doSuccess.createDelegate(text)},

      'param=' + encodeURIComponent(text.dom.value)

  );

createDelegate只能在function上调用,它把函数里的this强行指向我们需要的对象,然后我们就可以在回调函数doSuccess里直接通过this来引用createDelegate()中指定的这个对象了。它可以作为解决this问题的一个备选方案。

如果让我选择,我会尽量选择scope,因为createDelegate是要对原来的函数进行封装,重新生成function对象。简单环境下,scope就够用了,倒是createDelegate还有其他功能,比如修改调用参数等。

示例在10.store/08.html中。

10.10DWREXT整合

据不完全统计,从事Ajax开发的Java程序员有一大半都使用DWR。我们下面来介绍一下如何在EXT中使用DWR与后台交互。

10.10.1 在EXT中直接使用DWR

因为DWR在前台的表现形式和普通的JavaScript完全一样,所以我们不需要特地去做些什么,直接使用EXT调用DWR生成的JavaScript函数即可。以Grid为例,比如现在我们要显示一个通讯录的信息,后台记录的数据有:idnamesexemailteladdTimedescn。编写对应的POJO,代码如下所示。

public class Info {

    longid;

    Stringname;

    intsex;

    Stringemail;

    Stringtel;

    DateaddTime;

    Stringdescn;

}

然后编写操作POJOmanager类,代码如下所示。

public class InfoManager {

    privateList infoList = new ArrayList();

    publicList getResult() {

        returninfoList;

    }

}

代码部分有些删减,我们只保留了其中的关键部分,就这样把这两个类配置到dwr.xml中,让前台可以对这些类进行调用。

下面是EXTDWR交互的关键部分,我们要对JavaScript部分做如下修改,如代码清单10-7所示。

代码清单10-7 使用EXT调用DWR

var cm = new Ext.grid.ColumnModel([

    {header:'编号',dataIndex:'id'},

    {header:'名称',dataIndex:'name'},

    {header:'性别',dataIndex:'sex'},

    {header:'邮箱',dataIndex:'email'},

    {header:'电话',dataIndex:'tel'},

    {header:'添加时间',dataIndex:'addTime'},

    {header:'备注',dataIndex:'descn'}

]);

var store = newExt.data.JsonStore({

    fields:["id","name","sex",'email','tel','addTime','descn']

});

// 调用DWR取得数据

infoManager.getResult(function(data){

    store.loadData(data);

});

var grid = newExt.grid.GridPanel({

    renderTo:'grid',

    store:store,

    cm:cm

});

注意,执行infoManager.getResult()函数时,DWR就会使用Ajax去后台取数据了,操作成功后调用我们定义的匿名回调函数。在这里我们只做一件事,那就是将返回的data直接注入到ds中。

DWR返回的data可以被JsonStore直接读取,我们需要设置对应的fields参数,以告诉JsonReader需要哪些属性。

在这里,EXTDWR两者之间没有任何关系,将它们任何一方替换掉都可以。实际上它们只是在一起运行,并没有整合。我们给出的这个示例也是说明了一种松耦合的可能性,实际操作中完全可以使用这种方式。

10.10.2DWRProxy

要结合使用EXTDWR,不需要对后台程序进行任何修改,可以直接让前后台数据进行交互。不过还要考虑很多细节,比如Grid分页、刷新、排序、搜索等常见的操作。EXT的官方网站上已经有人放上了DWRProxy,借助它可以让DWREXT连接得更加紧密。不过,需要在后台添加DWRProxy所需要的Java类,这可能不是最好的解决方案。但我们相信,通过对它的内在实现的讨论,我们可以有更多的选择和想象空间。

注意     这个DWRProxy.js一定要放在ext-base.jsext-all.js后面,否则会出错。

我们现在就用DWRProxy来实现一个分页的示例。除了准备好插件DWRProxy.js外,还要在后台准备一个专门用于分页的封装类。因为不仅要告诉前台显示哪些数据,还要告诉前台一共有多少条数据。现在我们来重点看一下ListRange.java,如下面的代码所示。

public classListRange {

    Object[]data;

    inttotalSize;

}

其实ListRange非常简单,只有两个属性:提供数据的data和提供数据总量的totalSize。再看一下InfoManager.java,为了实现分页,我们专门编写了一个getItems方法,代码如下所示。

public ListRange getItems(Map conditions) {

    intstart = 0;

    intpageSize = 10;

    intpageNo = (start / pageSize) + 1;

    try{

        start= Integer.parseInt(conditions.get("start").toString());

        pageSize= Integer.parseInt(conditions.get("limit").toString());

        pageNo= (start / pageSize) + 1;

    }catch (Exception ex) {

        ex.printStackTrace();

    }

    Listlist = infoList.subList(start, start + pageSize);

    returnnew ListRange(list.toArray(), infoList.size());

}

getItems()的参数是Map,我们从中获得需要的参数,比如startlimit。不过HTTP里的参数都是字符串,而我们需要的是数字,所以要对类型进行相应的转换。根据startlimit两个属性从全部数据中截取一部分,放进新建的ListRange中,然后把生成的ListRange返回给前台,于是一切都解决了。

重头戏要上演了,我们就要使用传说中的Ext.data.DWRProxy了,还有Ext.data.List- RangeReader。通过这两个扩展,EXT完全可以支持DWR的数据传输协议。实际上,这正是EXT要把数据和显示分离设计的原因,这样你只需要添加自定义的proxyreader,不需要修改EXT的其他部分,就可以实现从特定途径获取数据的功能。后台还是DWR,所以至少在Grid部分,我们可以很好地使用它们的结合,主要代码如下所示。

var store = new Ext.data.Store({

    proxy: new Ext.data.DWRProxy(infoManager.getItems, true),

    reader:new Ext.data.ListRangeReader({

        totalProperty:'totalSize',

        root:'data',

        id:'id'

    },info),

    remoteSort:true

});

与我们上面说的一样,我们修改了proxy,也修改了reader,其他地方都不需要进行修改,Grid已经可以正常运行了。需要提醒的是DWRProxy的用法,其中包括两个参数:第一个是dwr- Call,它把一个DWR函数放进去,它对应的是后台的getItems方法;第二个参数是paging- AndSort,这个参数控制DWR是否需要分页和排序。

ListRangeReader部分与后台的ListRange.java对应。totalProperty表示后台数据总数,我们通过它指定从ListRange中读取totalSize属性的值来作为后台数据总数。还需要指定root参数,以告诉它在ListRange中的数据变量的名称为data,随后DWRProxy会从ListRange中的data属性中获取数据并显示到页面上。如果不想使用我们提供的ListRange.java,也可以自己创建一个类,只要把totalPropertydata两个属性与之对应即可。

10.10.3DWRTreeLoader

我们现在来尝试一下让树形也支持DWR。有了前面的基础,整合DWRtree就更简单了。在后台,我们需要树形节点对应的TreeNode.java。目前,只要idtextleaf三项就可以了。

public class TreeNode {

    Stringid;

    Stringtext;

    booleanleaf;

}

id是节点的唯一标记,知道了id就能知道是在触发哪个节点了。text是显示的标题,leaf比较重要,它用来标记这个节点是不是叶子。

这里还是用异步树,TreeNodeManager.java里的getTree()方法将获得一个节点的id作为参数,然后返回这个节点下的所有子节点。我们这里没有限制生成的树形的深度,你可以根据自己的需要进行设置。TreeNodeManager.java的代码如下所示。

public List getTree(String id) {

    Listlist = new ArrayList();

    Stringseed1 = id + 1;

    Stringseed2 = id + 2;

    Stringseed3 = id + 3;

    list.add(newTreeNode(seed1, "" + seed1, false));

    list.add(newTreeNode(seed2, "" + seed2, false));

    list.add(newTreeNode(seed3, "" + seed3, true));

    returnlist;

}

上面的代码并不复杂,它实现的效果与在Java中使用List或数组是相同的,因为返回给前台的数据都是JSON格式的。前台使用JavaScript处理返回信息的部分更简单,先引入DWRTree- Loader.js,然后把TreeLoader替换成DWRTreeLoder即可,如下面的代码所示。

var tree = new Ext.tree.TreePanel('tree', {

    loader:new Ext.tree.DWRTreeLoader({dataUrl: treeNodeManager.getTree})

});

参数依然是dataUrl,它的值treeNodeManager.getTree代表的是一个DWR函数,我们不需要对它进行深入研究,它的内部会自动处理数据之间的对应关系。DWR有时真的很方便。

10.10.4DWRProxyComboBox

DWRProxy既然可以用在Ext.data.Store中,那么它也可以为ComboBox服务,如代码清单10-8所示。

代码清单10-8DWRProxyComboBox整合

var info = Ext.data.Record.create([

    {name:'id', type: 'int'},

    {name:'name', type: 'string'}

]);

var store = newExt.data.Store({

    proxy:new Ext.data.DWRProxy(infoManager.getItems, true),

    reader:new Ext.data.ListRangeReader({

        totalProperty:'totalSize',

        root:'data',

        id:'id'

    },info)

});

var combo = newExt.form.ComboBox({

    store:store,

    displayField:'name',

    valueField:'id',

    triggerAction:'all',

    typeAhead:true,

    mode:'remote',

    emptyText:'请选择',

    selectOnFocus:true

});

combo.render('combo');

我们既可以用mode:'remote'triggerAction:'all'在第一次选择时读取数据,也可以设置mode:'local',然后手工操作store.load()并读取数据。

DWR要比Json-lib方便得多,而且DWR返回的数据可以直接作为JSON使用,使用Json-lib时还要面对无休无止的循环引用。

这次的示例稍微复杂一些,因为包括依赖jar包、classXMLJSP,所以示例单独放在10.store/dwr2/下,请将它们复制到tomcatwebapps下,然后再使用浏览器访问。

10.11localXHR支持本地使用Ajax

Ajax是不能在本地文件系统中使用的,必须把数据放到服务器上。无论是IISApache Tomcat,还是你熟悉的其他服务器,只要支持HTTP协议,就可以使用EXT中的Ajax

至于本地为何不能用Ajax,主要是因为Ajax要判断HTTP响应返回的状态,只有status=200时才认为这次请求是成功的。所以,localXHR做的就是强行修改响应状态,让Ajax可以继续下去。

下面我们来分析一下localXHR的源代码。

q   加入了一个forceActiveX属性,默认是false,它用来控制是否强制使用activexactivex是在IE下专用的。

q   修改createXhrObject函数,只是在最开始处加了一条判断语句,如下所示。

  if(Ext.isIE7 && !!this.forceActiveX){throw("IE7forceActiveX");}              

q   增加了getHttpStatus函数,这是为了处理HTTP的响应状态,如代码清单10-9所示。

代码清单10-9 处理HTTP响应状态

getHttpStatus: function(reqObj){

    var statObj = {

        status:0

        ,statusText:''

        ,isError:false

        ,isLocal:false

        ,isOK:false

    };

    try {

        if(!reqObj)throw('noobj');

        statObj.status= reqObj.status || 0;

        statObj.isLocal= !reqObj.status && location.protocol == "file:" ||

                           Ext.isSafari&& reqObj.status == undefined;

        statObj.statusText= reqObj.statusText || '';

        statObj.isOK= (statObj.isLocal ||

                        (statObj.status> 199 && statObj.status < 300) ||

                         statObj.status== 304);

    } catch(e){

        //status may not avail/valid yet.

        statObj.isError= true;

    }

    return statObj;

},

它为状态增添了更多语义,status表示状态值,statusText表示状态描述,isError表示是否有错误,isLocal表示是否在本地进行Ajax访问,isOK表示操作是否成功。

判断isLocal是否为本地的有两种方法:reqObj没有status,而且请求协议是file:;浏览器是Safari,而且reqObj.status没有定义。

statObj中的isOK属性用来判断此次请求是否成功。判断请求是否成功的条件很多,例如:isLocal的属性为true、响应状态值在199~300之间、响应状态值是304等。如果处理过程中出现了异常,就会将isError属性设置为true,最后会把配置好的statObj对象返回,等待下一个步骤的处理。

localXHR.jshandleTransactionResponse函数进行了简化。因为增加的getHttpStatus函数很好地封装了与请求相关的各种状态信息,所以在handleTransactionResponse函数中我们不会看到让人头晕目眩的响应状态代码。取而代之的是isErrorisOK这些更容易理解的属性,localXHR.js直接使用这些属性来处理响应。

createResponseObject函数被大大强化了。其实前半部分都是一样的,localXHR.js中对isLocal做了大量的处理,响应中的responseText可以从连接中获得。如果需要XML,它就使用ActiveXObject("Microsoft.XMLDOM")new DOMParser()responseText解析成XML放到response里,响应状态也是重新计算的,这样就能让Ajax正常调用了。

最后处理的是asyncRequest函数,如果在异步请求时出现异常,就调用handleTransac- tionResponse返回响应,然后根据各种情况稍微修改header属性。

我们来看看下面这行代码:

Ext.lib.Ajax.forceActiveX= (document.location.protocol == 'file:');              

如果协议是file:,就强制使用activex

10.12 本章小结

本章系统地讨论了Ext.data包中的各个类的功能和使用方式,还涉及如何将EXTDWR通过自定义的proxy相结合的示例。我们介绍了如何使用Ext.data.Connection与后台进行数据交互,还专门介绍了它的子类Ext.Ajax,并讨论了EXTAjax的应用以及在回调函数中使用scopecreateDelegate()解决this的问题。

接着详细介绍了类Ext.data.RecordExt.data.Store的功能和使用方法,这两个类结合起来形成了Ext.data中的主体数据模型,很多组件(包括GridComboBox)都是建立在它们之上的。除此之外,还讨论了常用的proxyreaderstoreSimpleStoreJsonStore,以及它们的应用场景。

最后我们介绍了扩展插件localXHR.js,它可以解决EXTAjax无法访问本地文件的问题。

你可能感兴趣的:(Extjs 数据的传输和存储)