Java™ Desktop 的再介绍”强调了今年的 JavaOne 大会。对于那些抱怨 Swing 太慢、太难使用、界面太难看的开发人员来说,Swing 和 GUI 开发所做的更新努力,并没有带来什么受人欢迎的好消息。如果您最近没有用过 Swing,那么您会很高兴听到其中的许多问题已经得到解决。Swing 被重新设计,它能执行得更好,并能更好地利用 Java 2D API。Swing 的开发者在 1.4 版甚至最新发布的 5.0 版中提高了外观支持。Swing 从没像现在这么好过。
如果以前曾经用过 JTable,那么您可能也同时被迫使用了 TableModel。您可能还注意到,每个 TableModel 中的所有代码,与其他 TableModel 中的代码几乎是一样的,在编译的 Java 类中,有差异的代码实际上是不存在的。本文将分析 TableModel/JTable 目前的设计方法,说明这种设计的不足,展示为什么它没有实现模型-视图-控制器(MVC)模式的真正目标。您将看到框架和构成 TMF 框架的代码 —— 我以前编写的代码与最常用的开放源代码项目的组合。使用该框架,开发人员可以把 TableModel 的大小从数百行代码减少到只有区区一行,并把重要的表信息放在外部 XML 文件中。在读完本文之后,只使用如下所示的一行代码,您就可以管理您的 JTable 数据:
1 TableUtilities.setViewToModel("tableconfig.xml", "My Table",
2 myJTable, CollectionUtilities.observableList(myData));
3
JTable 和 TableModel 存在的 MVC 问题
MVC 已经成为非常流行的 UI 设计模式,因为它把业务逻辑清晰地从数据的视图中分离了出来。Struts 是 MVC 在 Web 上应用的一个非常好的例子。最初,Swing 最大的一个卖点是它采用了 MVC,将视图从模型中分离了出来,代码背后的想法是:代码的模块化程度足够高,所以,不用修改模型中的任何代码,就可以分离出视图。我想,任何用过 JTables 和 TableModels 的人都会发笑,告诉您这是绝对不可能的。使用 MVC 设计模式的理想情况是,在开发人员用 JList 或 JComboBox 替换 JTable 时,可以不用修改表示数据的模式中的代码。但是,在 Swing 中做不到这点。Swing 使得把 JTable、 JList 和 JComboBox 热交换到应用程序中成为不可能,即使所有这三个组件都是用来为相同的数据模型提供视图。对于 Swing 中的 MVC 设计,这是一个严重的不足。如果您想为 JTable 交换 JList,就必须重写视图背后的全部代码,才能实现该交换。
JTable/TableModel 的另一个 MVC 缺陷是:模型变化的时候,视图不会更新自身。开发人员必须保持对模型的引用,并调用一个函数,这样模型才会告诉视图对自身进行更新;但是,理想的情况应当是:不需要任何额外的代码,就能实现自动更新。
最后,JTable 和 TableModel 组件设计的问题是,它们彼此之间缠杂得过于密切。如果您修改了 JTable 中的代码,那么您需要确保您没有破坏负责处理的 TableModel,反之亦然。对于一个被认为是在模块化基础上建立的设计模式来说,目前的实现显然是一种存在过多依赖关系的设计。
TMF 框架更好地遵循了 MVC 的目标,它把 JTable 中视图和模型的工作更加清晰地分离开来。虽然它还没有达到让组件能够热切换的更高目标,但是它已经在正确方向上迈出了一步。
让我们来检视 TMF 框架,看看它是如何让传统 TableModel 过时的。设计该框架的第一部分是学习 JTable 的使用 —— 开发人员如何使用它,它显示了什么内容,以便了理解哪些东西可以内化、通用化,哪些应当保留可配置状态,以便开发人员配置。对于 TableModel,也要进行同样的思考,我必须确定哪些东西可以从代码中移出,哪些必须留在代码中。一旦找出这些问题,接下来要做的就是确定能够让代码足够通用的最佳技术,以便所有人都能使用它,但是,还要让代码具备足够的可配置性,这也是为了让每个人都能使用它。
该框架分成三个基本部分:一个能够处理任何类型数据的通用 TableModel、一个外部 XML 文件(负责对不同表中不同的表内容进行配置),以及模型与视图之间的桥。
在本文中,您可以在 src 文件夹中找到文中介绍的所有源代码。特定于 TMF 的代码位于 com.ibm.j2x.swing.table 包中。
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 设置表信息
1 protected List mapValues = new ArrayList();
2 protected LinkedMap columnInfo = new LinkedMap();
3
4 protected void initializeValues(Collection values)
5 {
6 List listValues = new ArrayList(values);
7 mapValues.clear();
8 for (Iterator i=listValues.iterator(); i.hasNext();)
9 {
10 mapValues.add(new BeanMap(i.next()));
11 }
12 }
在 BeanTableModel 中比较有趣的检查代码实际上是通用 TableModel 的那一部分,这部分代码扩展了 AbstractTableModel 。将清单 2 中的代码与您通常用来建立传统 TableModel 的代码进行比较,您可以看到一些类似之处。
清单 2. BeanTableModel 中的通用 TableModel 代码
1 /**
2 * Returns the number of BeanMaps, therefore the number of JavaBeans
3 */
4 public int getRowCount()
5 {
6 return mapValues.size();
7 }
8 /**
9 * Returns the number of key-value pairings in the column LinkedMap
10 */
11 public int getColumnCount()
12 {
13 return columnInfo.size();
14 }
15
16 /**
17 * Gets the key from the LinkedMap at the specified index (and a
18 * good example of why a LinkedMap is needed instead of a HashMap)
19 */
20 public String getColumnName(int col)
21 {
22 return columnInfo.get(col).toString();
23 }
24 /**
25 * Gets the class of the column. A lot of developers wonder what
26 * this is even used for. It is used by the JTable to use custom
27 * cell renderers, some of which are built into JTables already
28 * (Boolean, Integer, String for example). If you write a custom cell
29 * renderer it would get loaded by the JTable for use in display if that
30 * specified class were returned here.
31 * The function uses the BeanMap to get the actual value out of the
32 * JavaBean and determine its class. However, because the BeanMap
33 * autoboxes things -- it converts the primitives to Objects for you
34 * (e.g. ints to Integers) -- the code needs to unautobox it, since the
35 * function must return a Class Object. Thus, it recognizes any primitives
36 * and converts them to their respective Object class.
37 */
38 public Class getColumnClass(int col)
39 {
40 BeanMap map = (BeanMap)mapValues.get(0);
41 Class c = map.getType(columnInfo.getValue(col).toString());
42 if (c == null)
43 return Object.class;
44 else if (c.isPrimitive())
45 return ClassUtilities.convertPrimitiveToObject(c);
46 else
47 return c;
48 }
责任编辑:小草