有没有一种方法可以更改 Java 属性 而无需向其侦听器触发值更改事件?

Is there a way to change a Java property without firing a value changed event to it's listeners?

我想做什么

我正在寻找一种方法来更改 属性,而无需调用侦听器的已更改方法。

更具体地说,我正在尝试实现 undo/redo 功能。我实现它的方式如下,在一个 BooleanProperty 和 JavaFX CheckBox.

的例子中
  1. CheckBoxselectedProperty 是通过鼠标点击改变的。
  2. A BooleanProperty(实际上是 JavaFX SimpleBooleanProperty)已更改,因为它双向绑定到 selectedProperty
  3. BooleanPropertyChangeListener 注册了这个并在应用程序的 undoStack 上添加了一个 CommandCommand 存储 属性、旧值和新值。
  4. 用户点击撤消按钮
  5. 应用程序通过按钮从堆栈中取出最后一个 Command 并调用它的 undo() 方法。
  6. undo() 方法将 BooleanProperty 改回。
  7. ChangeListener 再次注册此更改并创建新的 Command
  8. 形成无限循环

我的 Hacky 解决方案

我的方法是将 ChangeListener 传递给 Command 对象。然后 undo() 方法首先删除 ChangeListener,更改 BooleanProperty 然后再次添加 ChangeListener
ChangeListener 传递给 Command 感觉不对而且很老套(在我在第 3 步的实际实现中,ChangeListenerCommand 现在大家需要知道的是ChangeListener)

我的问题

真的是这样吗?有没有办法改变 步骤 6 中的 属性 并告诉它 而不是 通知它的听众?或者至少是为了获得它的听众?

如您所述,没有受支持的绕过侦听器的方法。您只需要将此逻辑构建到您的 undo/redo 机制中。这个想法基本上是在执行 undo/redo 时设置一个标志,如果是这样则不将更改添加到堆栈中。

这是一个非常简单的示例:请注意,这不是生产质量 - 例如,每次字符更改时,在文本控件中键入内容都会添加到堆栈中(在每次更改时保留当前文本的副本)。在实际代码中,您应该将这些更改结合在一起。

import java.util.Deque;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;

import javafx.beans.property.Property;
import javafx.beans.value.ChangeListener;

public class UndoManager {

    private boolean performingUndoRedo = false ;
    private Deque<Command<?>> undoStack = new LinkedList<>();
    private Deque<Command<?>> redoStack = new LinkedList<>();


    private Map<Property<?>, ChangeListener<?>> listeners = new HashMap<>();

    public <T> void register(Property<T> property) {
        // don't register properties multiple times:
        if (listeners.containsKey(property)) {
            return ;
        }
        // FIXME: should coalesce (some) changes on the same property, so, e.g. typing in a text
        // control does not result in a separate command for each character
        ChangeListener<? super T> listener = (obs, oldValue, newValue) -> {
            if (! performingUndoRedo) {
                Command<T> cmd = new Command<>(property, oldValue, newValue) ;
                undoStack.addFirst(cmd);
            }
        };
        property.addListener(listener);
        listeners.put(property, listener);
    }

    public <T> void unregister(Property<T> property) {
        listeners.remove(property);
    }

    public void undo() {
        if (undoStack.isEmpty()) {
            return ;
        }
        Command<?> command = undoStack.pop();
        performingUndoRedo = true ;
        command.undo();
        redoStack.addFirst(command);
        performingUndoRedo = false ;
    }

    public void redo() {
        if (redoStack.isEmpty()) {
            return ;
        }
        Command<?> command = redoStack.pop();
        performingUndoRedo = true ;
        command.redo();
        undoStack.addFirst(command);
        performingUndoRedo = false ;
    }



    private static class Command<T> {
        private final Property<T> property ;
        private final T oldValue ;
        private final T newValue ;

        public Command(Property<T> property, T oldValue, T newValue) {
            super();
            this.property = property;
            this.oldValue = oldValue;
            this.newValue = newValue;
        }

        private void undo() {
            property.setValue(oldValue);
        }

        private void redo() {
            property.setValue(newValue);
        }

        @Override 
        public String toString() {
            return "property: "+property+", from: "+oldValue+", to: "+newValue ;
        }
    }
}

这是一个快速测试工具:

import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ComboBox;
import javafx.scene.control.ListCell;
import javafx.scene.control.TextArea;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;

public class UndoExample extends Application {

    @Override
    public void start(Stage stage) throws Exception {
        ComboBox<Color> textColor = new ComboBox<Color>();
        textColor.getItems().addAll(Color.BLACK, Color.RED, Color.DARKGREEN, Color.BLUE);
        textColor.setValue(Color.BLACK);
        textColor.setCellFactory(lv -> new ColorCell());
        textColor.setButtonCell(new ColorCell());
        CheckBox italic = new CheckBox("Italic");
        TextArea text = new TextArea();
        updateStyle(text, textColor.getValue(), italic.isSelected());

        ChangeListener<Object> listener = (obs, oldValue, newValue) -> 
            updateStyle(text, textColor.getValue(), italic.isSelected());
        textColor.valueProperty().addListener(listener);
        italic.selectedProperty().addListener(listener);

        UndoManager undoMgr = new UndoManager();
        undoMgr.register(textColor.valueProperty());
        undoMgr.register(italic.selectedProperty());
        undoMgr.register(text.textProperty());

        Button undo = new Button("Undo");
        Button redo = new Button("Redo");
        undo.setOnAction(e -> undoMgr.undo());
        redo.setOnAction(e -> undoMgr.redo());

        HBox controls = new HBox(textColor, italic, undo, redo);
        controls.setSpacing(5);

        BorderPane root = new BorderPane(text);
        root.setTop(controls);

        Scene scene = new Scene(root);
        stage.setScene(scene);
        stage.show();
    }

    private void updateStyle(TextArea text, Color textColor, boolean italic) {
        StringBuilder style = new StringBuilder()
                .append("-fx-text-fill: ")
                .append(hexString(textColor))
                .append(";")
                .append("-fx-font: ");
        if (italic) {
            style.append("italic ");
        }
        style.append("13pt sans-serif ;");
        text.setStyle(style.toString());
    }

    private String hexString(Color color) {
        int r = (int) (color.getRed() * 255) ;
        int g = (int) (color.getGreen() * 255) ;
        int b = (int) (color.getBlue() * 255) ;
        return String.format("#%02x%02x%02x", r, g, b);
    }

    private static class ColorCell extends ListCell<Color> {
        private Rectangle rect = new Rectangle(25, 25);
        @Override
        protected void updateItem(Color color, boolean empty) {
            super.updateItem(color, empty);
            if (empty || color==null) {
                setGraphic(null);
            } else {
                rect.setFill(color);
                setGraphic(rect);
            }
        }       
    }

    public static void main(String[] args) {
        Application.launch(args);
    }

}

没有“黑客”几乎不可能做到这一点!
但是,还有一个更短的解决方案,通过使用反射:

/**
 * Set the value of property without firing any change event.
 * The value of property will be set via reflection.
 * This property must be "Base" property such as {@link DoublePropertyBase}.
 * 
 * @param property | Property to set!
 * @param newValue | New value of property.
 */
public static <T> void setPropertyWithoutFiringEvent(Property<T> property, T newValue)
{
    Class<?> cls = property.getClass();
    while (cls != null) //While until helper variable is found
    {
        try 
        {
            Field fieldH = cls.getDeclaredField("helper"), fieldV = cls.getDeclaredField("valid");
            fieldH.setAccessible(true);
            fieldV.setAccessible(true);
            
            Object helper = fieldH.get(property), valid = fieldV.getBoolean(property); //Temporary values
            fieldH.set(property, null); //Disabling ExpressionHelper by setting it on null;
            property.setValue(newValue);
            fieldH.set(property, helper); //Setting helper back!

            fieldV.set(property, valid); //Important
            return;
        } 
        catch (Exception e) 
        {
            cls = cls.getSuperclass(); //If not found go to super class of property next time!
        }
    }
    System.err.println("Property " + property + " cant be set because variable \"helper\" was not found!");
}

此函数暂时禁用 ExpressionHelper,它是负责触发更改事件的对象,然后它将更改 属性 的值并重新启用 ExpressionHelper!这将导致不会通知一个更改!
如果反射对你来说不是友好的解决方案,那么就使用上面的解决方案,但是这个更短更简单。