Exploring Vaadin (3) 閱讀 com.vaadin.ui.Form 源代碼
Form 是 vaadin 用戶界面的一個組成部分,下面結合其源代碼(ver 6.1.5)進行分析。
public class Form extends AbstractField implements Item.Editor, Buffered, Item,
Validatable {
Form 實現了 vaadin 數據模型的上述接口,其中最重要的應該是 Item.Editor,這個接口代表 Form 是用來編輯 Item 的。Item 可以是一個 pojo,一個數據表中的一行等等。總之,Item 是 Property 的集合。具體請看我的第一篇 Data 部分。
private Object propertyValue;
private Layout layout;
private Item itemDatasource;
private final LinkedList<Object> propertyIds = new LinkedList<Object>();
保存加入的 id,僅在addItemProperty(Object id, Property property) 和 addField(Object propertyId, Field field) 中被添加。在很多方法中被遍歷。與ownProperties的不同之處就在于多了一個方法,即addField中也會添加。
private Buffered.SourceException currentBufferedSourceException = null;
這個currentBufferedSourceException 在commit()以及discard()時被設置或者清除,用來保存各個field.commit()/field.discard()過程中捕捉到的Buffered.SourceException并且被拋出。在getErrorMessage()中被返回。
private boolean writeThrough = true;
private boolean readThrough = true;
private final HashMap<Object, Field> fields = new HashMap<Object, Field>();
private final HashMap<Object, Property> ownProperties = new HashMap<Object, Property>();
所有通過addItemProperty(Object id, Property property) 加入的property 被按照 id 加入到ownProperties 中。在 getItemProperty 時僅僅作為第三選擇,在沒有對應的Field時才被返回。在 removeItemProperty 時也被除去。
private FormFieldFactory fieldFactory;
private Collection<Object> visibleItemProperties;
private Layout formFooter;
private boolean validationVisibleOnCommit = true;
// special handling for gridlayout; remember initial cursor pos
private int gridlayoutCursorX = -1;
private int gridlayoutCursorY = -1;
上面是 members
private final ValueChangeListener fieldValueChangeListener = new ValueChangeListener() {
public void valueChange(com.vaadin.data.Property.ValueChangeEvent event) {
requestRepaint();
}
};
所有的 field 都會被加上這個監聽器,這樣當任何一個 field 的值發生變化時都會使 Form 重新繪制。
public ErrorMessage getErrorMessage() {
考慮三個 error 來源:
- getComponentError() 這個是從 AbstractComponent 繼承下來的,可以被設置, 設置時會觸發 Component.ErrorEvent。
- validationError,來自各個Field 的getErrorMessage(),但只抓第一個。詳情見下面。
- currentBufferedSourceException,上一次commit()或者discard()收集到的Buffered.SourceException,見上面介紹。
得到 validationError:如果 isValidationVisible() (默認否),那么對 propertyIds 遍歷,從 fields Map中得到各個 Field,如果 field.getErrorMessage() 不為空,則拋出 Validator.InvalidValueException。這個Validator.InvalidValueException 可能就是從field.getErrorMessage() 得到的那個。但是如果消息未空或者有其他問題,建立一個InvalidValueException,以 Field.getCaption() 作為內容。注意,只要一個Field得到 InvalidValueException,其他Field就不再遍歷了。
然后,考慮上面說到的三個源。如果都為空,返回空。否則拋出 CompositeErrorMessage,包含這三個錯誤源。
public void commit() throws Buffered.SourceException {
如果isInvalidCommitted()為否,并且isValid()為否,進行 validate(),可能拋出異常。
如果沒有進行 validate() 或者 validate() 通過,則遍歷 propertyIds,從 fields 里面按照 id 取出各個 Field,除非 isReadOnly(),否則調用 commit()。捕捉可能拋出的 Buffered.SourceException, 先加入到一個問題列表中。
最后,如果問題列表為空,清空currentBufferedSourceException ,requestRepaint(),順利返回。否則,create 一個新的 Buffered.SourceException,把收集到的 Exception 當作 cause,然后設為currentBufferedSourceException ,要求 requestRepaint(),并拋出剛剛生成的 currentBufferedSourceException。
public void discard() throws Buffered.SourceException {
過程和commit()基本一致,只是調用各個field.commit()換成了調用 field.discard()。同樣,也捕捉處理Buffered.SourceException,放在currentBufferedSourceException中。
public boolean isModified() {
遍歷 propertyIds,調用各個 Field 的 isModified(),任何為真則返回真。
public void setReadThrough(boolean readThrough) {
如果確實有變化,遍歷 propertyIds,調用各個 Field 的 setReadThrough()。
public void setWriteThrough(boolean writeThrough) throws SourceException,
同上處理。
public boolean addItemProperty(Object id, Property property) {
如果 propertyIds 已經包括 id,也就是說以前加入過,直接返回 false。
否則,通過 fieldFactory 建立Field。配置Field,包括setPropertyDataSource,然后從id.toString()經過長度,首字母大小寫調整,設置為Caption。最后調用 addField(id,field)加入 Field。
public void addField(Object propertyId, Field field) {
將要加入的field 加入到 fields,給要加入的 field 加上fieldValueChangeListener以便在 field 的值改變時重新繪制Form,如果propertyIds不包含 propertyId,加入(這是被外部調用的時候發生的)。調整field的 readthrough / writethrough / immediate 和 Form 一致,最后將 field 加入本 Form 的 Layout。在加入 Layout 時,如果是 CustomLayout,以propertyId.toString()為位置標記。否則只是簡單順次加入。
這里本人對 vaadin 的做法有保留。propertyId.toString()被作為 Field的Caption,又被作為很多 Map 的key,又被作為 CustomLayout 的位置標記,是不是擔當的角色太多了?從內部結構標識到給用戶的Caption, 會給國際化帶來麻煩嗎?等我進一步深入探索使用,才能得到結論。
public Property getItemProperty(Object id) {
依照下面次序返回:
如果 fields.get(id) 得到Field,再從field.getPropertyDataSource() 得到 property,則返回 property
否則,返回 field,因為 field 就是一個 property
再否則,返回 ownProperties.get(id)
關于 ownProperties,詳見上面說明
public Field getField(Object propertyId) {
return fields.get(propertyId);
}
public Collection getItemPropertyIds() {
return Collections.unmodifiableCollection(propertyIds);
}
以上兩條代碼簡單明了。Collections.unmodifiableCollection 不錯,今后可以多用。
public boolean removeItemProperty(Object id) {
從 ownProperties, fields, propertyIds, layout 中去掉 id 相對應的元素。這里面從ownProperties去掉后先判斷是不是有對應的field,如果有,才從fields, propertyIds, layout 中去掉。為什么這么寫?這是因為如果有 propertyId,就一定有相應的 field,加入到 layout?看代碼,應該是這樣吧。
public boolean removeAllProperties() {
對所有 propertyIds 依次調用 removeItemProperty。
public Item getItemDataSource()
就是簡單的getter。
public void setItemDataSource(Item newDataSource) {
setItemDataSource(newDataSource, newDataSource != null ? newDataSource
.getItemPropertyIds() : null);
}
看下面的實現。
public void setItemDataSource(Item newDataSource, Collection propertyIds) {
首先 removeAllProperties(),設置 itemDatasource,然后遍歷參數 propertyIds,從itemDatasource取出相應的 property,通過 fieldFactory 得到 Field,設置field.setPropertyDataSource 為所取出的property,調用 addField 來添加 Field。
奇怪的是這里為什么沒有設置 Field 的 Caption? 為什么不是調用 addItemProperty 來處理,而是要寫重復代碼?很奇怪。原來默認的DefaultFieldFactory里面在 createField 時確實設置 Caption的。createField 調用 createCaptionByPropertyId 來得到Caption,而createCaptionByPropertyId 進行格式處理,將 "firstName" 便成為 "First Name"。這個做法只能是一個默認的方便的做法。看來 FieldFactory確實要自己去寫的。可是這樣看來,addItemProperty 在 createField 之后又去操作 field.setCaption() 和進行格式化是多余了。注意 addItemProperty 是以 this,也就是這個 Form 作為Item參數去調用 createField的,而setItemDataSource 是以得到的 itemDataSource,也就是 newDataSource 進行的。這樣就明白了。addItemProperty 是添加一個不在 itemDataSource中的一個獨立的 property。這兩個函數,addItemProperty 和 setItemDataSource,要說addItemProperty 的名字起得有點誤導。應該是 addFormProperty 或者 addStandaloneProperty 更好些嗎?看來“正常”使用的時候,就是用來和一個Item綁定的時候,是應該不會調用addFormProperty 的。除非,除了Item的那些property之外,還要加其他property。
public Layout getLayout() {
就是 getter
public void setLayout(Layout newLayout) {
用新的 layout 替換舊的。如果有舊的 layout,把所有的 field 一一從舊 layout 中去掉,再加入到新的里面。
public Select replaceWithSelect(Object propertyId, Object[] values,
Object[] descriptions) {
這個沒有細看,用到時候再說吧。應該用 FieldFactory 來解決問題。
@Override
public void attach() {
super.attach();
layout.attach();
}
@Override
public void detach() {
super.detach();
layout.detach();
}
這兩個比較簡單。
@Override
public boolean isValid() {
依次調用 propertyIds 對應的 field,再調用 isValid(),進行與運算。最后與 super.isValid() 進行與運算。
@Override
與上面順序相反,先調用 super.validate(),再順次調用 propertyIds 對應的 field,再調用 validate()
public boolean isInvalidAllowed() {
return true;
}
public void setInvalidAllowed(boolean invalidValueAllowed) 沒有實現,直接拋出異常
public void setReadOnly(boolean readOnly) {
依次調用 super 以及 propertyIds 對應的各個 field 的相應方法
public void setFormFieldFactory(FormFieldFactory fieldFactory) {
public FormFieldFactory getFormFieldFactory() {
就是setter 和 getter
public Class getType() {
if (getPropertyDataSource() != null) {
return getPropertyDataSource().getType();
}
return Object.class;
}
很簡單。。。
protected void setInternalValue(Object newValue) {
首先 super.setInternalValue(newValue),propertyValue = newValue; 然后看是不是和舊的 propertyValue 不同。如果是的話,則調用受保護方法 setFormDataSource(newValue, getVisibleItemProperties())。setFormDataSource 得到 Item (如果 newValue 不是 Item,就生成BeanItem),調用setItemDataSource。visibleItemProperties 不知什么用途。但是總之,看起來 setInternalValue,setVisibleItemProperties是一起用的。注釋說這個方法是在Form被當作 Field使用時用的。
private Field getFirstField() {
得到 propertyIds 第一個元素對應的 Field
protected void setFormDataSource(Object data, Collection properties) {
如果data是一個Item,用之,否則生成BeanItem。調用setItemDataSource
public Collection getVisibleItemProperties() {
return visibleItemProperties;
}
只是getter
public void setVisibleItemProperties(Collection visibleProperties) {
設置 visibleItemProperties,然后調用AbstractField的getValue,如果為空則還是用itemDataSource,否則用getValue,去調用 setFormDataSource(value, getVisibleItemProperties());
public void setVisibleItemProperties(Object[] visibleProperties) {
將array轉成collection然后設置
public void focus() {
focus 在第一個 field
public void setTabIndex(int tabIndex) {
public void setImmediate(boolean immediate) {
依次調用各個field
protected boolean isEmpty() {
依次判斷各個field
public void addValidator(Validator validator) {
不支持。
public Layout getFooter() {
簡單,不再詳述。
public void setFooter(Layout newFormFooter) {
簡單,不再詳述。
public void setEnabled(boolean enabled) {
調用 super.setEnabled,奇怪,似乎沒有調用各個 field 的setEnabled()。