Swing的JList的MVC实现有问题吗?

11

前段时间我在这里提出了一个问题。所有的解决方案都是一些变通方法。

现在这不应该再是这样的。我感觉有些不对劲,但我无法确定是 Swing 的 MVC 模型本身有问题,还是我的思维方式有问题。

问题如下:我使用一个 JList 实现了文档页面缩略图列表,当用户从列表中选择另一个缩略图时,相应页面就会被加载。为此,我向 JList 添加了一个 ListSelectionListener,当选择改变时,它就会加载相应页面。但是,用户也可以使用其他控件来更改页面,我希望在缩略图列表中反映出这一点,即选中当前页面的缩略图。所以我通过调用 setSelectedIndex() 来更新 JList。但不幸的是,这会产生一个不必要的 ListSelectionEvent,导致监听器重新加载页面。

那么问题出在哪里呢?我只是从其他地方更改了模型,自然希望视图更新自己,但我并不希望它触发事件。Swing 是不是没有正确实现 MVC?还是我错过了某个关键点?


感谢大家提供的出色答案!我接受了@britishmutt的答案,因为它是最详细和有见地的,并且包含最干净的解决方案。问题在于加载页面的组件应该看到它被请求加载相同的页面并且不应该这样做。链接非常有用。我仍然认为Swing的MVC模型有缺陷。他们应该走传统的路线。他们的模型似乎比它值得的麻烦多了。 - Andrei Vajna II
我在监听器更新方面遇到了同样的问题好几次。想象一下,如果你有N个组件需要相互更新...即使你检查真正的显示更改来决定是否触发事件,在其他N-1个组件得到更新后,仍会有N-1个事件被触发。 - Timmos
7个回答

10
这是许多Swing程序员必须面对的问题:多个控件修改相同的数据,然后在每个控件中反映更新。在某个时候,必须有某种东西对将应用于模型的更新具有最终否决权:不论这个东西是什么,它都需要能够处理多个(可能是冗余的甚至是相互矛盾的)更新,并决定如何处理它们。这可以发生在模型层,但理想情况下应该是控制器来完成这项工作 - 毕竟,这是业务逻辑最可能存在的地方。
在这方面,Swing的问题在于,MVC的控制器部分通常在视图组件和模型之间有所分裂,因此很难将逻辑集中化。在某种程度上,Action接口通过将逻辑放在一个位置并允许其被不同组件共享来纠正这个问题,但这对于其他类型的事件或需要协调的多个不同类别的事件并不起作用。
因此,答案是遵循一种在Swing中暗示但没有明确的模式:仅当状态实际更改时才执行请求的更新,否则不执行任何操作。这在JList本身中就有一个例子:如果您尝试将已选择的索引设置为已选中的相同索引,则不会发生任何事情。不会触发任何事件,也不会进行任何更新:实际上会忽略更新请求。这是一件好事。这意味着您可以在JList上拥有侦听器来响应新选择的项目,然后反过来要求同一个JList重新选择同一项目,并且您不会陷入病态递归循环中。如果应用程序中所有的模型控制器都这样做,那么多个重复事件就没有问题了 - 每个组件只会在需要时更新自己(并随后触发事件),如果它确实进行了更新,那么它可以触发所有更新事件,但只有那些尚未收到消息的组件才会采取任何行动。

我很感谢你详细的回答,这为我带来了很多启示。我一度担心只有我会遇到这个问题,而且因为它已经出现了几次,所以一直困扰着我。你在最后一段中提出的建议是一开始发生的情况。但是尽管它没有进入无限循环,它仍然加载了两次页面。我也不喜欢所有这些事件在各个地方飞来飞去。至少我很高兴这是一个普遍的问题,我现在感觉不那么愚蠢了。 - Andrei Vajna II

5
这是预期行为。
来自模型-视图-控制器[MVC] [维基百科]的内容:
在事件驱动的系统中,当信息发生变化时,模型会通知观察者(通常是视图),以便它们可以做出反应。
因此,当您在JList上调用setSelectedIndex时,您正在更新其模型,然后该模型会通知每个ListSelectionListener。如果您可以“悄悄地”更新模型而不让任何人知道,那么这就不是MVC了。

很棒的引言!你说得完全正确。但是现在我想了想,JList是一个视图,所以它必须被更新。但是ListSelectionListener不是一个视图,它是一个控制器。所以我认为这就是问题所在。就像@OscarRyz所说,控制器和视图是密切相关的。实际上,控制器与模型耦合在一起。奇怪!因为控制器应该监听用户操作(比如点击另一个项目),而不是模型中的变化。所以我认为这就是问题所在。应该有一个addItemClickedListener()之类的方法,而不是addListSelectionListener()。 - Andrei Vajna II

3

Swing不完全符合MVC模式,但它的根源可以追溯到MVC模式(与其他MVC模式相比,Swing中的视图和控制器更为相关。详见Swing架构)。

但这似乎并不是你面临的问题。听起来你需要在事件监听器中进行验证,判断事件的类型,并决定是否忽略它:如果事件是由列表发起的,则执行更改。如果是由其他控件触发的,则不执行。


我不认为我能够检查它的来源。该事件是由JList(或者更确切地说,由其模型)创建的,但如果用户单击项目,则情况相同。该事件之前没有任何信息。 - Andrei Vajna II
2
重要的不是事件来自哪里,而是这个事件是否会导致模型状态和显示发生变化。最终负责刷新文档显示的代码需要知道当前正在显示哪个文档,如果被要求再次显示相同的文档,则应忽略该事件。 - Stewart Murrie

2
我仍然感觉在这里有某种概念上的问题。
我理解你的感受,但考虑到你不只是有一个简单的 JList 观察一个 ListSelectionModel,而是有一个 JList 和其他一些控件观察一个混合选择模型。在 @Taisin's example 中,这个混合模型是一个扩展了 DefaultListSelectionModel 并允许静默更改的 CustomSelectionModel。当兼容时,也可以分享一个模型,就像这个 question & answer 和这个教程中的 SharedModelDemo 所建议的一样。
供参考,此 线程 引用了文章 Java SE 应用程序设计与 MVC:应用程序设计问题,该文章更详细地讨论了这个问题。

谢谢你的理解!是的,我认为这似乎是最清晰的解决方案。但如果你仔细想想,就像你在解决Swing模型中的概念错误。在你提到的文章中,这是第三步:“更新模型。它通知控制器其属性的更改。”这不是MVC。模型永远不会通知控制器。它通知视图。通过覆盖模型以不抛出更新,你有效地修复了在第三步存在的bug。 - Andrei Vajna II
你是正确的,它并不是纯粹的MVC模式;它是一种可分离的模型架构,引用自@OscarRyz:http://java.sun.com/products/jfc/tsc/articles/architecture/#separable - trashgod
甚至在这里更清楚:http://www.oracle.com/technetwork/articles/javase/index-142890.html#3 我不知道他们为什么选择了这种设计。他们所说的原因是“使用这种修改后的MVC有助于更完全地将模型与视图解耦。”但是这为什么是好的?它解决了什么问题?目前对我来说,它似乎更麻烦。 - Andrei Vajna II

2

@dogbane所说的没错。

但是要解决问题,您需要在监听器期间添加某种状态检查,以查看事件是否应该被忽略。ListSelectionEvent有一个getValueAdjusting()方法,但那主要是内部使用的。您需要自己模拟它。

例如,当您从外部选择更新列表时,您将拥有以下代码...

try {
    setSelectionAdjusting(true);
    /* ... your old update code ... */
} finally {
    setSelectionAdjusting(false);
}

在ListSelectionListenerEvent的更新代码中

public void valueChanged(ListSelectionEvent e) {
    if (!isSelectionAdjusting()) {
        /* ... do what you did before ...*/
    }
}

作用域和访问问题留给读者自己思考。你需要编写setSelectionAdjusting方法,并可能将其设置在其他对象上。


听起来像是另一个黑客攻击。我仍然觉得这里在概念上有些问题。 - Andrei Vajna II

2
我通常会这样做:
    public class MyDialog extends JDialog {
        private boolean silentGUIChange = false;

    public void updateGUI {
        try {
            silenGUIChange = true;

            // DO GUI-Updates here:
            textField.setText("...");
            checkBox.setSelected (...);

        }
        finally {
            silentGUIChange = false;
        }
    }

    private void addListeners () {
        checkBox.addChangeListener (new ChangeListener () {
           public void stateChanged (ChangeEvent e) {
              if (silentGUIChange)
                 return;

              // update MODEL
              model.setValue(checkBox.isSelected());
          }
         });
    }

}

0
通常监听器的工作方式是每当它等待的事件发生时就会“触发”。如果我不得不猜测,那可能是你对事情的误解。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接