6

我认为这对我来说不是一个特定的问题;每个人之前可能都遇到过这个问题。为了正确地说明它,这是一个简单的 UI:

替代文字

如您所见,这两个微调器正在控制一个变量——“A”。唯一的区别是他们使用不同的视图来控制它。

由于这两个微调器的显示值是同步的,因此会出现循环事件。

如果我更改顶部微调器,“A”将被更改,底部微调器的值也将相应更新。但是,更新底部微调器的调用(例如 setValue)也会触发另一个事件,指示顶部微调器根据底部微调器的值进行更新。因此创建了一个坏循环,最终可能导致 StackOverFlow 异常。

我之前的解决方案有点麻烦:我放置了一个保护布尔值来指示是否应该执行第二次更新调用。

现在我想问“我怎样才能优雅地处理这种情况?一般来说,不是特定于微调器)”

谢谢


更新:

由于我有 2 个答案建议我使用观察者结构,因此我不得不说一下。

就像我所说的那样,它很棒,但远非完美。不仅因为其固有的复杂性,而且还因为它无法解决问题

为什么?要了解原因,必须实现 Java Swing 中 View 和 Model-Controller 的紧密耦合。让我们以我的微调器 UI 为例。假设变量 A 实际上是一个 Observer 对象。然后,在从顶部微调器触发第一个状态更改事件后,观察者“A”将更新其值并触发 PropertyChange 事件以通知底部微调器。然后是第二次更新,它更新了底部微调器的视图。但是,更改底部微调器的视图不可避免地会触发一个冗余事件,该事件将尝试再次设置“A”的值。之后,完全构建了致命循环,将抛出堆栈溢出。

理论上,观察者模型试图通过引入 2 条独立的反馈路径来解决直接循环。链式更新几率(在事件响应代码中)隐含地形成了连接两条路径的桥梁,再次形成一个循环。

4

9 回答 9

3

回到 Model-View-Controller,想想你的 Model 是什么,你的 View 是什么。

在您当前的实现中,您有两个模型(每个 Spinner 控件一个),它们通过 View 层同步。

您应该做的是共享相同的支持模型。对于减去值的微调器,创建原始模型的代理。IE:

class ProxySpinnerModel implements SpinnerModel {
    getValue() { return originalSpinner.getValue() - 10 }
    setValue(v) { originalSpinner.setValue(v+10) }
}

spinnerA = new JSpinner()
spinnerB = new JSpinner( new ProxySpinnerModel( spinnerA.getModel() ) )

现在,您不需要添加侦听器,因为它们都在使用相同的模型,并且默认实现(originalModel)已经具有向视图触发的更改侦听器。

于 2010-03-09T02:33:33.207 回答
3

问题解决了


我有很多不同的建议。特别是,我要感谢 Marc W 和 Reverend Gonzo。我是来对这些想法做一个总结的;这可以节省您浏览大量文本的时间。

如果你仔细地解耦 View 和 Model-Controller,这个问题可以很容易地绕过。死循环是由相关写入引起的:write_1 -> write_2 -> write_1 ->.... 直观地说,打破依赖可以优雅地解决问题。

如果我们深入研究这个问题,我们会发现更新相应的视图并不一定涉及外部写入调用。实际上,视图只取决于它所代表的数据。知道了这一点,我们就可以重写逻辑如下write_1 -> read_2 & write_2 -> read_1

为了说明这个想法,让我们比较不同海报提到的 3 种方法: alt text http://www.freeimagehosting.net/uploads/2707f1b483.png

如您所见,只有代理视图才能解决所有依赖关系,因此它是解决这个问题的通用解决方案。

在实践中,它可以像这样实现(在您的事件响应代码中):

 setValue(newValue);
 anotherSyncUI.parse();  // not anotherSyncUI.setValue() any more
 anotherSyncUI.repaint();

没有更多的循环。解决了。

于 2010-03-09T03:14:01.543 回答
2

这有点复杂,但你可以把它A变成一个可观察的对象。然后两个微调器(或任何需要根据A' 的值更新自身的东西)都会观察到A. 每当A更改时,微调器(或再次,任何对象)都会更新自己以反映A. 这将微调器的逻辑彼此分离。在您的示例中,微调器不应相互耦合,因为它们彼此之间确实没有任何关系。相反,它们都应该简单地绑定A并单独处理自己的视图更新。

每当第一个微调器中的值发生更改时,您只需更新A' 值以匹配它。每当第二个微调器中的值发生更改时,您当然会在将其分配给A.

更新

作为对您原始问题的更新的回应,我的回答是微调器不听取彼此的更改事件。每个微调器都有一个单独的事件处理方法。用户单击微调器中的向上或向下箭头会生成与以编程方式调用微调器不同的事件,setValue对吗?如果微调器彼此完全独立,则不会出现无限循环。

于 2010-03-08T04:12:03.243 回答
1

例如,对于第二个微调器,计算 A-10,然后将其与微调器的当前值进行比较。如果相同,则什么都不做,结束无限循环。对于第一个微调器也是如此。

我认为还有一些方法可以以不触发事件的方式更新微调器的模型,但我不知道它们是否在我的脑海中。

于 2010-03-08T03:43:02.973 回答
1

通常,您的模型不应由您的 GUI 定义。即,支持每个 JSpinner 的 SpinnerModel 不应该是您的值 A。(这将是对特定视图的非常不优雅的紧耦合依赖。)

相反,您的值 A 应该是 POJO 或对象的属性。在这种情况下,您可以向其添加 PropertyChangeSupport。(并且在任何情况下都可能已经这样做了,因为如果您的程序的其他部分更改了 A,您希望您的微调器自动更新)。

我意识到这与 Marc W 的回答相似,您担心它“复杂”,但 PropertyChangeSupport 几乎为您完成了所有工作。

事实上,对于非常简单的情况,您可以只使用一个类,将“setProperty”方法连接到“firePropertyChange”调用(以及将值存储在 HashMap 中以用于任何“getProperty”调用)。

于 2010-03-08T08:24:56.020 回答
1

对两个 JSpinner 使用一个 SpinnerModel。请参阅以下代码: 请注意,每次由其中一个 JSpinner 定义新值时,只调用一次 setValue()。

import java.awt.BorderLayout;

import javax.swing.*;

public class Test {
    public static void main(String[] args) {
        JFrame jf = new JFrame();
        SpinnerModel spinModel = new MySpinnerModel();
        JSpinner jspin1 = new JSpinner(spinModel);
        JSpinner jspin2 = new JSpinner(spinModel);
        jf.setLayout(new BorderLayout());
        jf.add(jspin1, BorderLayout.NORTH);
        jf.add(jspin2, BorderLayout.SOUTH);
        jf.pack();
        jf.setVisible(true);
        jf.setDefaultCloseOperation(3);
    }
}

class MySpinnerModel extends AbstractSpinnerModel {
    private int _value = 0;
    private int _min = 0;
    private int _max = 10;

    @Override
    public Object getNextValue() {
        if (_value == _max) {
            return null;
        }
        return _value + 1;
    }

    @Override
    public Object getPreviousValue() {
        if (_value == _min) {
            return null;
        }
        return _value - 1;
    }

    @Override
    public Object getValue() {
        return _value;
    }

    @Override
    public void setValue(Object value) {
        System.out.println("setValue(" + value + ")");
        if (value instanceof Integer) {
            _value = (Integer) value;
            fireStateChanged();
        }
    }
}
于 2010-03-08T12:37:49.207 回答
1

看来你真的在观察错误的事情。从给出的示例中,我假设您要检测的是用户对控件的操作,而不是值本身的变化。正如您所概述的,模型中的变化反映在微调器的值中,正是这形成了事件的无限循环。

但是,深入研究 UI 实现可能不是您想要的答案。在这种情况下,我会说你能做的最好的事情就是你当前的后卫解决方案,或者更好地将逻辑提取到你的模型中(类似于 Marc 和 William 所说的)。如何做到这一点将取决于所提供拼图的特定实现背后的“真实世界”模型。

于 2010-03-08T12:44:28.070 回答
0

我真的不想解决你的问题,但我觉得这很有趣。我已经面对它,并且每次都以不同的方式解决它。但是当我想到“为什么?” 而不是关于“如何?” 我很困惑。
之所以存在这个问题,是因为我使用了一个必须帮助我的自动化(MVC),而且正是以这种方式。如何使用组件的艺术使这种自动化成为漂亮代码的障碍。
为什么 set#setEvent()必须产生与 GUI 操作相同的事件?

于 2010-03-08T08:12:37.220 回答
0

虽然,我的观点也非常接近观察者模式,但它比那更轻!

将 A 作为带有 setter 的变量

private Integer A;

setA(int A)
{
  this.A = A;
  refreshSpinners();
}


refreshSpinners()
{
  setSpinnerA();
  setSpinnerAMinus10();
}

setSpinnerA()
{
   // show value of A
}

setSpinnerAMinus10()
{
   // show value of A-10
}
于 2010-03-08T11:59:54.003 回答