views:

241

answers:

9

Hi fellows, 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.

+1  A: 

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.

Matt McHenry
I used to use comparing method. Before long, i found it not simple as the boolean guarding method. Besides, using the spinner is just to give an example. It is not always spinners. Sometimes, comparing can be difficult or cumbersome.And yes, one of the most elegant way is to block the event firing.
dex
+2  A: 

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.

Marc W
This is an solution with great potentiality and architecturally elegant. It resembles the way how Document and text components are linked together. One major **DRAWBACK** is that creating an observer per variable type can significantly complicate a simple problem. Thus makes it impossible to completely replace the guarding boolean. I gave you a positive vote but i can't mark it as correct answer at this point(may be later). Let's see more suggestions from different ppl's talented brains. :)
dex
I added an update to my answer to reflect the update to your question.
Marc W
That is still problematic. The issue doesn't deal with whether i deploy one or two event-response methods. The very reason is, those two spinners are DEPENDENT, since their views are synchronized.
dex
Putting change listeners on mouse clicks/key presses is an extremely convoluted (an incorrect by Swing best practices) way to capture change events and sync controls.
Milan Ramaiya
dex, as other people here have said as well, you should not be thinking about the spinners as being dependent. Just because they display data from the same source does NOT make them depend on one another. They both *individually* depend on the same data source, and that is all. If you are able to think about it in this way, the problem will become much easier to solve.
Marc W
That's right. I added a new answer to help other ppl easily understand the idea (also save them lots of time from reading our discussion.)
dex
A: 
elou
An interesting thought. However, I didn't feel it's a barrier to a beautiful code but lacking of functionality. For instance, if the event object parameter can **offer more info to reveal the invocation chain**, this problem can be solved peacefully.
dex
+1  A: 

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).

William Billingsley
A: 

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
}
Mohd Farid
Hi, thx but it still has problems. Remember that those spinners not only serves as Views but also Controllers. If a user click on one spinner's button, stack overflow will happen. **For details, pls read my update.**
dex
+1  A: 

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();
        }
    }
}
Morgaelyn
This is a smart synchronization method. However, it's not the answer im looking for. pls read my question carefully. i more care about a generic solution to this kind of issue.
dex
+1  A: 

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.

Steven Mackenzie
True. +1 vote. However, i believe this problem has a general solution. Only we haven't found out.
dex
I also brushed on this in my updated answer (before reading yours, Steven). Don't observe the UI controls; observe the value in the model (which is `A` in this case).
Marc W
+2  A: 

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 sync'ed 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 SpinnerNodel {
    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 defaul implementation (originalModel) already has change listeners which it fires to the view.

Milan Ramaiya
Great! ive made a diagram in my own reply for a comparison with other proposals. suggestions are welcome.
dex
+2  A: 

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

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.

dex
I'm not quite sure this is clearly described at all, but I'm glad you found a solution that you understand! I hope you were able to understand all the concepts presented by the posters here.
Marc W
thx Marc. i believe one proper example is the best way for ppl to understand. :)
dex