本文说的集合简单指多个具有同样结构的数据实体的总和,比如视图的部分或所有文档,查询数据库得到的记录集,一组模型(model)对象等等。在web应用程序里通常用表格来显示这样一个集合。作为基于组件的web框架,XPages提供了几种标准控件来显示这样一个集合:Repeat、View和Data Table。View用于显示一个Notes视图;Data Table以一个表格的形式显示一个集合;Repeat控件最灵活,可以自定义重复的数据显示的形式。深入地看,三者在机制上有很多共同点。它们都是通过value属性绑定到一个数据对象,这个数据对象或是一个DataModel对象,或可以被接合(adapt)成一个DataModel对象。DataModel则是JSF里支持对子组件逐行处理的组件(如UIData)数据绑定的数据源的抽象类【注1】。在JDK里,DataModel有六个直接子类: ArrayDataModel,CollectionDataModel, ListDataModel, ResultDataModel, ResultSetDataModel,ScalarDataModel,也意味着有六类对象可以被接合为一个DataModel:Array、Collection、List、javax.servlet.jsp.jstl.sql.Result、java.sql.ResultSet和一个单独的Java对象。至此我们就清楚地知道XPages自带的Repeat、View和Data Table三种控件可以展示怎样的集合。
我们知道用View控件显示一个Notes视图时,可以用value和data两个属性任意一个来绑定数据源。设置value属性在后台对应com.ibm.xsp.component.xp.XspViewPanel(View控件的Java类)【注2】继承自com.ibm.xsp.component.UIDataEx的setValue(java.lang.Object value)方法,它又是覆盖javax.faces.component.UIData的setValue方法。设置data属性对应com.ibm.xsp.component.xp.XspViewPanel的setData(com.ibm.xsp.model.DataSource data)方法。无论是哪一种,最后都绑定到Notes视图的包装类com.ibm.xsp.model.domino.DominoViewData【注3】
Data Table控件的情况简单些,绑定数据时使用value属性,对应Java类com.ibm.xsp.component.xp.XspDataTableEx同样继承自继承自com.ibm.xsp.component.UIDataEx。
Repeat控件的value属性直接来自Java类com.ibm.xsp.component.UIRepeat的setValue(java.lang.Object value)方法。
各种数据源和一个实例
除了View控件是为Notes视图量身定做的,Data Table和Repeat控件适合展示各种类型的集合数据,在上面提到的六种可接合成DataModel的对象中,javax.servlet.jsp.jstl.sql.Result和java.sql.ResultSet宜用于显示关系型数据库的记录,Array、Collection、List和单独的Java对象则可用于业务逻辑遇到的各种模型数据。下面就用一个实例来演示Repeat控件绑定到一个数组数据源。
样例的派车应用程序介绍:公司在毗邻的几个城市设有办公室,部分员工需要往返各个办公室工作。公司提供若干车辆负责接送员工。需乘车前往异地的员工提前确定行程。公司希望设计一个系统,让员工自行填写乘车申请,负责派车的专员根据申请定时制定出每日的乘车安排,并通知给相关员工。
这个系统的核心是按照员工的乘车申请和可用的汽车和司机资源,自动生成每天的乘车安排表,并可依据临时或特殊情况做手动修改。乘车申请、汽车和司机的资料维护都是一般的新建、修改、查找和删除文档的工作。自动生成的乘车安排表在数据设计上对应一个主文档,表里的每一行(包括司机、汽车、目的地和乘客)可以设计成文档里的一组字段,不过对应一个派车子文档更加灵活和结构清晰。表单、视图和生成主文档子文档的程序不在这里讨论。我们来看看怎样在一个界面上展现乘车安排表。因为涉及到数量不定且可以实时修改的派车文档,如果设计成Notes客户端应用程序,在乘车安排的表单上要么使用隐藏公式来控制一个表格显示和编辑各行派车数据,要么嵌入视图。前者的表格复杂不易维护,而且只适用于以字段组代表派车数据的方案,若选用派车子文档,很难实现界面与数据之间的方便而准确的同步。后者也有多种障碍,首先不方便在主表单的界面里编辑包含派车数据的子文档(Notes视图的InViewEdit功能不好用),需要打开子文档,在另一个窗口编辑;其次主表单和嵌入视图相互隔绝,读取不到对方的信息,以致添加和删除子文档的按钮置于任何一方都难敷应用:放在主表单处,得不到嵌入式图中选中文档的句柄;置于视图上,无法根据主文档的字段设置隐藏公式。设计成web系统,页面可以动态修改,有更大的灵活性。不过如果是传统的Notes web开发,在显示派车数据的表格和承载它们的文档之间双向交流仍然很麻烦。XPages界面与数据分离的策略解决了这个难题。用Repeat控件就可以实现一个绑定到子文档集合的动态的表格。效果如下两图。乘车安排表的主体是一个包含司机、车辆、目的地和乘客四列的表格。在自动生成的安排表上,用户可以修改每一行的内容,也可以新增全新的一行或删除某行。
一张自动生成的乘车安排表
新增一行
这个XPage在设计视图如下:
展示派车数据的表格的代码如下:
<xp:table style="width:90%;margin-left:5%" rendered="#{javascript:sessionScope.get('showDetails')||!document1.isNewNote();}"> <xp:tr> <xp:td style="width:20%"> <xp:span style="font-weight:bold">Driver</xp:span> </xp:td> <xp:td style="font-weight:bold;width:25%">Vehicle</xp:td> <xp:td style="font-weight:bold;width:15%"> Destination/Route </xp:td> <xp:td style="font-weight:bold">Passengers</xp:td> </xp:tr> <xp:tr> <xp:td colspan="4"> <xp:repeat id="repeat1" rows="30" var="row" indexVar="index"> <xp:this.value><![CDATA[#{javascript:var ids=[]; var dc=document1.getDocument().getResponses(); if (dc===null){ return ids; } var doc=dc.getFirstDocument(); while (doc !== null){ ids.push(doc.getNoteID()); doc=dc.getNextDocument(doc); } return ids;}]]></xp:this.value> <xp:panel> <xp:this.data> <xp:dominoDocument var="docDetail" action="editDocument" formName="Detail" documentId="#{javascript:row}" computeWithForm="onsave" ignoreRequestParams="true"> </xp:dominoDocument> </xp:this.data> <xp:table style="width:100%"> <xp:tr> <xp:td style="width:20%"> <xp:comboBox id="comboBox1" value="#{docDetail.Driver}"> <xp:selectItem itemLabel="Please select"> </xp:selectItem><xp:selectItems id="selectItems1"> <xp:this.value><![CDATA[#{javascript:getSetting('Driver');}]]></xp:this.value> </xp:selectItems> </xp:comboBox> </xp:td> <xp:td style="width:20%"> <xp:comboBox id="comboBox2" value="#{docDetail.Vehicle}"> <xp:selectItem itemLabel="Please select"> </xp:selectItem><xp:selectItems id="selectItems2"> <xp:this.value><![CDATA[#{javascript:getSetting('Vehicle');}]]></xp:this.value> </xp:selectItems> </xp:comboBox> </xp:td> <xp:td style="width:15%"> <xp:inputText id="inputText1" value="#{docDetail.Destination}"> </xp:inputText> </xp:td> <xp:td style="width:35%"> <xp:inputText id="inputText2" value="#{docDetail.Passengers}" style="width:95%"> </xp:inputText> </xp:td> <xp:td rendered="#{javascript:document1.isEditable();}"> <xp:button value="Delete" id="button3"> <xp:eventHandler event="onclick" submit="true" refreshMode="complete" id="eventHandler1"> <xp:this.action><![CDATA[#{javascript:docDetail.getDocument().remove(true);}]]></xp:this.action> </xp:eventHandler> </xp:button> </xp:td> </xp:tr> </xp:table> </xp:panel> </xp:repeat> </xp:td> </xp:tr> </xp:table>外层的表格第一行显示了四个列的标题,第二行唯一的单元格里放置了一个Repeat控件,它的value属性用SSJS计算,为当前文档的子文档的NoteID组成的数组。Repeat控件下只有一个子组件,就是一个Panel,它又包装了代表一行派车数据的一个表格。这样做是因为Panel控件可以包含独立的数据源,在这里即是一个派车子文档,这个xp:dominoDocument由设置documentId为Repeat控件的var属性row获得。这个代表一行派车数据的表格随每一个派车子文档重复一次,它只有简单的一行四列,对应着外层表格的四列。每列依据包容的字段的类型,设有一个组合框或编辑框控件。
再来看看添加一行和删除一行的代码,它们分别位于页面上的Add Vehicle和表格里每一行末尾的Delete按钮里:
<xp:button value="Add Vehicle" id="button4" disabled="#{javascript:document1.isNewNote();}"> <xp:eventHandler event="onclick" submit="true" refreshMode="complete"> <xp:this.action><![CDATA[#{javascript:var doc=database.createDocument(); doc.appendItemValue('Form', 'Detail'); doc.makeResponse(document1.getDocument()); doc.computeWithForm(false, false); doc.save(); }]]></xp:this.action> </xp:eventHandler> </xp:button> <xp:button value="Delete" id="button3"> <xp:eventHandler event="onclick" submit="true" refreshMode="complete" id="eventHandler1"> <xp:this.action><![CDATA[#{javascript:docDetail.getDocument().remove(true);}]]></xp:this.action> </xp:eventHandler> </xp:button>
可以看出,代码很简单,分别只是为当前文档新增了一个子文档和删除一个子文档。其后,从数据到界面的转变就由XPages引擎自动完成。
最后,XPages里的Repeat、View和Data Table这三个控件在JSF的其它实现(implementation)里也有类似的组件。View和Data Table就和Java EE里的javax.faces.component.html.HtmlDataTable都继承自javax.faces.component.UIData,Repeat则在各种facelets技术的版本里都有,如org.apache.myfaces.view.facelets.component.UIRepeat。
注1:
原文为DataModel is an abstraction around arbitrary data binding technologies that can be used to adapt a variety of data sources for use by JavaServer Faces components that support per-row processing for their child components (such as UIData).
取自(http://docs.oracle.com/javaee/7/api/javax/faces/model/DataModel.html),可直译为DataModel是JSF里包装任意数据绑定技术的抽象类,这些数据绑定技术可被用于接合不同数据源,这些数据源被用于支持对子组件逐行处理的JavaServer Faces组件(如UIData)。虽然意识更贴近原文,但因为中文表达法的限制,这样的嵌套定语从句读起来有些拗口。
注2:
下面这些Java API可以在XPages Extension API(http://public.dhe.ibm.com/software/dw/lotus/Domino-Designer/JavaDocs/XPagesExtAPI/8.5.2/index.html)查到,XPages控件和模型(model)涉及到的类和接口之繁杂,在笔者看来是缺乏良好架构的明证,不知是因为XPages的设计人员经验不足、分工混乱还是时间仓促。以后还会专文论述。
com.ibm.xsp.component.xp.XspViewPanel
java.lang.Object
extended by javax.faces.component.UIComponent
extended by javax.faces.component.UIComponentBase
extended by javax.faces.component.UIData
extended by com.ibm.xsp.component.UIDataEx
extended bycom.ibm.xsp.component.UIViewPanel
extended bycom.ibm.xsp.component.xp.XspViewPanel
All Implemented Interfaces:
FacesAjaxComponent, FacesComponent, FacesDataIterator,FacesNestedDataTable, FacesRefreshableComponent, FacesRowIndex, ThemeControl,javax.faces.component.NamingContainer, javax.faces.component.StateHolder
注3:
com.ibm.xsp.model.domino.DominoViewData
java.lang.Object
extended by com.ibm.xsp.complex.ValueBindingObjectImpl
extended by com.ibm.xsp.model.AbstractDataSource
extended by com.ibm.xsp.model.AbstractViewDataSource
extended bycom.ibm.xsp.model.domino.DominoViewData
All Implemented Interfaces:
ComponentBindingObject, ValueBindingObject, DataPublishingObject, DataSource, ModelDataSource, ViewDataSource, javax.faces.component.StateHolder