技术开发 频道

智能数据使 Swing 保持简单

  如上面所提到的,iData 层被细分为不可修改数据和可修改数据的功能。由于 MutableIData 接口继承了 ImmutableIData 接口,我们将从研究不可修改数据的功能开始。

  只读智能数据的数据间接层(ImmutableIData)

  ImmutableIData 接口是 iData 层的一部分;它表示不可修改 iData 间接。它由两个方法和一个推荐的方法覆盖组成:

  getData() 从 DataObject 返回一个具有类型的数据值。

  getSource() 返回 DataObject 本身。

  覆盖 toString() 方法返回 getData() 结果的 string 表示。

  作为示例,让我们看一下 Manufacturer 字段的 ImmutableIData 实现。

  清单 2. “自行车制造商”的 ImmutableIData 实现

1 public class BicycleManufacturerIData implements ImmutableIData
2 {
3   //the DataObject
4   Bicycle bicycle = null;
5   public BicycleManufacturerIData(Bicycle bicycle)
6   {
7     this.bicycle = bicycle;  //cache the DataObject
8   }
9   public Object getSource()
10   {
11     return this.bicycle; //this simply returns the DataObject
12   }
13   public Object getData()
14   {
15     //returns the manufacturer field from the DataObject.  
16     //This is the main logical method of the indirection layer.
17     return bicycle.getManufacturer();
18   }
19   public String toString()
20   {
21     //create a safe to String method to avoid null pointer exceptions
22     //while painting...
23     Object data = this.getData();
24     if (data != null)    
25       return data.toString();
26     else
27       return "";
28   }
29 }
30

  iData 工具箱提供了一个实现 ImmutableIData 的称作 DefaultImmutableIData 的抽象类。它覆盖 Object 中的 toString() 方法以安全地返回 getData().toString() 。示例的剩余部分将扩展 iData 层接口的缺省实现。这些缺省实现也包含在该工具箱中。

  与 JTable 集成

  让我们继续“自行车商店”示例,并将 iData 技术集成到 JTable 中。表有 manufacturer 、 modelName 、 modelID 、 price 、 cost 和 inventory 列。假定 ImmutableIData 实现的剩余部分紧跟 manufacturer iData 语句行编写。

  实际添加到 JTable DataModel 的数据是 ImmutableIData 实现,每个实现都含有一个 DataObject 。这种添加含有 DataObject 的 iData 层实现的思想就是前面所指的间接层的实现。

  图 2. 带有含有 DataObject 的 ImmutableIData 实现的 JTable 单元

  我发现使用一个助手(helper)方法(我将其称为 createRow() )来立即创建一整行是有用的。当使用 AbstractTableModel 的子类时,可以将这一行整个添加到模型中。 createRow() 方法将 DataObject 作为参数,并为特定的表实例化适当的 ImmutableIData 实现。

  清单 3. createRow() 方法

1 protected Vector createRow(Bicycle bicycle)
2   {
3     Vector vec = new Vector();
4       vec.add(new BicycleModelNameImmutableIData(bicycle));
5       vec.add(new BicycleModelIDImmutableIData(bicycle));
6       vec.add(new BicycleManufacturerImmutableIData(bicycle));
7       vec.add(new BicyclePriceAndCostImmutableIData(bicycle));
8       vec.add(new BicycleProfitImmutableIData(bicycle));
9       vec.add(new BicycleInventoryImmutableIData(bicycle));
10     return vec;
11   }
12

  此外, createRow() 方法是确定模型中应放置何种 ImmutableIData 实现的逻辑的集中位置。从类管理的角度,使用匿名内部类也是有用的,对于简单的 ImmutableIData 实现,可以直接在 createRow() 方法中声明这些类。

  渲染顺序

  缺省渲染器通过调用对象的 toString() 方法来创建他们要显示对象的字符串表示。这就是 ImmutableIData 实现要有一个有用的 toString() 方法是很重要的原因之一。在渲染期间,渲染器从 JTable 接收到 ImmutableIData 实现。要渲染 iData,调用了 toString() 方法。以下表示了整个渲染顺序:

  iData 实现上的 toString()

  iData 实现上的 getData()

  DataObject 上的 get[FieldName]()

  图 3. 渲染顺序

        

  图 4. 只读表

        

  动态持久性

  使用 DataObject 作为 iData 的数据不仅为 iData 间接提供了灵活性,而且添加了一个提供动态持久性的有用的数据间接层。请考虑一个已显示的表的示例,正在从外部更新该表的值。通常,客户机需要实现复杂的逻辑来推断模型中持久存储更新值的地方。当使用 iData 和 DataObject 间接时,由于会自动持久存储新值,因此这一逻辑完全没有必要。这就是用包含相同 DataObject 实例的多个 iData 对象填充模型的结果。当更改 DataObject 的内部值时,由于所有 iData 对象指向同一个实例,因此 DataObject 本身不会改变。通过使用 DataObject 的读(get)和写(set)方法对其进行再查询,无须任何手工持久性的工作,就总能返回最新的结果。客户机对更新要执行的唯一操作是重画(repaint),这一操作强制渲染器重新渲染更新的单元,即依次检索并显示新数据值。

  这种间接的一个结果是有统一的客户机数据高速缓存的能力。假定组件使用来自中央客户机高速缓存的 iData 间接和 DataObjects ,在整个应用程序中,所有数据编辑将动态持久存储。这极大地简化了负责显示动态数据的交易系统和其它客户机。

  示例:虚拟列

  虚拟列体现了 iData 技术的灵活性。虚拟列是一个含有数据的列(这些数据没有显式地包含在模型中,而是由多个字段组合而成)。设想称为 profit 的列,它将显示 price 和 cost 之间的差额。要创建这个列,需要创建一个 ImmutableIData 实现,其中 getData() 返回 price 和 cost 之间的差额。

  清单 4. 利润虚拟列

1 public class BicycleProfitImmutableIData extends DefaultImmutableIData
2 {
3   ...
4   
5   public Object getData()
6   {
7     //return the difference of the price and cost field from the DataObject
8     return new Double(bicycle.getPrice() - bicycle.getCost());
9   }
10 }
11

  使用标准模型创建这种类型的虚拟列将需要大量逻辑。首先,要使用正确的值填充该模型。当编辑 price 或 cost 时,可能会出现一些问题:在整个应用程序中需要复杂和经常容易出错的逻辑来更新 profit 值。有了 iData 技术,编辑 price 或 cost 时,无须任何更新操作。动态地存在持久性。

  使用 iData 对象作为构造模块

  间接层作为一组 iData 对象来实现,这样会带来实质性的好处。例如,附加两个数据值的 PriceAndCost 显示也可以使用组合来实现。不是直接从新的 CompositePriceAndCost 显示中的 DataObject 检索这两个值,而是可以使用以前编写的 BicyclePriceImmutableIData 和 BicycleCostImmutableIData 对象。通过从两个 iData 层实现(由一个分隔符来分隔,在本例中,分隔符为斜杠)检索值,然后附加这两个值,这样 getData() 就创建了返回字符串。最终的 getData() 方法类似于这样:

  清单 5. PriceAndCostImmutableIData 的组合实现

1 public Object getData()
2 {
3     // append the price, a slash, and the cost using pre-built iData
4     // implementations    
5     return new String( (String)priceIData.getData() + " / " +
6         (String)costIData.getData() );  
7 }
8

  这种组合不同 iData 实现的能力提高了代码重用和灵活性。可以通过对已有 iData 实现进行不同的组合来开发新的 iData 实现。由于这种组合有利于运行时 iData 实现的动态组合,这意味着需要更少的具体类和更大的灵活性。工具箱含有一些实现简单的基于组合的 iData 对象的助手类,包括前缀和后缀字符串修饰符 iData 实现,这些实现使用任意一个带有后缀和/或前缀的 iData 对象来修饰 iData 的字符串表示。

  基于反射(reflection)的 ImmutableIData 实现(UniversalImmutableIData)

  iData 方法的主要缺陷之一是类的数目过多。在大型应用程序中,iData 类的数目可能会迅速变得难以控制。大多数 iData 层实现重复相同的顺序,在这一顺序里, getData() 请求被重定向到 DataObject 中的 get[FieldName]() 方法。通常,可以使用反射来实现这一点。工具箱含有一个基于反射的 ImmutableIData 实现的缺省实现,名为 UniversalImmutableIData 。 UniversalImmutableIData 使用一个 DataObject 和一个字段名作为初始化参数。在内部,它获取字段名,然后检索 get[FieldName]() 方法,当调用 getData() 或 toString() 方法时会调用 get[FieldName]() 方法。这种方法简化了开发,同时减少了类的数目,所付出的代价只是由于使用反射而带来的性能上的轻微降低。大多数应用程序不会受这一性能下降影响,而大型或实时应用程序的开发人员应该牢记这一点。

  清单 6. 使用 UniversalImmutableIData 的 createRow() 方法

1 protected Vector createRow(Bicycle bicycle)
2 {
3   Vector vec = new Vector();
4     vec.add(new UniversalImmutableIData(bicycle, "modelName"));
5     vec.add(new UniversalImmutableIData(bicycle, "modelID"));
6     vec.add(new UniversalImmutableIData(bicycle, "manufacturer"));
7     vec.add(new UniversalImmutableIData(bicycle, "priceAndCost"));
8     vec.add(new UniversalImmutableIData(bicycle, "inventory"));
9   return vec;
10 }
11

  清单 7. 来自基于反射的 UniversalImmutableIData 的样本

1 protected String field = ...  //the field name
2   protected Method accessorMethod = ... //the accessor method
3   protected Object source = ... //the DataObject
4   ...
5   protected void setMethods()
6   {
7     if (field == null || field.equals(""))
8       return;
9     //capitalize the first letter of the field, so you get getName,
10     //not getname...
11     String firstChar = field.substring(0,1).toUpperCase();
12     //remove first letter
13     String restOfField = field.substring(1);
14     //add together the string "get" + the capitalized first letter,
15     //plus the remaining
16     String fieldAccessor = "get" + firstChar + restOfField;
17     //cache the method object for future use
18     this.setAccessorMethod(fieldAccessor);
19   }
20   ...
21   protected void setAccessorMethod(String methodName)
22   {
23     try
24     {
25       accessorMethod = source.getClass().getMethod(methodName, null);
26     }
27     catch ( ... )
28     {
29       ...
30     }
31   }
32   ...
33   public Object getData()
34   {
35     try
36     {
37       return accessorMethod.invoke(source, null);    
38     }
39     catch ( ... )
40     {
41       ...
42     }
43   }
44

  可编辑智能数据的数据间接层(MutableIData)

  通过增加一个 setData() 方法, MutableIData 继承了 ImmutableIData ,使之可修改。 setData() 方法采用新数据值作为一个参数,并返回一个表示编辑是否成功的布尔值。通常,有必要对 setData() 方法中的新数据值进行强制类型转换,以与 DataObject 中那个字段高速缓存的数据类型相匹配。标准实现安全地测试对象类型,如果类类型不匹配,则返回值为 false。

  清单 8. “自行车制造商”的 MutableIData

1 public boolean setData(Object data)
2   {
3      if (!data instanceof String)
4          return false;
5      ((Bicycle)this.getSource()).setManufacturer((String)data);
6          return true;
7   }
8

  一旦编写完所有的新 MutableIData 对象,则更新 getRow() 方法来实例化 MutableIData 实例而不是与其对应的 Immutable 实例。我发现,当字段明确是不可修改时,只生成 ImmutableIData 对象。否则,知道不会调用 setData() 方法,生成了 MutableIData 并且仅在只读表中使用它。

  定制编辑器

  既然已经能够修改数据了,就有了一个大的改变:编辑数据需要定制编辑器。如果使用缺省编辑器,那么 JTable 将会检索编辑器以查找 Object 类型的数据,这个编辑器实际上是一个 String 编辑器。一旦停止编辑,该编辑器就返回一个 String 值,这个 String 值持久存储在模型中,其中使用 String 值替换 iData 层实现。下图描绘了要保持 iData 间接的完整性必须遵循的编辑顺序。

  图 5. 编辑顺序

        

  虽然可以扩展已有的编辑器来遵循该顺序,但这种方法是不切实际的;它会导致过多的类和造成不必要的复杂性。定制编辑器在某些独特情况下是可行的,但是大多数编辑器将遵循相同的顺序,可以将这一顺序封装在一个单独的类中。iData 工具箱包含这个类的一个实现,称为 UniversalTableCellEditor 。

  UniversalTableCellEditor 使用 TableCellEditor 的内涵而不是其扩展。在编辑时, UniversalTableCellEditor 从 iData 层实现抽取出数据值,并使用该值初始化所含的 TableCellEditor 。当停止编辑时, UniversalTableCellEditor 从 TableCellEditor 中检索该值并相应地在 iData 实现中设置该值。如果开始没有指定编辑器,则 UniversalTableCellEditor 检索 JTable 中的缺省编辑器以查找 iData 实现的数据类型。

  在以上所述的整个编辑程序完全封装在 UniversalTableCellEditor 中。这意味着,可以使用任何编辑器,甚至是第三方编辑器,而不需要实现 iData 逻辑的扩展。

  我建议通过将每个 TableColumn 的缺省编辑器设置成 UniversalTableCellEditor 来设置 JTable 中的编辑器。iData 工具箱含有一个带有几个静态助手方法的实用类。实用类中的 configureTable() 方法对 TableColumns 进行遍历,将每个当前编辑器设置成包含那一列以前的单元编辑器的 UniversalTableCellEditor 的新实例。

  当工具箱与渲染器相关时,它有具有类似功能的 UniversalTableCellRenderer ,它具有与渲染器类似的功能。工具箱中还包括 JTree 和 JComboBox/JList 的通用编辑器和渲染器组合。

  示例:单元内验证保证了价格高于成本

  编辑的标准困难是 单元内验证,即在单元编辑停止之前进行数据验证。 setData() 方法创建了一个用于单元内验证的集中位置。请考虑这样一个示例,在对 price 或 cost 进行编辑之后,如果 price 值低于 cost ,则用户应该接到通知。这时,我们希望向用户显示下列选项:

  对两个值不做任何处理。

  提高 price 使之等于 cost 。

  修改未编辑的那个值,使这个值与刚编辑过的值之间的差值与最初的价差相等。

  在 setData() 方法中,实现它们相对比较容易。它向用户提供了一个 JOptionPane 以标识首选的选项。一旦选定了某个选项,则会执行计算以设置适当的值。知道实现这一业务逻辑的所有数据值以及集中位置是 iData 技术灵活性的关键。

  清单 9. 单元内验证

1 String doNotEdit = "Do Not Edit";
2 String priceEqualsCost = "Price = Cost";
3 String keepProfitDifference = "Keep Profit Difference";
4 String keepProfitPercentage = "Keep Profit Percentage";
5 ...
6 public boolean setData(Object data)
7 {
8     double newCost = new Double(data.toString()).doubleValue();
9     double oldCost = this.bicycle.getCost();
10     double price = bicycle.getPrice();
11     ((Bicycle)this.getSource()).setCost(newCost);
12     if (price > newCost)
13     {
14       Object result = JOptionPane.showInputDialog
15       (
16         null,
17         "Cost you have entered is more than the set price for this bicycle"
18         + "\nPlease select from the following options",
19         "",
20         JOptionPane.QUESTION_MESSAGE,
21         null,
22         new Object[]{doNotEdit, priceEqualsCost, keepProfitDifference,
23             keepProfitPercentage},
24         priceEqualsCost
25       );
26       if (result != null)
27       {
28         //persist the data
29         if (result.equals(priceEqualsCost))
30           this.bicycle.setPrice(bicycle.getCost());  
31         //keep the delta between price and cost
32         else if (result.equals(keepProfitDifference))
33           this.bicycle.setPrice( newCost + (oldPrice - oldCost) );
34         //keep the same profit percentage
35         else if (result.equals(keepProfitPercentage))
36           this.bicycle.setPrice( newCost * (oldPrice / oldCost) );
37       }
38     }
39     return true;
40   }
41

  使用非 JTable 组件

  虽然,出于一致性考虑,在我们的示例中一直使用 JTable,但是使用另一个 Swing 组件来研究一个简单的示例也是值得的。让我们创建一个含有自行车名称的 JList。仅对 Bicycle 对象的集合进行遍历,将它们封装在 BicycleModelNameImmutableIData 对象中,然后将这些对象添加到 JList 中。请注意,在 JList 中使用了与 JTable 中相同的 iData 实例。在任何其它组件中也可以以相同方式使用这些实例。

  清单 10. JList 初始化

1 protected void initList()
2 {
3     ...
4     while ( ... )
5     {
6        //wrap the bicycle in an iData object and add it to the list model
7       Bicycle bike =  ...
8       model.addElement(new BicycleModelNameMutableIData(bicycle));
9     }
10     //wrap the lists renderer in the iData toolkit universal renderer
11     //for JLists
12     list.setCellRenderer(new
13       UniversalListCellRenderer(list.getCellRenderer()));
14 }
15

  图 6. JList 示例

        

0
相关文章