Swing通用数据验证模块

这段时间真是忙得要死,一方面要开发公司项目的系统框架,要将项目分成不同的子项目,编写核心 代码;另一方面要将极限编程(XP)引入团队开发,部署各类 XP需要的服务例如subversion啦,ant+ivy 啦,Hudson啦等等。顺便说句题外话,ubuntu还真是不是一般的好用,建议有能力的全部转到ubuntu上去 开发。

我目前开发的这个框架的客户端是具肥的客户端,也就是Swing客户端了。Swing应用相对于Web应用有 很多优势,因为它更肥。数据验证就是其中一个。当然现在的Web应用通过使用Ajax也要比以前强很多了 ,但是还是避免不了在验证数据时向服务段发出请求,至少你无法避免验证结果从Web服务器传输到用户 浏览器上这段过程。而Swing这类肥客户端可以实现完全在本地对数据进行验证,甚至可以断网继续工作 (这也是Web应用目前在研发的一个重要课题)。

前段时间开发出了一个可以应用于所有Swing应用的通用数据验证模块,发现它在项目中使用后,对于 普通的数据验证,程序员几乎不需要编码,效率提高了不少,就写了一篇博文拿出来和大家分享。原文是 用英文写的,在这里:http://polygoncell.blogspot.com/2008/07/validation-module-for-swing- application.html。英文好的朋友可以直接去那里看。

编写这个模块使用了很多不同的开源框架和类库,其中很重要的一个就是JXLayer。文章写完后,我就 跑去邀请JXLayer的作者Alexp来指点一下,然后就在我的文章后面开始了一段讨论,挺有意思的,他不愧 为是Swing team里面的牛人啊!厉害啊!呵呵。

ok,回到今天这篇文章的正题。今天的主要目的是将我的英文博文翻译成中文(自己的文章,我就不 逐字逐句翻译了,意思到了就行了,可能还会随兴展开一番讨论)在这里展示给大家,与大家分享开发经 验,希望大家能够从中获益,也希望能够以文会友,广交朋友。废话少说,切入正题。

数据验证(Validation)一直是软件开发中非常重要的一环,有了它,你的系统会让客户感到更加友 善,同时你的系统也得到了一定程度的保护。一般来说,数据验证既可以在客户端也可以在服务端。默认 的JSF数据验证就是在服务端,数据只能在被提交以后才能够被验证,然后把错误信息传递回用户的浏览 器。后来大规模使用Ajax后,基本可以实现对修改的数据“即时”验证,注意这里是个打了引号的即时, 数据事实上还是要在浏览器和服务端之间进行传递的,只不过Ajax将这种传递改为隐式了而已,理论上并 没有真正实现(断网)即时验证。而在Swing应用上就能够达成这种愿望。

事实上,开发Swing应用时,数据验证一直比较棘手,需要手工编码的地方太多,效率不高。后来出了 JGoodies Validation 结合JGoodies binding后,好了一些。这个JGoodies Validation既可以实现model 层面的验证,也可以实现Bean层面的验证,但是多年使用下来,发现其实它比较适用于中小项目,而且要 编写的代码其实一点不比自己手动编写的少。

JGoodies流行了一段时间后,sun开始推出自己的bean绑定方案:beansbinding(JSR 295),我个人 感觉要比JGoodies binding好用(JGoodies的作者Karsten也在专家组里,这个人我以前和他一起共事过 ,我的MSN space里面还有跟他的合影,绝对是Swing界的牛人)。这个beansbinding也提供数据验证,但 是它的这个数据验证只是在target被改动后,数据被同步回source之前才会起作用,使用起来局限性比较 大,而且编码量也不小。

由于目前绝大部分项目是基于POJO的,Hibernate validaTor已经提供了一个很好的数据验证框架,我 们完全没必要再重复发明轮子,我们应该努力站在巨人的肩膀上,这样我们才能站得更高,看得更远。于 是我考虑结合beansbinding和Hibernate ValidaTor开发数据验证。还有一个重要的问题,那就是数据错 误的时候,需要在用户界面上展示相应的信息,例如Error icon和错误提示,这部分我考虑使用JXLayer 。

你可以在如下链接中找到相关框架的具体信息:

1. Hibernate ValidaTor: http://www.hibernate.org/hib_docs/validaTor/reference/en/html_single/

2. Beansbinding: https://beansbinding.dev.java.net/

3. JXlayer: http://weblogs.java.net/blog/alexfromsun/

阅读这篇文章,不需要你熟悉这些类库,不过了解这些类库能够帮助你更好地理解这篇文章。

我的这个通用模块是参考JXLayer里面的一个demo类TextValidationDemo的,这个JXlayer是由 Alexander Potochkin开发的,我很喜欢,使用起来很顺手,强烈推荐使用。

下面开始介绍代码。首先是建立一个java项目,对于这个小项目,我使用netbeans。这里说句题外话 ,中型和大型的Swing应用,建议最好还是不要使用netbeans的GUI Builder,一方面它生成的代码超级烂 ,另一方面很难测试。目前市面上有很多好用的layout的框架,例如 JGoodies form和MigLayout,开发 效率绝对不比netbeans的GUI builder差,你还不需要面对令人头疼的机器成的代码。

项目创建好后,加入类库:

然后写一个persistence bean:

package de.jingge.domain;import javax.persistence.Entity;import javax.persistence.GeneratedValue;import javax.persistence.GenerationType;import javax.persistence.Id;import org.hibernate.validaTor.Length;import org.hibernate.validaTor.NotEmpty;@Entitypublic class Country extends AbstractBean {private static final long serialVersionUID = 5341382564159667599L;public static final String PROPERTYNAME_NAME = "name";public static final String PROPERTYNAME_CODE = "code";private String name;private String code;private Long id;public Country() {}public Country(String code, String name) {   super();   setCode(code);   setName(name);}@Id@GeneratedValue(strategy = GenerationType.AUTO)public Long getId() {   return id;}public void setId(Long id) {   this.id = id;}@NotEmptypublic String getName() {   return name;}public void setName(String name) {   firePropertyChange(PROPERTYNAME_NAME, this.name, this.name = name);}@Length(min=2, max= 2, message="Code length must be 2")@NotEmptypublic String getCode() {   return code;}public void setCode(String code) {   firePropertyChange(PROPERTYNAME_CODE, this.code, this.code = code);}}

这里我为了强调可以在Swing客户端直接使用和验证persistence bean,故意写了一个persistence bean,实际应用中,这个类只需要是一个pojo就行了。

这个Country类代表一个国家,它有两个属性,code和name,我给他们分别加上个各自的验证限制。 code不能为空,且必须正好是两个字符,例如CN,DE,US。name不能为空。这些annotaion均出自 Hibernate ValidaTor。那个父类AbstractBean出自SwingX类库,我们的Country类继承了它之后就可以支 持property change event了。

ok, 下面可以开始编写这个模块的核心代码了。前面说过,我会使用JXlayer。使用它的好处是:所有 JXlayer的painting event都会被转到UI类来,我们只需要编写一个集成Hibernate ValidaTor的UI类就可 以了,我称这个类为HibernateValidationUI,代码如下:

package de.jingge.view;import java.awt.Graphics2D;import java.awt.image.BufferedImage;import javax.swing.BorderFacTory;import javax.swing.JComponent;import javax.swing.text.JTextComponent;import org.hibernate.validaTor.ClassValidaTor;import org.hibernate.validaTor.InvalidValue;import org.jdesktop.beansbinding.ELProperty;import org.jdesktop.beansbinding.PropertyStateEvent;import org.jdesktop.beansbinding.PropertyStateListener;import org.jdesktop.jxlayer.JXLayer;import org.jdesktop.jxlayer.plaf.AbstractLayerUI;/*** Header:* Description: A layerUI which will validate the referenced property value of* the object each time when the paint() method is called.* The value of the given object property will be observed.* Note: This UI works only with {@link JXLayer}. Any change of the property* will force repainting the UI. The work process looks like: property changed ->* jxlayer will be repainted -> the paint() method of this UI will be called.* The logic of validation will be handled by the Hibernate validaTor* framework.**/public class HibernateValidationUI extends AbstractLayerUI {private Object object;private String propertyName;private ClassValidaTor validaTor;private ELProperty elProperty;private PropertyStateListener propertyChangeHandler;public HibernateValidationUI(Object obj, String propertyName) {   this.object = obj;   this.propertyName = propertyName;   propertyChangeHandler = new PropertyChangeHandler();   validaTor = new ClassValidaTor(obj.getClass());   elProperty = ELProperty.create("${" + propertyName + "}");}public void installUI(JComponent c) {   super.installUI(c);   elProperty.addPropertyStateListener(object, propertyChangeHandler);}public void uninstallUI(JComponent c) {   super.uninstallUI(c);   elProperty.removePropertyStateListener(object, propertyChangeHandler);}protected void paintLayer(Graphics2D g2, JXLayer l) {   super.paintLayer(g2, l);   InvalidValue[] validationMessages = validaTor.getInvalidValues(object,       propertyName);   if (validationMessages.length > 0) {     BufferedImage image = Java2DIconFacTory.createErrorIcon();     g2.drawImage(image, l.getWidth() - image.getWidth() - 1,         l.getHeight() - 8, null);     l.getView().setToolTipText(validationMessages[0].getMessage());     return;   }   l.getView().setToolTipText(null);}boolean isValid() {   return validaTor.getInvalidValues(object, propertyName).length == 0;}class PropertyChangeHandler implements PropertyStateListener {   @Override   public void propertyStateChanged(PropertyStateEvent pse) {     setDirty(true);   }}}

这个HibernateValidationUI类只有一个构建器,它接收两个参数,一个是source object,也就是我 们要修改的那个Bean类的实例,另外一个是这个bean的一个属性,这个HibernateValidationUI就负责验 证这个属性。

在installUI()方法中,我们启动对属性变化的观察类,而在uninstallUI()方法里面,我们需要卸载 这个观察类。

当给定对象的属性值发生变化时,PropertyChangeHandler的propertyStateChanged()方法就会被调 用,这个功能是通过elProperty和PropertzChangeHandler相结合来实现的。在propertyStateChangeed() 方法里UI类的方法setDirty()会被调用,该方法的调用会导致UI类的状态变化,进而引发(re)painting, 之后经过一系列的方法调用传递,paintLayer(Graphics2D g2, JXLayer l)这个 方法将会被调用,这个方法要做的就是我们这个数据验证模块的核心功能:

1. 调用Hibernate ValidaTor验证该属性。

2. 如果数据不正确,则在GUI上显示一个error icon,并且将错误信息作为tooltip展示给用户。

在第二点里面产生了一个问题,谢谢Alexp对我的指点。Swing team里面有一些规定,其中之一就是, 在paint()方法里面最好不要改变Component的状态,而setTooltip()方法将会改变 component的状态,因 此需要在paint()方法之外调用。我目前使用下来,还没有发现什么严重的错误,决定暂时不改了,回头 有时间在将这个代码翻新一下。

类中用到的Java2DIconFacTory代码如下:

package de.jingge.view;import java.awt.Color;import java.awt.Graphics2D;import java.awt.RenderingHints;import java.awt.image.BufferedImage;public class Java2DIconFacTory {public static BufferedImage createErrorIcon() {   return createErrorIcon(7, 8);}public static BufferedImage createErrorIcon(int width, int height) {   BufferedImage icon = new BufferedImage(width, height,       BufferedImage.TYPE_INT_ARGB);   Graphics2D g2 = (Graphics2D) icon.getGraphics();   g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,       RenderingHints.VALUE_ANTIALIAS_ON);   g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,       RenderingHints.VALUE_STROKE_PURE);   g2.setColor(Color.RED);   g2.fillRect(0, 0, width, height);   g2.setColor(Color.WHITE);   g2.drawLine(0, 0, width, height);   g2.drawLine(0, height, width, 0);   g2.dispose();   return icon;}}

没什么太多好解释的,就是使用Java 2D画一个Error icon。

接着,我们需要编写一个FacTory类,构建一个JTextField,尽量把复杂技术封装起来,这样程序员开 发起来可以提高效率,代码如下:

package de.jingge.view;import javax.swing.JTextField;import javax.swing.text.JTextComponent;import org.jdesktop.beansbinding.AutoBinding;import org.jdesktop.beansbinding.BeanProperty;import org.jdesktop.beansbinding.BindingGroup;import org.jdesktop.beansbinding.Bindings;import org.jdesktop.beansbinding.ELProperty;import org.jdesktop.jxlayer.JXLayer;import static org.jdesktop.beansbinding.AutoBinding.UpdateStrategy.*;public class GuiComponentFacTory {public static JXLayer createTextField(     BindingGroup bindingGroup, Object sourceObject,     String sourceProperty) {   JTextField field = new JTextField();   AutoBinding binding = Bindings.createAutoBinding(READ_WRITE,       sourceObject, ELProperty.create("${" + sourceProperty + "}"),       field, BeanProperty.create("text"));   bindingGroup.addBinding(binding);   bindingGroup.bind();   return new JXLayer(field, new HibernateValidationUI(       sourceObject, sourceProperty));}}

createTextField()方法主要将给定对象属性的值与JTextField的text绑定,然后将JTextField纳入到 JXLayer的管理之下。这样一来,一旦用户在JTextField里面修改数据,这个改变就会同步到该对象属性 上,然后就引发了前面描述的一系列逻辑,最终改变的数据就会被Hiberante ValidaTor加以验证。

最后,我们可以编写一个Demo application来看看效果如何,代码如下:

package de.jingge.main;import de.jingge.domain.Country;import java.awt.BorderLayout;import java.awt.Dimension;import java.awt.Toolkit;import javax.swing.JFrame;import javax.swing.JLabel;import javax.swing.JPanel;import javax.swing.UIManager;import javax.swing.UnsupportedLookAndFeelException;import javax.swing.text.JTextComponent;import net.miginfocom.swing.MigLayout;import org.jdesktop.beansbinding.BindingGroup;import org.jdesktop.jxlayer.JXLayer;import static de.jingge.view.GuiComponentFacTory.*;public class ValidationApplicaton {private BindingGroup bg;private Country country;private JXLayer codeField;private JXLayer nameField;/*** @param args the command line arguments*/public static void main(String[] args) {   try {     UIManager.setLookAndFeel(         "com.sun.java.swing.plaf.nimbus.NimbusLookAndFeel");   } catch (UnsupportedLookAndFeelException ex) {     System.err.println(         "Nimbus L&F does not support. Default L&F will be used.");   } catch (ClassNotFoundException e) {     // TODO Auto-generated catch block     e.printStackTrace();   } catch (InstantiationException e) {     // TODO Auto-generated catch block     e.printStackTrace();   } catch (IllegalAccessException e) {     // TODO Auto-generated catch block     e.printStackTrace();   }   ValidationApplicaton app = new ValidationApplicaton();   JFrame. frame. = new JFrame("Demo Validation Application");   frame.setPreferredSize(new Dimension(360, 150));   frame.getContentPane().add(app.buildPanel(), BorderLayout.CENTER);   frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);   setCenter(frame);   frame.setVisible(true);   frame.pack();}private static void setCenter(JFrame. frame) {   Toolkit toolkit = Toolkit.getDefaultToolkit();   Dimension screenSize = toolkit.getScreenSize();   // Calculate the frame. location   int x = (screenSize.width - (int) frame.getPreferredSize().getWidth()) / 2;   int y = (screenSize.height - (int) frame.getPreferredSize().getHeight()) / 2;   // Set the new frame. location   frame.setLocation(x, y);}public ValidationApplicaton() {   country = new Country();   bg = new BindingGroup();}private JPanel buildPanel() {   codeField = createTextField(bg, country, Country.PROPERTYNAME_CODE);   nameField = createTextField(bg, country, Country.PROPERTYNAME_NAME);   JPanel panel = new JPanel(new MigLayout("",       "[50px, right]10[200px:250px:300px]", "[center]"));   panel.add(new JLabel("Code:"), "cell 0 0");   panel.add(codeField, "cell 1 0, w 200px:250px:300px");   panel.add(new JLabel("Name:"), "cell 0 1");   panel.add(nameField, "cell 1 1, w 200px:250px:300px");   return panel;}}

这个类比较简单了,我简单解释一下:

在main()方法里面,我们创建了一个JFrame,然后放入一个JPanel

setCenter()方法负责将窗口至于屏幕的正中间。

在构建器里面,我们创建了Country和BindingGroup的对象实例。

在buildPanel()方法里面,我们使用MigLayout构建了一个Panel,其中codeField和nameField对应各 自的对象属性。更多关于MigLayout的信息看这里:http://www.miglayout.com/。这也是一个例子,大家 可以看到使用MigLayout开发Swing真的是非常方便。

从这个Demo里面也可以看出,编写好pojo后,程序员只需要调用createTextField(bg, country, Country.PROPERTYNAME_CODE); 就可以创建一个支持数据验证的JTextField,编码量已经可以说是最大限 度的降低了。

运行程序,你会看到:

这个code和name的数据都不合法,用户看到了error icon。

将鼠标移到Text field上,你会看到:

填好合法数据后,Error icon就不见了:

总结:

使用这个通用数据验证模块有很多好处:

1. 如果项目使用ORM,例如Hibernate,这个方案应该是解决数据验证的最好方案之一。

2. 对于普通的数据验证,例如非空,email,长度等等,程序员根本不需要编码,只要在POJO上使用 相应的Hibernate ValidaTor annotation就可以了。

3. 对于复杂的数据验证,Hibernate ValidaTor提供了很好的扩展机制,只要写一个annotation外加 一个ValidaTor就可以了。Swing应用这边仍然不需要编写任何代码。

综上所述,可以看出通过使用这个通用数据验证模块,开发效率会提高很多。

更重要的是心理上的完全自由和放松,

Swing通用数据验证模块

相关文章:

你感兴趣的文章:

标签云: