程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> Swing通用數據驗證模塊

Swing通用數據驗證模塊

編輯:關於JAVA

這段時間真是忙得要死,一方面要開發公司項目的系統框架,要將項目分成不同的子項目,編寫核心 代碼;另一方面要將極限編程(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;
@Entity
public 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;
}
@NotEmpty
public 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")
@NotEmpty
public 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<jTextComponent> {
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<jTextComponent> 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<jTextComponent> 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<jTextComponent> 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<jTextComponent>(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<jTextComponent> codeField;
private JXLayer<jTextComponent> 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應用這邊仍然不需要編寫任何代碼。

綜上所述,可以看出通過使用這個通用數據驗證模塊,開發效率會提高很多。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved