javaswingmodel-view-controllerjtextfielddocumentlistener

Swing JTextField text changed listener DocumentListener infinity loop


Okay I've a problem with a swing event listener... Short introduction I develop a Java application with a Swing UI structured by the MVC pattern.

Based on this classes model and view are only connected through the controller class. The view class contains a text field for user input which should update the model class with the user input without pressing a button. This means I need a listener for JTextField that waits for user input/change of text...

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

I think the problem here is that the model class called also the controller if properties changed and the controller informs/changes the view again -> Result: Infinity loop

Both solutions I found didn't work for me:

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

The problem is when the document listener is called because the user input something the model is changed call the property changed method of view and view replace the text with the same text which raise again a document changed event and the listener is called... Infinity loop

I tried to work with an ActionListener it works fine but it is necessary for the user to press return to assign the changes... Are there any other options to listen for text changes in a JTextField without DocumentListener? Or what should I changed by my MVC pattern to solve this issue?

EDIT

I tried the solution of Peter Walser but a new exception was thrown:

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

It seems like the document of JTextField is still locked during inform the model, because it calls the method setText the exception is thrown and this operation is illegal but I can't figure out why?!

EDIT

For now I solved this bug by the instructions and the first solution of Peter Walser combined with executing the instructions set by the DocumentListener on the EDT!


Solution

  • There are two approaches to properly solve this:

    Fire a property change event only if actually something changed

    There's no point in notifying if the property value is the same as before (not changed at all). Avoiding the unnecessary event will effectively break your loop:

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

    or (fancy compact form):

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

    Do one-way synchronization to avoid cascades

    Changing a propery in the model can change a property in the view can change a property in the model can change... - this can quickly run in circles.

    To break these cascades, do a one-way synchronization:
    while the model notifies the view about changes, ignore any cascading updates.

    To do so, you need a flag on the controller (in your case, the view holding the microcontrollers a.k.a. Swing listeners):

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

    The first approach is pretty straightforward (but can get complicated when dealing with complex objects and collections). The second approach is easy and more forgiving by design - view always represents model (no changes missed), and cascading updates are blocked.