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

Posted

技术标签:

【中文标题】Swing JTextField 文本更改侦听器 DocumentListener 无限循环【英文标题】:Swing JTextField text changed listener DocumentListener infinity loop 【发布时间】:2018-12-21 22:39:00 【问题描述】:

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

MyView -> 文本由用户更改,视图应通过控制器通知模型 MyModel -> 存储数据并通过控制器通知视图有关更改 MyController -> 用于通知模型和视图更改的接口

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

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

我认为这里的问题是,如果属性更改并且控制器再次通知/更改视图,模型类也会调用控制器 -> 结果:无限循环

我发现的两种解决方案都不适合我:

Swing JTextField on text change

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);
    

问题是当调用文档侦听器时,因为用户输入了模型更改的内容调用视图的属性更改方法,并且视图将文本替换为相同的文本,这再次引发文档更改事件并调用侦听器。 .. 无限循环

我尝试使用ActionListener,它工作正常,但用户有必要按回车键分配更改... 是否有任何其他选项可以在没有 @ 的情况下侦听 JTextField 中的文本更改987654329@?或者我应该通过我的 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.access$500(EventQueue.java:97)
at java.awt.EventQueue$3.run(EventQueue.java:709)
at java.awt.EventQueue$3.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$4.run(EventQueue.java:733)
at java.awt.EventQueue$4.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的指令和第一个解决方案结合执行EDT上DocumentListener设置的指令解决了这个错误!

【问题讨论】:

@camickr 是的,但我没有尝试过滤文本...这并不是要在用户键入时更改文本字段中的文本...模型通知/每次更改自身时都更改JTextField 中的文本,因为有第二个控制器通过套接字的数据更改模型...所以模型必须可以更改 JTextField 文本,但在这种特殊情况下当用户而不是套接字更改 JTextField 文本时,模型并不打算再次更改视图... I tried the solution of Peter Walser - Peter 提出了两种解决方案。不知道你试过哪一种?我会使用第一种方法。 but I got a new exception:... - 已经给出了如何避免该问题的解决方案。发布一个正确的minimal reproducible example 来证明问题,这样我们就不必猜测你在做什么。这就是您所需要的一个带有文本字段和按钮的 JFrame。用户可以在文本字段中输入文本来更新模型。该按钮将直接向模型添加文本。那么也许我们可以帮助调试。 @camickr 也感谢您抽出宝贵的时间,我用第一个解决方案解决了这个问题 【参考方案1】:

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

仅在实际发生更改时触发属性更改事件

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

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);
    

进行单向同步以避免级联

更改模型中的属性可以更改视图中的属性可以更改模型中的属性可以更改... - 这可以快速运行。

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

为此,您需要在控制器上设置一个标志(在您的情况下,是持有微控制器的视图,也就是 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;
    

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

【讨论】:

我尝试了您的解决方案,但出现了一个新异常:java.lang.IllegalStateException: Attempt to mutate in notification...我会更新问题,也许您可​​以看看... 更新了答案,它甚至变得更简单:可以防止更新时的级联更新,而无需检查侦听器中的“更新”标志。也应该解决“通知中的变异”问题:) 它不起作用我不知道如何解决这个问题 :),我调试了它,问题是 @Override public void modelPropertyChange(final PropertyChangeEvent event) 方法中的 setText 调用由更改模型...堆栈跟踪的来源与之前相同... 非常感谢您的宝贵时间!您修改模型的第一个解决方案解决了这个问题...我通过执行代码以在 EDT 上显式通知模型来防止已解决的异常,您的解决方案可以防止无限循环,非常感谢!【参考方案2】:

在“用户名”属性的 PropertyChangeListener 中,您可以:

    从文本字段中删除DocumentListener 更新文本字段 将DocumentListener 添加回文本字段。

我尝试了 DocumentListener 但它不起作用,抛出异常:java.lang.IllegalStateException: Attempt to mutate in notification

仅供参考,要摆脱该消息,您可以将代码包装在 Swing 实用程序中。invokeLater() 以便在侦听器代码完成执行后执行代码。虽然我认为你仍然会得到你的无限循环。

【讨论】:

是的,问题在于 JTextField 不是作为小部件注册为 PropertyChangeListener 的侦听器,而是整个类 MyView 与控制器类连接,方法 @Override public void modelPropertyChange(final PropertyChangeEvent event) 也处理所有其他小部件。 .. 所以不可能以简单的方式移除监听器 @0x1C1B,我想我不清楚,您需要从文本字段中删除 DocumentListener,而不是 PropertyListener。然后,当通过 PropertyChangeEvent 更新文本字段时,您将不会再次通知模型更改,因此您没有循环。 哦,好吧,这更有意义,谢谢,我会试试的

以上是关于Swing JTextField 文本更改侦听器 DocumentListener 无限循环的主要内容,如果未能解决你的问题,请参考以下文章

在 jTextfield 中禁用“粘贴”

文本组件

将文件路径拖放到 Java Swing JTextField

Java JTextField文本框实时监控

使用类属性的多个类似 DocumentListeners - 如何将它们压缩为一个侦听器?

如何避免 Swing 中的无限更新循环?