问题
如何正确组合ReactFX中的多个属性更改流,以便在UndoFX (或任何用例)中使用?
详细信息
下面简短地解释一下我要完成的任务(完整的示例代码是张贴在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,然后该流包含两个单独的事件,而不是一个。
在进一步阅读之后,我尝试将流组合起来并映射到一个单独的change对象中:
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,将两个属性设置为旧值。
摘要
回到我的问题:正确的方法是什么?是否有一种方法可以将两个属性的流组合成一个不会导致此问题的更改对象?
编辑-尝试1
根据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编辑-尝试2-成功!
Tomas很好地指出了解决方案(我应该意识到这一点)。只需在执行redo()时挂起
UndoManager<UndoChange> um = UndoManagerFactory.unlimitedHistoryUndoManager(
bothStream,
c -> c.invert(),
c -> bothStream.suspendWhile(c::redo),
(c1, c2) -> c1.mergeWith(c2)
);发布于 2017-12-14 17:21:32
因此,任务是将处理按钮单击过程中从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,以便在撤消组合更改(这是使UndoManager高兴所必需的)时,从bothStream发出(组合的)更改的完全相反。这可以通过将apply参数包装到bothStream.suspendWhile()中的UndoManager构造函数来实现,例如:
UndoManagerFactory.unlimitedHistoryUndoManager(
bothStream,
c -> c.invert(),
c -> bothStream.suspendWhile(c::redo) // suspend while applying the change
)https://stackoverflow.com/questions/47803831
复制相似问题