Swing JTextField 文本更改监听器 DocumentListener 无限循环

Swing JTextField text changed listener DocumentListener infinity loop

好的,我的 swing 事件侦听器有问题...简短介绍 我开发了一个 Java 应用程序,其中包含一个由 MVC 模式构建的 Swing UI。

基于此classes 模型和视图仅通过控制器连接class。视图 class 包含一个用于用户输入的文本字段,它应该使用用户输入 更新模型 class 而无需 按下按钮。这意味着我需要一个 JTextField 的侦听器,等待用户 input/change 的文本...

我试过DocumentListener但是不行,抛出异常:java.lang.IllegalStateException: Attempt to mutate in notification

我认为这里的问题是,如果属性发生变化,模型 class 也会调用控制器,并且控制器 informs/changes 再次调用视图 -> 结果:无限循环

我找到的两种解决方案都不适合我:

JTextField listener when text changes that modifies textField's text

MyModel.java

public void setHost(String host) // Method called by controller to change model
{
    String oldHost = this.host;
    this.host = host;

    this.firePropertyChange("Host", oldHost, this.host); // Model inform view about changes
}

MyView.java

@Override public void modelPropertyChange(final PropertyChangeEvent event)
{
    // Method used to update view and called by controller

    if(event.getPropertyName().equals("Username"))
    {
        String username = (String) event.getNewValue();
        this.nameField.setText(username);
    }
}

问题是当调用文档侦听器时,因为用户输入了一些东西,模型被更改,调用视图的 属性 changed 方法,视图用相同的文本替换文本,这再次引发文档更改事件并且监听器被称为... Infinity loop

我尝试使用 ActionListener 它工作正常,但用户必须按 return 来分配更改... 是否还有其他选项在没有 DocumentListener 的情况下监听 JTextField 中的文本更改?或者我应该通过我的 MVC 模式更改什么来解决这个问题?

编辑

我尝试了 Peter Walser 的解决方案,但抛出了一个新的异常:

java.lang.reflect.InvocationTargetException
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.x1c1b.carrierpigeon.service.mvc.AbstractController.setModelProperty(AbstractController.java:62)
at org.x1c1b.carrierpigeon.desktop.ui.controller.LoginController.changeUsername(LoginController.java:12)
at org.x1c1b.carrierpigeon.desktop.ui.view.LoginView$UsernameChangedListener.updateFieldState(LoginView.java:221)
at org.x1c1b.carrierpigeon.desktop.ui.view.LoginView$UsernameChangedListener.insertUpdate(LoginView.java:203)
at javax.swing.text.AbstractDocument.fireInsertUpdate(AbstractDocument.java:201)
at javax.swing.text.AbstractDocument.handleInsertString(AbstractDocument.java:748)
at javax.swing.text.AbstractDocument.insertString(AbstractDocument.java:707)
at javax.swing.text.PlainDocument.insertString(PlainDocument.java:130)
at org.x1c1b.carrierpigeon.desktop.ui.util.TextFieldLimit.insertString(TextFieldLimit.java:26)
at javax.swing.text.AbstractDocument.replace(AbstractDocument.java:669)
at javax.swing.text.JTextComponent.replaceSelection(JTextComponent.java:1328)
at javax.swing.text.DefaultEditorKit$DefaultKeyTypedAction.actionPerformed(DefaultEditorKit.java:884)
at javax.swing.SwingUtilities.notifyAction(SwingUtilities.java:1668)
at javax.swing.JComponent.processKeyBinding(JComponent.java:2882)
at javax.swing.JComponent.processKeyBindings(JComponent.java:2929)
at javax.swing.JComponent.processKeyEvent(JComponent.java:2845)
at java.awt.Component.processEvent(Component.java:6316)
at java.awt.Container.processEvent(Container.java:2239)
at java.awt.Component.dispatchEventImpl(Component.java:4889)
at java.awt.Container.dispatchEventImpl(Container.java:2297)
at java.awt.Component.dispatchEvent(Component.java:4711)
at java.awt.KeyboardFocusManager.redispatchEvent(KeyboardFocusManager.java:1954)
at java.awt.DefaultKeyboardFocusManager.dispatchKeyEvent(DefaultKeyboardFocusManager.java:835)
at java.awt.DefaultKeyboardFocusManager.preDispatchKeyEvent(DefaultKeyboardFocusManager.java:1103)
at java.awt.DefaultKeyboardFocusManager.typeAheadAssertions(DefaultKeyboardFocusManager.java:974)
at java.awt.DefaultKeyboardFocusManager.dispatchEvent(DefaultKeyboardFocusManager.java:800)
at java.awt.Component.dispatchEventImpl(Component.java:4760)
at java.awt.Container.dispatchEventImpl(Container.java:2297)
at java.awt.Window.dispatchEventImpl(Window.java:2746)
at java.awt.Component.dispatchEvent(Component.java:4711)
at java.awt.EventQueue.dispatchEventImpl(EventQueue.java:760)
at java.awt.EventQueue.access0(EventQueue.java:97)
at java.awt.EventQueue.run(EventQueue.java:709)
at java.awt.EventQueue.run(EventQueue.java:703)
at java.security.AccessController.doPrivileged(Native Method)
at java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:74)
at java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:84)
at java.awt.EventQueue.run(EventQueue.java:733)
at java.awt.EventQueue.run(EventQueue.java:731)
at java.security.AccessController.doPrivileged(Native Method)
at java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:74)
at java.awt.EventQueue.dispatchEvent(EventQueue.java:730)
at java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:205)
at java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:116)
at java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:105)
at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:101)
at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:93)
at java.awt.EventDispatchThread.run(EventDispatchThread.java:82)
Caused by: java.lang.IllegalStateException: Attempt to mutate in notification
    at javax.swing.text.AbstractDocument.writeLock(AbstractDocument.java:1338)
    at javax.swing.text.AbstractDocument.replace(AbstractDocument.java:658)
    at javax.swing.text.JTextComponent.setText(JTextComponent.java:1669)
    at org.x1c1b.carrierpigeon.desktop.ui.view.LoginView.modelPropertyChange(LoginView.java:76)
    at org.x1c1b.carrierpigeon.service.mvc.AbstractController.propertyChange(AbstractController.java:47)
    at java.beans.PropertyChangeSupport.fire(PropertyChangeSupport.java:335)
    at java.beans.PropertyChangeSupport.firePropertyChange(PropertyChangeSupport.java:327)
    at java.beans.PropertyChangeSupport.firePropertyChange(PropertyChangeSupport.java:263)
    at org.x1c1b.carrierpigeon.service.mvc.AbstractModel.firePropertyChange(AbstractModel.java:27)
    at org.x1c1b.carrierpigeon.desktop.ui.model.LoginModel.setUsername(LoginModel.java:39)
    ... 52 more

好像JTextField的文档在通知模型的时候还是锁着的,因为它调用方法setText抛出异常,这个操作是非法的,但我想不通为什么?!

编辑

现在我通过 Peter Walser 的说明和第一个解决方案结合执行由 DocumentListener 在 EDT 上设置的说明解决了这个错误!

在 "Username" 属性 的 PropertyChangeListener 中,您可以:

  1. 从文本字段中删除 DocumentListener
  2. 更新文本字段
  3. DocumentListener 添加回文本字段。

I tried DocumentListener but it doesn't work, a exception is thrown: java.lang.IllegalStateException: Attempt to mutate in notification

仅供参考,要删除该消息,您可以将代码包装在 Swing utilities.invokeLater() 中,以便在侦听器代码执行完毕后执行代码。虽然我认为你仍然会得到你的无限循环。

有两种方法可以正确解决这个问题:

仅当确实发生某些更改时才触发 属性 更改事件

如果 属性 值与之前 相同(根本没有改变),通知 没有意义。避免不必要的事件将有效地打破你的循环:

public void setHost(String host) {
  // check if property actually changed
  if (Objects.equals(this.host, host) return;
  String oldHost = this.host;
  this.host = host;
  this.firePropertyChange("Host", oldHost, this.host); 
}

或(花哨的紧凑形式):

public void setHost(String host) {
    if (!Objects.equals(this.host, host)) {
        firePropertyChange("Host", this.host, this.host=host);
    }
}

进行单向同步以避免级联

改变模型中的一个属性可以改变视图中的一个属性可以改变模型中的一个属性可以改变... - 这可以快速 运行 圈子。

要打破这些级联,进行单向同步:
当模型通知视图有关更改时,忽略任何级联更新。

为此,您需要在控制器上设置一个标志(在您的例子中,是包含微控制器的视图 a.k.a。Swing 侦听器):

MyView.java:

boolean updating;

@Override public void modelPropertyChange(final PropertyChangeEvent event)
{
    if (updating) {
        // cascading update, ignore
        return;
    }
    updating=true;
    try {
        if(event.getPropertyName().equals("Username")) {
        {
            String username = (String) event.getNewValue();
            this.nameField.setText(username);
        }
        ...
    }
    finally {
        updating=false;
    }
}

第一种方法非常简单(但在处理复杂的对象和集合时会变得复杂)。 第二种方法很简单,设计上更宽容 - 视图始终代表模型(没有遗漏任何更改),并且级联更新被阻止。