让我们来检视Swing TMF 框架,看看它是如何让传统 TableModel 过时的。设计该框架的第一部分是学习 JTable 的使用 —— 开发人员如何使用它,它显示了什么内容,以便了理解哪些东西可以内化、通用化,哪些应当保留可配置状态,以便开发人员配置。对于 TableModel,也要进行同样的思考,我必须确定哪些东西可以从代码中移出,哪些必须留在代码中。一旦找出这些问题,接下来要做的就是确定能够让代码足够通用的最佳技术,以便所有人都能使用它,但是,还要让代码具备足够的可配置性,这也是为了让每个人都能使用它。
该框架分成三个基本部分:一个能够处理任何类型数据的通用 TableModel、一个外部 XML 文件(负责对不同表中不同的表内容进行配置),以及模型与视图之间的桥。
com.ibm.j2x.swing.table.BeanTableModel
BeanTableModel 是框架的第一部分。它充当的是通用 TableModel ,您可以用它来处理任何类型的数据。我知道,您可能会说,“您怎么这么肯定它适用于所有的数据呢?”确实,很明显,我不能这么肯定,而且实际上,我确信有一些它不适用的例子。但是从我使用 JTables 的经验来说,我愿意打赌(即使看起来我有点抬杠),实际使用中的 JTables,99% 都是用来显示数据对象列表(也就是说,JavaBeans 组件的 ArrayList)。基于这个假设,我建立了一个通用表模型,它可以显示任何数据对象列表,它就是 BeanTableModel。
BeanTableModel 大量使用了 Java 的内省机制,来检查 bean 中的字段,显示正确的数据。它还使用了来自 Jakarta Commons Collections 框架的两个类来辅助设计。
在我深入研究代码之前,请让我解释来自类的几个概念。因为我可以在 bean 上使用内省机制,所以我需要了解 bean 本身的信息,主要是了解字段的名称是什么。我可以通过普通的内省机制来完成这项工作:我可以检查 bean ,找出其字段。但是,对于表来说,这还不够好,因为多数开发人员想让他们的表按照指定顺序显示字段。除此之外,还有一项表需要的信息,我无法通过内省机制从 bean 中获得,即列名消息。所以,为了获得正确显示,对于表中的每个列,您需要两条信息:列名和将要显示的 bean 中的字段。我用键-值对的格式表示该信息,其中,将列名用作键,字段作为值。
正因为如此,我在这里使用了来自 Collections 框架的适合这项工作的两个类。 BeanMap 用作实用工具类,负责处理内省机制,它接手了内省机制的所有繁琐工作。普通的内省机制开发需要大量的 try / catch 块,对于表来说,这是没有必要的。 BeanMap 把 bean 作为输入,像处理 HashMap 那样来处理它,在这里,键是 bean 中的字段(例如, firstName ),值是 get 方法(例如, getFirstName() )的结果。BeanTableModel 广泛地运用 BeanMap ,消除了操作内省机制的麻烦,也使得访问 bean 中的信息更加容易。
LinkedMap 是另外一个在 BeanTableModel 中全面应用的类。我们还是回到为列名-字段映射所进行的键-值数据设置,对于数据对象来说,很明显应当选择 HashMap。但是,HashPap 没有保留插入的顺序,对于表来说,这是非常重要的一部分,开发人员希望在每次显示表的时候,都能以指定的顺序显示列。这样,插入的顺序就必须保留。解决方案是 LinkedMap ,它是 LinkedList 与 HashMap 的组合,它既保留了列,也保留了列的顺序信息。参见清单 1,可以查看我是如何用 LinkedMap 和 BeanMap 来设置表的信息的。
清单1. 用 LinkedMap 和 BeanMap 设置表信息
- protected List mapValues = new ArrayList();
- protected LinkedMap columnInfo = new LinkedMap();
- protected void initializeValues(Collection values)
- {
- List listValues = new ArrayList(values);
- mapValues.clear();
- for (Iterator i=listValues.iterator(); i.hasNext();)
- {
- mapValues.add(new BeanMap(i.next()));
- }
- }
在 BeanTableModel 中比较有趣的检查代码实际上是通用 TableModel 的那一部分,这部分代码扩展了 AbstractTableModel 。将清单 2 中的代码与您通常用来建立传统 TableModel 的代码进行比较,您可以看到一些类似之处。
清单 2. BeanTableModel 中的通用 TableModel 代码
- /**
- *ReturnsthenumberofBeanMaps,thereforethenumberofJavaBeans
- */
- publicintgetRowCount()
- {
- returnmapValues.size();
- }
- /**
- *Returnsthenumberofkey-valuepairingsinthecolumnLinkedMap
- */
- publicintgetColumnCount()
- {
- returncolumnInfo.size();
- }
- /**
- *GetsthekeyfromtheLinkedMapatthespecifiedindex(anda
- *goodexampleofwhyaLinkedMapisneededinsteadofaHashMap)
- */
- publicStringgetColumnName(intcol)
- {
- returncolumnInfo.get(col).toString();
- }
- /**
- *Getstheclassofthecolumn.Alotofdeveloperswonderwhat
- *thisisevenusedfor.ItisusedbytheJTabletousecustom
- *cellrenderers,someofwhicharebuiltintoJTablesalready
- *(Boolean,Integer,Stringforexample).Ifyouwriteacustomcell
- *rendereritwouldgetloadedbytheJTableforuseindisplayifthat
- *specifiedclasswerereturnedhere.
- *ThefunctionusestheBeanMaptogettheactualvalueoutofthe
- *JavaBeananddetermineitsclass.However,becausetheBeanMap
- *autoboxesthings--itconvertstheprimitivestoObjectsforyou
- *(e.g.intstoIntegers)--thecodeneedstounautoboxit,sincethe
- *functionmustreturnaClassObject.Thus,itrecognizesanyprimitives
- *andconvertsthemtotheirrespectiveObjectclass.
- */publicClassgetColumnClass(intcol)
- {
- BeanMapmap=(BeanMap)mapValues.get(0);
- Classc=map.getType(columnInfo.getValue(col).toString());
- if(c==null)
- returnObject.class;
- elseif(c.isPrimitive())
- returnClassUtilities.convertPrimitiveToObject(c);
- else
- returnc;
- }
- /**
- *TheBeanTableModelautomaticallyreturnsfalse,andifyou
- *needtomakeaneditabletable,you'llhavetosubclass
- *BeanTableModelandoverridethisfunction.
- */
- publicbooleanisCellEditable(introw,intcol)
- {
- returnfalse;
- }
- /**
- *ThefunctionthatreturnsthevaluethatyouseeintheJTable.Itgets
- *theBeanMapwrappingtheJavaBeanbasedontherow,itusesthe
- *columnnumbertogetthefieldfromthecolumninformationLinkedMap,
- *andthenusesthefieldtoretrievethevalueoutoftheBeanMap.
- */
- publicObjectgetValueAt(introw,intcol)
- {
- BeanMapmap=(BeanMap)mapValues.get(row);
- returnmap.get(columnInfo.getValue(col));
- }
- /**
- *TheoppositefunctionofthegetValueAt--itduplicatestheworkofthe
- *getValueAt,butinsteadputstheObjectvalueintotheBeanMapinstead
- *ofretrievingitsvalue.
- */
- publicvoidsetValueAt(Objectvalue,introw,intcol)
- {
- BeanMapmap=(BeanMap)mapValues.get(row);
- map.put(columnInfo.getValue(col),value);
- super.fireTableRowsUpdated(row,row);
- }
- /**
- *TheBeanTableModelimplementstheCollectionListenerinterface
- *(1ofthe3partsoftheframework)andthuslistensforchangesinthe
- *dataitismodelingandautomaticallyupdatestheJTableandthe
- *modelwhenachangeoccurstothedata.
- */
- publicvoidcollectionChanged(CollectionEvente)
- {
- initializeValues((Collection)e.getSource());
- super.fireTableDataChanged();
- }
正如您所看到的,BeanTableModel 的整个 TableModel 足够通用化,可以在任何表中使用。它充分利用了内省机制,省去了所有特定于 bean 的编码工作,在传统的 TableModel 中,这类编码工作绝对是必需的 —— 同时也是完全冗余的。BeanTableModel 还可以在 TMF 框架之外使用,虽然在外面使用会丧失一些威力和灵活性。
#p#
看过这段代码之后,您会提出两个问题。首先,BeanTableModel 从哪里获得列名-字段与键-值配对的信息?第二,到底什么是 ObservableCollection ?这些问题会将我们引入框架的接下来的两个部分。这些问题的答案以及更多的内容,将在本文后面接下来的章节中出现。
Swing Castor XML 解析器
保存必需的列名-字段信息的最合理的位置位于 Java 类之外,这样,不需要再重新编译 Java 代码,就可以修改这个信息。因为关于列名和字段的信息是 TMF 框架中惟一明确与表有关的信息,这意味着整个表格都可以在外部进行配置。
显然,该解决方案会自然而然把 XML 作为配置文件的语言选择。配置文件必须为多种表模型保存信息;您还需要能够用这个文件指定每个列中的数据。配置文件还应当尽可能地易于阅读,因为开发人员之外的人员有可能要修改它。
这些问题的最佳解决方案是 Castor XML 解析器。查看 Castor 实际使用的最佳方法就是查看如何在框架中使用它。
让我们来考虑一下配置文件的目的:保存表模型和表中列的信息。 XML 文件应当尽可能简单地显示这些信息。TMF 框架中的 XML 文件用清单 3 所示的格式来保存表模型信息。
清单3. TMF 配置文件示例
- <model>
- <className>demo.hr.TableModelFreeExample< SPAN>className>
- <name>Hire< SPAN>name>
- <column>
- <name>First Name< SPAN>name>
- <field>firstName< SPAN>field>
- < SPAN>column>
- <name>Last Name< SPAN>name>
- <field>lastName< SPAN>field>
- < SPAN>column>
- < SPAN>model>
与这个目的相反的目标是,开发人员必须处理的 Java 对象应当像 XML 文件一样容易理解。通过 Castor XML 解析器用来存储http://storage.it168.com/" target=_blank>存储列信息的三个 Java 对象,就可以看到这一点,这三个对象是: TableData (存储文件中的所有表模型)、 TableModelData (存储特定于表模型的信息)和 TableModelColumnData (存储列信息)。这三个类提供了 Java 开发人员所需的所有包装器,以便得到有关 TableModel 的所有必要信息。
将所有这些包装在一起所缺少的一个环节就是 映射文件,它是一个 XML 文件,Castor 用它把简单的 XML 映射到简单的 Java 对象中。在完美的世界中,映射文件也应当很简单,但事实要比这复杂得多。良好的映射文件要使别的一切东西都保持简单;所以一般来说,映射文件越复杂,配置文件和 Java 对象就越容易处理。映射文件所做的工作顾名思义就是把 XML 对象映射到 Java 对象。清单 4 显示了 TMF 框架使用的映射文件。
清单 4. TMF 框架使用的 Castor 映射文件
- xmlversionxmlversion="1.0"?>
- <mapping>
- <description>Amappingfileforexternalizedtablemodels< SPAN>description>
- <classnameclassname="com.ibm.j2x.swing.table.TableData">
- <map-toxmlmap-toxml="data"/>
- <fieldnamefieldname="tableModelData"collection="arraylist"type=
- "com.ibm.j2x.swing.table.TableModelData">
- <bind-xmlnamebind-xmlname="tableModelData"/>
- < SPAN>field>
- < SPAN>class>
- <classnameclassname="com.ibm.j2x.swing.table.TableModelData">
- <map-toxmlmap-toxml="model"/>
- <fieldnamefieldname="className"type="string">
- <bind-xmlnamebind-xmlname="className"/>
- < SPAN>field>
- <fieldnamefieldname="name"type="string">
- <bind-xmlnamebind-xmlname="name"/>
- < SPAN>field>
- <fieldnamefieldname="columns"collection="arraylist"type=
- "com.ibm.j2x.swing.table.TableModelColumnData">
- <bind-xmlnamebind-xmlname="columns"/>
- < SPAN>field>
- < SPAN>class>
- <classnameclassname="com.ibm.j2x.swing.table.TableModelColumnData">
- <map-toxmlmap-toxml="column"/>
- <fieldnamefieldname="name"type="string">
- <bind-xmlnamebind-xmlname="name"/>
- < SPAN>field>
- <fieldnamefieldname="field"type="string">
- <bind-xmlnamebind-xmlname="field"/>
- < SPAN>field>
- < SPAN>class>
- < SPAN>mapping>
仅仅通过观察这段代码,您就可以看出,映射文件清晰地勾划出了每个用来存储表模型信息的类,定义了类的类型,并将 XML 文件中的名称连接到了 Java 对象中的字段。请保持相同的名称,这样会让事情简单、更好管理一些,但是没必要保持名称相同。
到现在为止,列名和字段信息都已外部化,可以读入包含列信息的 Java 对象中,并且可以很容易地把信息发送给 BeanTableModel,并用它来设置列。
Swing ObservableCollection
TMF 框架的最后一个关键部分,就是 ObservableCollection 。您们当中的某些人可能熟悉 ObservableCollection 的概念,它是 Java Collections 框架的一个成员,在被修改的时候,它会抛出事件,从而允许其侦听器根据这些事件执行操作。虽然从来没有将它引入 Java 语言的正式发行版中,但在 Internet 上,这个概念已经有了一些第三方实现。就本文而言,我使用了自己的 ObservableCollection 实现,因为框架只需要一些最基本的功能。我的实现使用了一个称为 collectionChanged() 的方法,每次发生修改时, ObservableCollection 都会在自己的侦听器上调用该方法。也可以将该用法称为 Collection 类的 Decorator(有关 Collections 的 Decorator 更多信息,请参阅 Collections 框架的站点),只需要增加几行代码,您就可以在普通的 Collection 类中创建 Collection 类的 Observable 实例。清单 5 显示了 ObservableCollection 用法的示例。(这只是一个示例,没有包含在 j2x.zip 中。)
清单 5. ObservableCollection 用法示例
- convert a normal list to an ObservableList
- ObservableList oList = CollectionUtilities.observableList(list);
- // A listener could then register for events from this list by calling
- oList.addCollectionListener(this);
- // trigger event
- oList.add(new Integer(3));
- // listener receives event
- public void collectionChanged(CollectionEvent e)
- {
- // event received here}
ObservableCollection 有许多 TMF 框架之外的应用程序。如果您决定采用 TMF 框架,您会发现,在开发代码期间, ObservableCollection 框架有许多实际的用途。
但是,它在 TMF 框架中的用途,重点在于它能更好地定义视图和模型之间的关系,当数据发生变化时,可以自动更新视图。您可以回想一下,这正是传统 TableModel 的最大限制,因为每当数据发生变化时,都必须用表模型的引用来更新视图。而在 TMF 框架中使用 ObservableCollection 时,当数据发生变化时,视图会自动更新,不需要维护一个到模型的引用。在 BeanTableModel 的 collectionChanged() 方法的实现中,您可以看到这一点。
Swing TableUtilities
在该框架中执行的最后一步操作,是将所有内容集成到一些实用方法中,让 TMF 框架使用起来简单明了。这些实用方法可以在 com.ibm.j2x.swing.table.TableUtilities 类中找到,该类提供了您将需要的所有辅助函数:
getColumnInfo() :该实用方法用 Castor XML 文件解析指定的文件,并返回指定表模型的所有列信息,返回的形式是 BeanTableModel 所需的 LinkedMap 。当开发人员选择从 BeanTableModel 中派生子类时,这个方法很重要。
getTableModel() :该实用方法是建立在上面的 getColumnInfo() 方法之上,它获得列的信息,然后把信息传递给 BeanTableModel,返回已经设置好所有信息的 BeanTableModel。
setViewToModel() :该实用方法是最重要的函数,也是 TMF 框架的主要吸引人的地方。它也是建立在 getTableModel() 方法之上,也有一个到 JTable 的引用(JTable 中有这个表的模型),以及一个到数据(要在表中显示)的引用。它对 JTable 上的 TableModel 进行设置,并把数据传递给 TableModel,结果是:只需一行代码,就为 JTable 完成了 TableModel 的设置。TMF 框架在该方法上得到了最佳印证,TableModel 将永远地被下面这个简单的方法所代替:
- TableUtilities.setViewToModel("table_config.xml", "Table", myJTable, myList);
【编辑推荐】