Eclipse 4.6 提供了名为 ISideEffect的数据绑定工具. ISideEffect可以实现当一个或多个观察对象(IObservable)改变时执行特定代码。
ISideEffect很像一个侦听器,但它却不需要开发者像侦听器那样作任何依附对象的动作(addChangeListener/removeChangeListener)。当被监控的观察对象改变时它会自动反应执行指定的代码。
口说无凭,还是举个简单的栗子吧:
下面这个代码片中,当userFirstName或userLastName 改变时会自动更新Label 对象中的内容,
IObservableValue<String> userFirstName = ...
IObservableValue<String> userLastName = ...
Label yourUsername = ...
ISideEffect sideEffect =
ISideEffect.create(
() -> {return "Your username is: " + userFirstName .getValue()+"."+userLastName.getValue();},// 返回观察对象列表
yourUsername::setText// 要执行的动作
);
在这段代码中,一般做数据绑定必须的WidgetProperties,DataBindingContext对象和bindValue方法都没有粗线,就一个简单的ISideEffect.create方法就完成了userFirstName和userLastName -> yourUsername 之间的单向数据绑定。
刚看到的这个例子时,我震惊了。。。尼玛这是何方神器啊?好牛逼啊。
深入研究ISideEffect.create方法的源码,才搞明白原理:
要完全讲清楚它的机制说起来太麻烦也没那能力,就只简单说说它的实现原理了几个关键点吧。
说到底,ISideEffect的实现基本原理还是通过加载侦听器(addChangeListener)到被观察对象来实现数据绑定的。
只不过这载侦听器的动作以及很多相关动作都在开发者没有察觉的情况下被悄悄完成了。
首先调用create方法后,ISideEffect会自动分析并获取第一个参数中涉及的所有被观察对象(IObservable)。
怎么获取的呢?
这就要说到另一个神器ObservableTracker,ObservableTracker中的runAndMonitor方法有一个神奇的功能就是可以返回第一个参数中所有被读取过的IObservable对象列表。
很显然上面的例子中,第一个参数是个lambda表达式,() -> {return "Your username is: " + userFirstName.getValue()+"."+userLastName.getValue();}
,这个表达式用调用了userFirstName.getValue()
和userLastName.getValue()
。
于是ISideEffect就知道需要监控userFirstName 和userLastName这两个Observable对象。
然后就会在userFirstName 和userLastName上添加ChangeListener,当userFirstName 和userLastName中任何一个对象改变时,会先执行第一参数指定的lambda表达式,返回”Your username is: xxxx”,然后执行第二个表达式,将yourUsername内容设置为第一个lambda表达返回的值。
那么再问一句:ObservableTracker.runAndMonitor
又是如何能分析出所有被观察对象的呢?
简单说,这完全依赖于另一个方法的配合ObservableTracker.getterCalled
,所有的IObservable对象都会在getter方法中调用ObservableTracker.getterCalled
,是它收集并向ObservableTracker透露了消息,才让ObservableTracker.runAndMonitor
达成目的。
下面是ObservableTracker.getterCalled
的调用层次结构图
下面是ObservableTracker.getterCalled
的源码
public static void getterCalled(IObservable observable) {
if (observable.isDisposed())
Assert.isTrue(false, "Getter called on disposed observable " //$NON-NLS-1$
+ toString(observable));
Realm realm = observable.getRealm();
if (!realm.isCurrent())
Assert.isTrue(false, "Getter called outside realm of observable " //$NON-NLS-1$
+ toString(observable));
if (isIgnore())
return;
Set<IObservable> getterCalledSet = currentGetterCalledSet.get();
// 下面这句getterCalledSet.add(observable)就像间谍一样把当前的observable对象提供给了ObservableTracker
if (getterCalledSet != null && getterCalledSet.add(observable)) {
// If anyone is listening for observable usage...
IChangeListener changeListener = currentChangeListener.get();
if (changeListener != null)
observable.addChangeListener(changeListener);
IStaleListener staleListener = currentStaleListener.get();
if (staleListener != null)
observable.addStaleListener(staleListener);
}
}
关于ObservableTracker.getterCalled
更详细的说明参见本文最后附参考资料中的《Tracked Getter》
ISideEffect与DataBindingContext 的区别
ISideEffect与原有的DataBindingContext binding机制相比有着明显区别,它们之间一种相互补充的关系:
DataBindingContext实现的是一对一的数据绑定,支持双向数据同步更新,支持数据类型转换、数据验证,几乎方方面面都照顾到了,可以看作是个大而全的体系。
但是这个大而全的体系并不是在所有的场景下用起来都顺手,有时还显得臃肿,
比如我们在很多应用场景下并不需要双向数据同步更新,只需要单向的控制,也不需要类型转换和数据验证,这时DataBindingContext复杂的调用方式就显得麻烦臃肿。(参见我的下一篇博客《jface databinding: Radio Button group及ISideEffect绑定数据对象的例子》中用ISideEffect控制组件visiable状态的例子)。
再比如当多个Observable对象更新时都要同时更新同一个数据对象时(比如状态条),DataBindingContext就要创建多个绑定,好麻烦,这个数据对象就会被短时间内更新多次。
再回头来看ISideEffect,DataBindingContext的缺点就是ISideEffect的优点,我们可以把ISideEffect视为支持一对一、一对多、多对一、多对多的单向数据绑定机制(自然不支持双向数据更新),因为它不局限于一对一的灵活性,所以它没有数据类型转换、数据验证的概念。换个角度来看,可以把ISideEffect理解为一个触发器,当一个或多个Observable对象改变时自动触发执行指定的动作,具体是什么动作,可以是任意的,不一定是数据更新,播放一段音乐也是可以的。。。
前面说过了,在多对一、多对多的场景下,当多个观察对象(IObservable)更新时,ISideEffect会自动响应,所以在短时间内有多个观察对象(IObservable)更新的的情况下,ISideEffect只响应一次,可以避免过多的更新动作,这是DataBindingContext做不到的。
参考资料:
《Tracked Getter》
《ObservableTracker》
《Using the ISideEffect API》