Текст Swing JTextField изменил слушателя DocumentListener бесконечный цикл

Хорошо, у меня проблема с прослушивателем событий Swing... Краткое введение Я разрабатываю приложение Java с пользовательским интерфейсом Swing, структурированным по шаблону MVC.

  • MyView -> Текст изменяется пользователем, и представление должно информировать модель контроллером
  • MyModel -> Хранить данные и информировать представление об изменениях через контроллер
  • MyController -> Интерфейс, используемый для информирования модели и представления об изменениях

На основе этих классов модель и представление связаны только через класс контроллера. Класс представления содержит текстовое поле для пользовательского ввода, которое должно обновлять класс модели пользовательским вводом без нажатия кнопки. Это означает, что мне нужен прослушиватель для JTextField, который ожидает ввода/изменения текста пользователем...

Я пробовал DocumentListener, но это не работает, выдается исключение: java.lang.IllegalStateException: Attempt to mutate in notification

Я думаю, что проблема здесь в том, что класс модели также вызывает контроллер, если свойства изменились, и контроллер снова информирует/меняет представление -> Результат: бесконечный цикл

Оба решения, которые я нашел, не сработали для меня:

Swing JTextField при изменении текста

Прослушиватель JTextField при изменении текста, который изменяет текст textField

МояМодель.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 без DocumentListener? Или что я должен изменить в своем шаблоне 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 в сочетании с выполнением инструкций, установленных DocumentListener на EDT!


person 0x1C1B    schedule 13.07.2018    source источник
comment
@camickr Да, конечно, но я не пытался фильтровать текст... Не предполагается, что текст в текстовом поле изменяется, пока пользователь печатает... Модель информирует/меняет текст в JTextField каждый раз когда он изменил себя, потому что есть второй контроллер, который меняет модель по данным сокета... Поэтому необходимо, чтобы модель могла изменить текст JTextField, но в этом особом случае, когда пользователь, а не сокет, меняет JTextField текст, чем не предполагается, что модель снова изменит вид...   -  person 0x1C1B    schedule 13.07.2018
comment
I tried the solution of Peter Walser - Петр предложил два решения. Мы не знаем, что вы пробовали? Я бы использовал первый подход. but I got a new exception:... - уже было дано решение, как избежать этой проблемы. Опубликуйте правильный минимально воспроизводимый пример, демонстрирующий проблему, чтобы нам не пришлось гадать, что вы делаете. Это все, что вам нужно, это JFrame с текстовым полем и кнопкой. Пользователь может ввести текст в текстовое поле, чтобы обновить модель. Кнопка добавит текст прямо в модель. Тогда, возможно, мы сможем помочь отладить.   -  person camickr    schedule 13.07.2018
comment
@camickr Также спасибо за ваше время, я решил это с первым решением   -  person 0x1C1B    schedule 13.07.2018


Ответы (2)


Есть два подхода к правильному решению этой проблемы:

Инициировать событие изменения свойства, только если что-то действительно изменилось

Нет нет смысла уведомлять, если свойство значение такое же, как и раньше (совсем не изменилось). Избегание ненужного события эффективно разорвет ваш цикл:

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

Первый подход довольно прост (но может усложниться при работе со сложными объектами и коллекциями). Второй подход проще и проще по дизайну — представление всегда представляет модель (никакие изменения не пропущены), а каскадные обновления блокируются.

person Peter Walser    schedule 13.07.2018
comment
Я попробовал ваши решения, но получил новое исключение: java.lang.IllegalStateException: Attempt to mutate in notification... Я обновлю вопрос, может быть, вы могли бы взглянуть на него... - person 0x1C1B; 13.07.2018
comment
обновил ответ, он стал еще проще: каскадные обновления при обновлении можно предотвратить, не проверяя флаг «обновить» в слушателях. также должен решить проблему «мутировать в уведомлении» :) - person Peter Walser; 13.07.2018
comment
Это не работает, я не могу понять, как исправить это дерьмо :), я отладил его, и проблема заключается в вызове setText в методе @Override public void modelPropertyChange(final PropertyChangeEvent event), вызываемом измененной моделью... Источником трассировки стека является так же, как прежде... - person 0x1C1B; 13.07.2018
comment
Большое спасибо за ваше время! Ваше первое решение с модификацией модели решило эту проблему... Я предотвращаю решенное исключение, выполняя код для явного информирования модели о EDT, и ваше решение предотвращает бесконечный цикл, большое спасибо! - person 0x1C1B; 13.07.2018

В вашем PropertyChangeListener для свойства «Имя пользователя» вы можете:

  1. удалить DocumentListener из текстового поля
  2. обновить текстовое поле
  3. добавьте DocumentListener обратно в текстовое поле.

Я пробовал DocumentListener, но он не работает, выдается исключение: java.lang.IllegalStateException: попытка мутации в уведомлении

Только для информации, чтобы избавиться от этого сообщения, вы можете обернуть код в Swingutils.invokeLater(), чтобы код выполнялся после завершения выполнения кода прослушивателя. Хотя я думаю, что вы все равно получите свой бесконечный цикл.

person camickr    schedule 13.07.2018
comment
Да, проблема в том, что не JTextField, поскольку виджет зарегистрирован как слушатель для PropertyChangeListener, вместо этого весь класс MyView связан с классом контроллера, а метод @Override public void modelPropertyChange(final PropertyChangeEvent event) также обрабатывает все остальные виджеты... Таким образом, это невозможно простым способом удалить слушатель - person 0x1C1B; 13.07.2018
comment
@ 0x1C1B, кажется, я не совсем понял, вам нужно удалить DocumentListener из текстового поля, а не PropertyListener. Затем, когда текстовое поле обновляется через PropertyChangeEvent, вы не будете снова уведомлять модель об изменении, поэтому у вас нет цикла. - person camickr; 13.07.2018
comment
О, ладно, это логичнее, спасибо, я попробую - person 0x1C1B; 13.07.2018