Объединение нескольких потоков изменений в ReactFX
Вопрос
Как правильно объединить несколько потоков изменений свойств в 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 изменяются одновременно, в потоке будут два изменения, и каждое будет отменяться отдельно, а не вместе. Каждый вызов сеттера вносит изменение в соответствующий поток, который затем отправляется 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
быть помещенным в поток между установкой двух свойств обратно к старым значениям.
Резюме
Итак, вернемся к моему вопросу: как правильно это сделать? Есть ли способ объединить, чтобы изменить потоки для двух свойств в один объект изменения, который не вызывает эту проблему?
Изменить - Попытка 1
Согласно ответу Томаса, я добавил / изменил следующий код (ПРИМЕЧАНИЕ: код в репо обновлен):
changeAStream
а также changeBstream
оставаться прежним.
Вместо того, чтобы объединить поток, я сделал, как предложил Томас, и создал бинарный оператор, чтобы свести два изменения к одному:
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 - Успех!!
Томас был достаточно мил, чтобы указать на решение (которое я должен был понять). Просто приостановите, делая redo()
:
UndoManager<UndoChange> um = UndoManagerFactory.unlimitedHistoryUndoManager(
bothStream,
c -> c.invert(),
c -> bothStream.suspendWhile(c::redo),
(c1, c2) -> c1.mergeWith(c2)
);
1 ответ
Таким образом, задача состоит в том, чтобы объединить в одном изменения, которые были получены от bothStream
во время обработки нажатием кнопки.
Вам понадобится функция для уменьшения двух UndoChange
s в один:
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
)