首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >结合ReactFX中的多个更改流

结合ReactFX中的多个更改流
EN

Stack Overflow用户
提问于 2017-12-13 23:47:54
回答 1查看 198关注 0票数 0

问题

如何正确组合ReactFX中的多个属性更改流,以便在UndoFX (或任何用例)中使用?

详细信息

下面简短地解释一下我要完成的任务(完整的示例代码是张贴在GitHub):

有一个有两个属性的示例模型。为了简单起见,它们是双重属性

代码语言:javascript
复制
public class DataModel {
    private DoubleProperty a, b;
    //...
    //with appropriate getters, setters, equals, hashcode
    //...
}

在示例代码中,有一些按钮可以更改其中一个或两个属性。如果是这样的话,我想撤销对这两个方面的更改。

根据UndoFX示例,对于从基类继承的每个类(这里也是缩写),也有一些更改类:

代码语言:javascript
复制
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);
   }   
}

所有更改都实现了一个接口。

代码语言:javascript
复制
public interface UndoChange {
   public void redo();
   public UndoChange invert();
   public Optional<UndoChange> mergeWith(UndoChange other);

}

在阅读了文档之后,我首先创建了一个事件流,用于捕获对每个属性的更改:

代码语言:javascript
复制
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));

我的第一次尝试是像这样合并流

代码语言:javascript
复制
EventStream<UndoChange> bothStream = EventStreams.merge(changeAStream, changeBStream); 

在这种情况下,如果同时更改A和B属性,则流中将有两个更改,每个更改将分别撤消,而不是一起撤消。每次对setter的调用都会在适当的流中进行更改,然后将流发送到bothStream,然后该流包含两个单独的事件,而不是一个。

在进一步阅读之后,我尝试将流组合起来并映射到一个单独的change对象中:

代码语言:javascript
复制
 EventStream<UndoChange> bothStream = EventStreams.combine(changeAStream, changeBStream).map(ChangeBoth::new); 

其中ChangeBoth被定义为:

代码语言:javascript
复制
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中的代码已经更新):

changeAStreamchangeBstream保持不变。

我没有像Tomas建议的那样组合流,而是创建了一个二进制操作符,将两个更改减少为一个:

代码语言:javascript
复制
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);
};

并将事件流更改为

代码语言:javascript
复制
SuspendableEventStream<UndoChange> bothStream = EventStreams.merge(changeAStream, changeBStream).reducible(abOp);

现在按钮操作不是在setonAction中实现的,而是用事件流来处理的。

代码语言:javascript
复制
  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更改的示例,然后是一个撤销

代码语言:javascript
复制
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()时挂起

代码语言:javascript
复制
  UndoManager<UndoChange> um = UndoManagerFactory.unlimitedHistoryUndoManager(
          bothStream, 
          c -> c.invert(), 
          c -> bothStream.suspendWhile(c::redo),
          (c1, c2) -> c1.mergeWith(c2)
  );
EN

回答 1

Stack Overflow用户

回答已采纳

发布于 2017-12-14 17:21:32

因此,任务是将处理按钮单击过程中从bothStream发出的更改组合成一个。

您需要一个函数来将两个UndoChange缩减为一个:

代码语言:javascript
复制
BinaryOperator<UndoChange> reduction = ???; // implement as appropriate

使bothStream将更改减少为一次“暂停”:

代码语言:javascript
复制
SuspendableEventStream<UndoChange> bothStream =
        EventStreams.merge(changeAStream, changeBStream).reducible(reduction);

现在,您只需要在处理按钮单击时挂起bothStream。这样做是可以的:

代码语言:javascript
复制
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构造函数来实现,例如:

代码语言:javascript
复制
UndoManagerFactory.unlimitedHistoryUndoManager(
        bothStream, 
        c -> c.invert(), 
        c -> bothStream.suspendWhile(c::redo)  // suspend while applying the change
)
票数 0
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/47803831

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档