Is there something wrong with Swing's MVC impl

2019-01-20 08:25发布

问题:

Some time ago I asked this question. All solutions are workarounds.

Now this can't be. I feel that something is wrong here, but I can't tell if it is Swing's MVC model that is conceptually wrong, or if it is my thinking that is conceptually wrong.

Here is the problem again. I am using a JList to implement a list of thumbnails for the pages of a document. If the user selects another thumbnail from the list, that page is loaded. To do this I added a ListSelectionListener to the JList, which when the selection changes, it loads that page. But the user can also change the page using another control. Naturally, I want this to be reflected in the thumbnail list by having that page selected here. So I setSelectedIndex() to update the JList. Unfortunately this has the unwanted effect of raising a ListSelectionEvent which causes the listener to reload the page.

Now what is wrong here? I just changed the model from somewhere else, so naturally I want the view to update itself, but I don't want it to trigger events. Is Swing not implementing MVC right? Or am I missing a point here?

回答1:

This is an issue that a lot of us Swing programmers have to face: multiple controls modifying the same data, with the update then reflected in each control. At some point, something has to have the ultimate veto on what updates will be applied to the model: whatever that something is needs to be able to handle multiple (potentially redundant or even contradictory) updates and decide what to do with them. This could happen in the model layer, but ideally it should be the controller that does this - this piece, after all, is most likely where the business logic resides.

The problem with Swing, in this regard, is that the controller piece of MVC is often split somewhat between the view component and the model so it can be difficult to have that logic centralised. To some extent the Action interface rectifies this for actionPerformed() events by putting the logic in one place and allowing it to be shared by different components, but this doesn't help for the other types of event, or when there are multiple different classes of event that need to be coordinated.

The answer, then, is to follow a pattern that's hinted at in Swing but not made explicit: only perform the requested update if the state will actually change, otherwise do nothing. An example of this is in the JList itself: if you attempt to set the selected index of a JList to the same index that's already selected nothing will happen. No events will be fired, no updates will occur: the update request is effectively ignored. This is a good thing. This means that you can, for example, have a listener on your JList that will respond to a newly selected item, and then in turn ask the same JList to reselect that same item and you won't get stuck in a pathologically recursive loop. If all the model-controllers in an application do this then there's no problem with multiple, repeated events firing off all over the place - each component will only update itself (and subsequently fire off events) if it needs to, and if it does update then it can fire off all the update events it wants, but only those components that haven't already got the message will do anything about it.



回答2:

This is expected behaviour.

From Model-View-Controller [Wikipedia]:

In event-driven systems, the model notifies observers (usually views) when the information changes so that they can react.

So, when you call setSelectedIndex on the JList, you are updating its model, which then notifies each ListSelectionListener. It wouldn't be MVC if you could "silently" update a model without letting anyone know.



回答3:

Swing is not exactly MVC, but has it's roots in MVC ( the difference lays in the fact, the view and the controller in Swing are more closely related than in other MVC see Swing architecture for more details ).

But this may not seem to be the problem you're facing. It sounds like you have to validate in the event listeners, for the type of event and decide whether to ignore it or not: If the event was originated in the list do change it. If was triggered by some other control, do not.



回答4:

What @dogbane said.

But to fix the problem you need to add some sort of a state check during the listener to see if the event is one you should ignore. The ListSelectionEvent has a getValueAdjusting() method, but that is mostly internal. What you need to do is simulate it yourself.

So for example when you update the list from an external selection you would have code like...

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

and in the update code for the ListSelectionListenerEvent

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

Scoping and access issues are left as an exercise for the reader. You would have to write the setSelectionAdjusting, and possibly set it on other objects as well.



回答5:

I still feel like there is something conceptually wrong here.

I empathize, but it may help to consider that you don't have a simple JList observing a ListSelectionModel. Instead, you have a JList and some-other-control observing a hybrid selection model. In @Taisin's example, the hybrid is a CustomSelectionModel that extends DefaultListSelectionModel and allows silent changes. When compatible, it's also possible to share a model, as suggested in this question & answer and this SharedModelDemo from the tutorial.

For reference, this thread cites the article Java SE Application Design With MVC: Issues With Application Design, which addresses the issue in more detail.



回答6:

I always do like this:

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

}


回答7:

Typically the way a listener works is it will "go off" every time the event that it is waiting for occurs. If I had to speculate it is a misunderstanding from your side of things.