如何在ReactFX中正确组合多个属性更改流以用于UndoFX(或任何用例)?
以下简要说明了我要完成的工作(完整示例代码为posted at GitHub):
有一个示例模型有两个属性。为简单起见,它们是双重属性
public class DataModel {
private DoubleProperty a, b;
//...
//with appropriate getters, setters, equals, hashcode
//...
}
在示例代码中,有一些按钮可以更改其中一个或两个属性。如果改变是什么,我想撤消对两者的更改。
根据UndoFX示例,还有从基类继承的每个(也在此缩写)的更改类:
public abstract class ChangeBase<T> implements UndoChange {
protected final T oldValue, newValue;
protected final DataModel model;
protected ChangeBase(DataModel model, T oldValue, T newValue) {
this.model = model;
this.oldValue = oldValue;
this.newValue = newValue;
}
public abstract ChangeBase<T> invert();
public abstract void redo();
public Optional<ChangeBase<?>> mergeWith(ChangeBase<?> other) {
return Optional.empty();
}
@Override
public int hashCode() {
return Objects.hash(this.oldValue, this.newValue);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final ChangeBase<?> other = (ChangeBase<?>) obj;
if (!Objects.equals(this.oldValue, other.oldValue)) {
return false;
}
if (!Objects.equals(this.newValue, other.newValue)) {
return false;
}
if (!Objects.equals(this.model, other.model)) {
return false;
}
return true;
}
}
public class ChangeA extends ChangeBase<Double> {
//...
//constructors and other method implementations
//..
@Override
public void redo() {
System.out.println("ChangeA redo "+this);
this.model.setA(this.newValue);
}
}
public class ChangeB extends ChangeBase<Double> {
//...
//constructors and other method implementations
//...
@Override
public void redo() {
System.out.println("ChangeA redo "+this);
this.model.setB(this.newValue);
}
}
所有更改都实现了一个接口
public interface UndoChange {
public void redo();
public UndoChange invert();
public Optional<UndoChange> mergeWith(UndoChange other);
}
在阅读完文档之后,我开始创建一个事件流来捕获每个属性的更改:
EventStream<UndoChange> changeAStream =
EventStreams.changesOf(model.aProperty())
.map(c -> new ChangeA(model, (Change<Number>)c));
EventStream<UndoChange> changeBStream =
EventStreams.changesOf(model.bProperty())
.map(c -> new ChangeB(model, (Change<Number>)c));
我的第一次尝试是像这样合并流
EventStream<UndoChange> bothStream = EventStreams.merge(changeAStream, changeBStream);
在这种情况下会发生的情况是,如果A和B属性同时更改,则流中将有两个更改,并且每个都将单独撤消而不是一起撤消。每次调用setter都会对相应的流进行更改,然后将其发送到bothStream
,然后包含两个单独的事件而不是一个。
经过多次阅读后,我尝试将流和地图组合成一个单独的更改对象:
EventStream<UndoChange> bothStream = EventStreams.combine(changeAStream, changeBStream).map(ChangeBoth::new);
其中ChangeBoth
定义为:
public class ChangeBoth implements UndoChange {
private final ChangeA aChange;
private final ChangeB bChange;
public ChangeBoth(ChangeA ac, ChangeB bc) {
this.aChange = ac;
this.bChange = bc;
}
public ChangeBoth(Tuple2<UndoChange, UndoChange> tuple) {
this.aChange = ((ChangeBoth)tuple.get1()).aChange;
this.bChange = ((ChangeBoth)tuple.get2()).bChange;
}
@Override
public UndoChange invert() {
System.out.println("ChangeBoth invert "+this);
return new ChangeBoth(new ChangeA(this.aChange.model, this.aChange.newValue, this.aChange.oldValue),
new ChangeB(this.bChange.model, this.bChange.newValue, this.bChange.oldValue));
}
@Override
public void redo() {
System.out.println("ChangeBoth redo "+this);
DataModel model = this.aChange.model;
model.setA(this.aChange.newValue);
model.setB(this.bChange.newValue);
}
//...
// plus appropriate mergeWith, hashcode, equals
//...
}
这会导致IllegalStateException: Unexpected change received
被抛出。经过一番挖掘,我确定了为什么会发生这种情况:当ChangeBoth
被撤消时(通过invert()
和redo()
调用),它会将每个属性设置回旧值。但是,当它设置每个属性时,更改将通过流回发,从而在将两个属性设置为旧值之间将新的ChangeBoth
放入流中。
回到我的问题:做正确的方法是什么?有没有办法组合将两个属性的流更改为一个不会导致此问题的更改对象?
根据Tomas的回答,我添加/更改了以下代码(注意:repo中的代码已更新):
changeAStream
和changeBstream
保持不变。
而不是组合流,我做了Tomas建议并创建了一个二元运算符,以将两个更改减少为一个:
BinaryOperator<UndoChange> abOp = (c1, c2) -> {
ChangeA ca = null;
if(c1 instanceof ChangeA) {
ca = (ChangeA)c1;
}
ChangeB cb = null;
if(c2 instanceof ChangeB) {
cb = (ChangeB)c2;
}
return new ChangeBoth(ca, cb);
};
并将事件流更改为
SuspendableEventStream<UndoChange> bothStream = EventStreams.merge(changeAStream, changeBStream).reducible(abOp);
现在按钮操作未在setonAction
中实施,而是由事件流处理
EventStreams.eventsOf(bothButton, ActionEvent.ACTION)
.suspenderOf(bothStream)
.subscribe((ActionEvent event) ->{
System.out.println("stream action");
model.setA(Math.random()*10.0);
model.setB(Math.random()*10.0);
});
这适用于合适地组合事件,但撤消A + B更改仍会中断。它适用于单独的A和B更改。这是两个A + B更改然后撤消
的示例A+B Button Action in event stream
Change in A stream
Change in B stream
A+B Button Action in event stream
Change in A stream
Change in B stream
ChangeBoth attempting merge with combinedeventstreamtest.ChangeBoth@775ec8e8... merged
undo 6.897901340713284 2.853416510829745
ChangeBoth invert combinedeventstreamtest.ChangeBoth@aae83334
ChangeA invert combinedeventstreamtest.ChangeA@32ee049a
ChangeB invert combinedeventstreamtest.ChangeB@4919dd13
ChangeBoth redo combinedeventstreamtest.ChangeBoth@b2155b1e
Change in A stream
Exception in thread "JavaFX Application Thread" java.lang.IllegalArgumentException: Unexpected change received.
Expected:
combinedeventstreamtest.ChangeBoth@b2155b1e
Received:
combinedeventstreamtest.ChangeA@2ad21e08
Change in B stream
托马斯很好地指出了解决方案(我应该意识到)。只需在执行redo()
时暂停:
UndoManager<UndoChange> um = UndoManagerFactory.unlimitedHistoryUndoManager(
bothStream,
c -> c.invert(),
c -> bothStream.suspendWhile(c::redo),
(c1, c2) -> c1.mergeWith(c2)
);
答案 0 :(得分:1)
因此,任务是将处理按钮单击期间从bothStream
发出的更改合并为一个。
您需要一个功能,将两个UndoChange
减少为一个:
BinaryOperator<UndoChange> reduction = ???; // implement as appropriate
让bothStream
在“暂停”时将更改减少为一个:
SuspendableEventStream<UndoChange> bothStream =
EventStreams.merge(changeAStream, changeBStream).reducible(reduction);
现在您只需在处理按钮点击时暂停bothStream
。这可以这样做:
EventStreams.eventsOf(bothButton, ActionEvent.ACTION) // Observe actions of bothButton ...
.suspenderOf(bothStream) // but suspend bothStream ...
.subscribe((ActionEvent event) -> { // before handling the action.
model.setA(Math.random()*10.0);
model.setB(Math.random()*10.0);
})
在撤消/重做撤消管理器中的更改时暂停bothStream
,以便在撤消时从bothStream
发出(组合)更改的完全反向合并变更(这是使UndoManager
满意的必要条件)。这可以通过将apply
参数包装到UndoManager
中的bothStream.suspendWhile()
构造函数来完成,例如:
UndoManagerFactory.unlimitedHistoryUndoManager(
bothStream,
c -> c.invert(),
c -> bothStream.suspendWhile(c::redo) // suspend while applying the change
)