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()。