技术开发 频道

用TableModelFree框架简化Swing开发

  让我们来检视 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    }
49    /**
50     * The BeanTableModel automatically returns false, and if you
51     * need to make an editable table, you'll have to subclass
52     * BeanTableModel and override this function.
53     */    
54    public boolean isCellEditable(int row, int col)
55    {
56       return false;
57    }
58    /**
59     * The function that returns the value that you see in the JTable.  It gets
60     * the BeanMap wrapping the JavaBean based on the row, it uses the
61     * column number to get the field from the column information LinkedMap,
62     * and then uses the field to retrieve the value out of the BeanMap.  
63     */
64    public Object getValueAt(int row, int col)
65    {
66       BeanMap map = (BeanMap)mapValues.get(row);
67       return map.get(columnInfo.getValue(col));
68    }
69    /**
70     * The opposite function of the getValueAt -- it duplicates the work of the
71     * getValueAt, but instead puts the Object value into the BeanMap instead
72     * of retrieving its value.
73     */
74    public void setValueAt(Object value, int row, int col)
75    {
76       BeanMap map = (BeanMap)mapValues.get(row);
77       map.put(columnInfo.getValue(col), value);
78       super.fireTableRowsUpdated(row, row);
79    }
80   
81    /**
82     * The BeanTableModel implements the CollectionListener interface
83     * (1 of the 3 parts of the framework) and thus listens for changes in the
84     * data it is modeling and automatically updates the JTable and the
85     * model when a change occurs to the data.
86     */  
87    public void collectionChanged(CollectionEvent e)
88    {
89       initializeValues((Collection)e.getSource());
90       super.fireTableDataChanged();
91    }

  正如您所看到的,BeanTableModel 的整个 TableModel 足够通用化,可以在任何表中使用。它充分利用了内省机制,省去了所有特定于 bean 的编码工作,在传统的 TableModel 中,这类编码工作绝对是必需的 —— 同时也是完全冗余的。BeanTableModel 还可以在 TMF 框架之外使用,虽然在外面使用会丧失一些威力和灵活性。

  看过这段代码之后,您会提出两个问题。首先,BeanTableModel 从哪里获得列名-字段与键-值配对的信息?第二,到底什么是 ObservableCollection ?这些问题会将我们引入框架的接下来的两个部分。这些问题的答案以及更多的内容,将在本文后面接下来的章节中出现。

  Castor XML 解析器

  保存必需的列名-字段信息的最合理的位置位于 Java 类之外,这样,不需要再重新编译 Java 代码,就可以修改这个信息。因为关于列名和字段的信息是 TMF 框架中惟一明确与表有关的信息,这意味着整个表格都可以在外部进行配置。

  显然,该解决方案会自然而然把 XML 作为配置文件的语言选择。配置文件必须为多种表模型保存信息;您还需要能够用这个文件指定每个列中的数据。配置文件还应当尽可能地易于阅读,因为开发人员之外的人员有可能要修改它。

  这些问题的非常好的解决方案是 Castor XML 解析器。查看 Castor 实际使用的非常好的方法就是查看如何在框架中使用它。

  让我们来考虑一下配置文件的目的:保存表模型和表中列的信息。 XML 文件应当尽可能简单地显示这些信息。TMF 框架中的 XML 文件用清单 3 所示的格式来保存表模型信息。

  清单3. TMF 配置文件示例

1 <model>
2       <className>demo.hr.TableModelFreeExample</className>
3       <name>Hire</name>
4       <column>
5          <name>First Name</name>
6          <field>firstName</field>
7       </column>
8       <column>
9          <name>Last Name</name>
10          <field>lastName</field>
11       </column>
12    </model>
13

  与这个目的相反的目标是,开发人员必须处理的 Java 对象应当像 XML 文件一样容易理解。通过 Castor XML 解析器用来存储列信息的三个 Java 对象,就可以看到这一点,这三个对象是: TableData (存储文件中的所有表模型)、 TableModelData (存储特定于表模型的信息)和 TableModelColumnData (存储列信息)。这三个类提供了 Java 开发人员所需的所有包装器,以便得到有关 TableModel 的所有必要信息。

  将所有这些包装在一起所缺少的一个环节就是 映射文件,它是一个 XML 文件,Castor 用它把简单的 XML 映射到简单的 Java 对象中。在完美的世界中,映射文件也应当很简单,但事实要比这复杂得多。良好的映射文件要使别的一切东西都保持简单;所以一般来说,映射文件越复杂,配置文件和 Java 对象就越容易处理。映射文件所做的工作顾名思义就是把 XML 对象映射到 Java 对象。清单 4 显示了 TMF 框架使用的映射文件。

  清单 4. TMF 框架使用的 Castor 映射文件

1 <?xml version="1.0"?>
2    <mapping>
3       <description>A mapping file for externalized table models</description>
4     
5       <class name="com.ibm.j2x.swing.table.TableData">
6          <map-to xml="data"/>
7          <field name="tableModelData" collection="arraylist" type=
8            "com.ibm.j2x.swing.table.TableModelData">
9             <bind-xml name="tableModelData"/>
10          </field>
11       </class>
12     
13       <class name="com.ibm.j2x.swing.table.TableModelData">
14          <map-to xml="model"/>
15          <field name="className" type="string">
16             <bind-xml name="className"/>
17          </field>
18          <field name="name" type="string">
19             <bind-xml name="name"/>
20          </field>
21          <field name="columns" collection="arraylist" type=
22            "com.ibm.j2x.swing.table.TableModelColumnData">
23             <bind-xml name="columns"/>
24          </field>
25       </class>
26     
27       <class name="com.ibm.j2x.swing.table.TableModelColumnData">
28          <map-to xml="column"/>
29          <field name="name" type="string">
30             <bind-xml name="name"/>
31          </field>
32          <field name="field" type="string">
33             <bind-xml name="field"/>
34          </field>        
35       </class>
36     
37    </mapping>
38

  仅仅通过观察这段代码,您就可以看出,映射文件清晰地勾划出了每个用来存储表模型信息的类,定义了类的类型,并将 XML 文件中的名称连接到了 Java 对象中的字段。请保持相同的名称,这样会让事情简单、更好管理一些,但是没必要保持名称相同。

  到现在为止,列名和字段信息都已外部化,可以读入包含列信息的 Java 对象中,并且可以很容易地把信息发送给 BeanTableModel,并用它来设置列。

  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 用法示例

1 // convert a normal list to an ObservableList
2    ObservableList oList = CollectionUtilities.observableList(list);
3    // A listener could then register for events from this list by calling
4    oList.addCollectionListener(this);
5    // trigger event
6    oList.add(new Integer(3));
7    // listener receives event
8    public void collectionChanged(CollectionEvent e)
9    {
10       // event received here
11    }
12

  ObservableCollection 有许多 TMF 框架之外的应用程序。如果您决定采用 TMF 框架,您会发现,在开发代码期间, ObservableCollection 框架有许多实际的用途。

  但是,它在 TMF 框架中的用途,重点在于它能更好地定义视图和模型之间的关系,当数据发生变化时,可以自动更新视图。您可以回想一下,这正是传统 TableModel 的最大限制,因为每当数据发生变化时,都必须用表模型的引用来更新视图。而在 TMF 框架中使用 ObservableCollection 时,当数据发生变化时,视图会自动更新,不需要维护一个到模型的引用。在 BeanTableModel 的 collectionChanged() 方法的实现中,您可以看到这一点。

  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 将永远地被下面这个简单的方法所代替:

1 TableUtilities.setViewToModel("table_config.xml", "Table", myJTable, myList);
0
相关文章