Java 中处理循环事件的优雅方法?

发布于 2024-08-24 18:48:05 字数 1082 浏览 8 评论 0原文

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

替代文字

正如您所看到的,这两个旋转器控制着一个变量——“A”。唯一的区别是他们使用不同的视图来控制它。

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

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

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

现在我想问“我如何优雅地处理这种情况?< /strong> (一般来说,不是特定于旋转器)"

thx


更新:

由于我有 2 个答案建议我利用观察者结构,所以我必须说点什么关于它。

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

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

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

i think this not a specific problem to me; everybody might have encountered this issue before.
To properly illustrate it, here's a simple UI:

alt text

As you can see, those two spinners are controlling a single variable -- "A". The only difference is that they control it using different views.

Since these two spinners' displaying values are synchronized, cyclic event shows up.

If i change the top spinner, "A" will be changed and the bottom spinner's value will also be updated accordingly. However, updating the bottom spinner's call (such as setValue) will also trigger another event instructing the top spinner to update based on the bottom spinner's value. Thus creates a bad cycle which can eventually cause a StackOverFlow exception.

My previously solution is kinda cumbersome: i placed a guarding boolean to indicate whether the 2nd updating call should be performed.

Now i'd like to ask "how can i handle such situation elegantly? ( in general, not specific to spinners )"

thx


Update:

Since i've got 2 answers suggesting me to utilize the observer structure, i have to say something about it.

Like what i've said, it's great but far from being perfect. Not only because of its inherent complexity, but also Its inability to solve the problem.

Why? To see the reason, you must realize the tight coupling of the View and Model-Controller in Java Swing. Lets take my spinner UI for an example. Suppose the variable A is actually an Observer object. Then, after firing the first state change event from the top spinner, the Observer "A" will update its value and fire a PropertyChange event to notify the bottom spinner. Then comes the 2nd updating which updates the bottom spinner's View. However, changing bottom spinner's view inevitably triggers a redundant event that will try to set "A"'s value again. Afterwards, the deadly loop is fully constructed and the stack overflow will be thrown.

In theory, the Observer model tries to solve the direct cycle by introducing 2 independent feedback paths. The chained updating odds(in event-response codes) implicitly form a bridge connecting both paths, making a cycle again.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(9

笔落惊风雨 2024-08-31 18:48:05

回到模型-视图-控制器,想想你的模型是什么,你的视图是什么。

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

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

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)已经具有向视图触发的更改侦听器。

Going back to Model-View-Controller, think about what your Model is, and what your View is.

In your current implementation, you have two models (one for each Spinner control), and they're being synced through the View layer.

What you should be doing though is share the same backing model. For the spinner with a subtracted value, create a proxy to the original model. 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() ) )

Now, you don't need to add listeners, since they're both working off the same model and the default implementation (the originalModel) already has change listeners which it fires to the view.

回眸一笑 2024-08-31 18:48:05

问题解决了


我有很多不同的建议。特别,
我要感谢 Marc W &贡佐牧师。我在这里对这些想法做一个总结;这可以节省您浏览大量文本的时间。

如果您仔细解耦视图和模型控制器,则可以轻松绕过此问题。
死循环是由相关写入引起的:write_1 -> write_2 ->; write_1 ->...。直观上,打破依赖可以优雅地解决问题。

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

为了说明这个想法,让我们比较一下不同发帖者提到的 3 种方法:
替代文本 http://www.freeimagehosting.net/uploads/2707f1b483.png

作为您可以看到,只有代理视图才能解决所有依赖关系,因此它是解决这一问题的通用解决方案。

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

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

不再有循环。解决了。

Problem Solved


I've got many different suggestions. Particularly,
i want to thank Marc W & Reverend Gonzo. I'm here to make a summary for these ideas; this can save your time navigating thru big chunk of texts.

This problem can be easily bypassed if you carefully decouple the View and Model-Controller.
The dead cycle is caused by dependent writes: write_1 -> write_2 -> write_1 ->.... Intuitively, breaking the dependency can solve the problem elegantly.

If we look into the problem in depth, we can find updating the corresponding views doesn't necessarily involves an external write call. Actually, a view only depends on the data it's representing. Known this, we can then re-write the logic as follow: write_1 -> read_2 & write_2 -> read_1.

To illustrate this idea, lets compare the 3 methods mentioned by different posters:
alt text http://www.freeimagehosting.net/uploads/2707f1b483.png

As you can see, only the proxied view can solve all the dependency thus it's the generic solution for this knid of problem.

In practice, it can be implemented as something like this (in your event-response codes):

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

No more loops. Solved.

苄①跕圉湢 2024-08-31 18:48:05

这有点复杂,但是您可以使 A 实际上成为一个可观察的对象。然后,两个旋转器(或任何需要根据 A 的值更新自身的东西)都会观察 A。每当 A 发生变化时,微调器(或者再次,任何对象)都会更新自身以反映 A 的新值。这将旋转器的逻辑彼此解耦。在您的示例中,旋转器不应相互耦合,因为它们实际上彼此无关。相反,它们应该简单地绑定到 A 并单独处理自己的视图更新。

每当第一个微调器中的值发生更改时,您只需更新 A 的值即可匹配它。每当第二个微调器中的值发生更改时,您当然会在将其分配给 A 之前将其值加 10。

更新

针对您原来问题的更新,我的答案是旋转器不会监听彼此的更改事件。每个微调器都有一个单独的事件处理方法。用户单击微调器中的向上或向下箭头会生成与以编程方式在微调器上调用 setValue 不同的事件,对吗?如果旋转器彼此完全独立,就不会有无限循环。

It's a bit complicated, but you could make A actually be an object that's observable. Both spinners (or whatever needs to update itself based on A's value) would then observe A. Whenever A changes, the spinners (or again, whatever object) update themselves to reflect the new value of A. This decouples the spinners' logic from one another. In your example here, the spinners should not be coupled to one another because they really have nothing to do with each other. Instead, they should both simply be bound to A and take care of their own view updating individually.

Whenever the value in the first spinner is changed, you would simply update A's value to match it. Whenever the value in the second spinner is changed, you would of course add 10 to its value before assigning it to A.

Update

In response to the update to your original question, my answer is that the spinners do not listen to one another's change events. Have a separate event handling method for each spinner. A user clicking the up or down arrows in the spinner generates a different event than calling setValue on the spinner programmatically, correct? If the spinners are completely independent of one another, there will be no infinite loop.

怎会甘心 2024-08-31 18:48:05

例如,对于第二个旋转器,计算 A-10,然后将其与旋转器的当前值进行比较。如果相同,则不执行任何操作,结束无限循环。对于第一个旋转器也是如此。

我认为还有一些方法可以以不触发事件的方式更新旋转器的模型,但我不知道它们。

E.g. for the second spinner, calculate A-10 and then compare it to the current value of the spinner. If it's the same, do nothing, ending the infinite loop. Similarly for the first spinner.

I think there are also ways to update the spinner's model in a way that doesn't fire an event, but I don't know them off the top of my head.

此生挚爱伱 2024-08-31 18:48:05

对两个 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();
        }
    }
}

Use a single SpinnerModel for both JSpinners. See the following code:
Note that the call to setValue() is only made once each time a new value is defined by one of the JSpinners.

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();
        }
    }
}
囍孤女 2024-08-31 18:48:05

看来你确实观察到了错误的事情。从给出的示例中,我假设您想要检测的是用户在控件上的操作,而不是值本身的变化。正如您所概述的,模型中的更改反映在旋转器的值中,正是这一点形成了事件的无限循环。

然而,进一步深入研究 UI 实现可能不是您想要的答案。在这种情况下,我想说你能做的最好的事情就是要么使用当前的保护解决方案,要么更好地将逻辑提取到模型中(类似于马克和威廉所说的)。如何做到这一点将取决于所提供的难题的特定实现背后的“现实世界”模型。

It seems you're really observing the wrong thing. From the example given I presume what you want to detect is the user's actions on the controls, not the changes in the values themselves. As you've outlined, changes in your model are reflected in the values of the spinners, and it is this which forms the infinite loop of events.

However, diving further into the UI implementation may not be the answer you want. In that case I'd say the best you can do is either your current guard solution, or to better extract the logic into your model (similar to what Marc and William have said). How that can be done will depend on the 'real world' model behind a particular implementation of the provided puzzle.

感受沵的脚步 2024-08-31 18:48:05

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

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

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

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

As a rule, your model should not be defined by your GUI. Ie, the SpinnerModel that backs each JSpinner should not be your value A. (That would be a horribly inelegant tightly coupled dependency on a particular view.)

Instead, your value A should either be a POJO or a property of an object. In which case, you can add PropertyChangeSupport to it. (And presumably have already done so in any case, as you want your spinners to update automatically if A is changed by other parts of your program).

I realise this is similar to Marc W's answer, and you were concerned that it's "complicated", but PropertyChangeSupport does almost all of it for you.

In fact, for trivially simple cases, you can just use a single class that wires a "setProperty" method through to a "firePropertyChange" call (as well as storing the value in a HashMap for any "getProperty" calls).

你丑哭了我 2024-08-31 18:48:05

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

I don't really want to solve your problem but I find it interesting. I have already been confront to it and solved it each time a different way. But when I think about the 'why ?' and not about the 'how ?' am staying perplexed.
This problem only exists because I am using an automatism (MVC) which had to help me, and exactly in that way. The art how the components are used make this automatism a barrier to a beautiful code.

Why do set #setEvent() has to produce the same event as a GUI action?

情愿 2024-08-31 18:48:05

不过,我的观点也非常接近观察者模式,但它比那轻一点!

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

Though, my opinion is also pretty close to Observer pattern but it is a bit lighter than that!!!

Have A as a variable with 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
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文