Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

MVC - SwingWorker with a long running process that should update the view

How to obtain separation of the View from model when using a SwingWorker with a long running process that should send updates back to the controller ?

  • I can use the SwingWorkers doInBackground() to keep the EDT responsive by calling e.g model.doLongProcess() from in there great!

  • The issue I have is trying to get data back before the process is finished, to update the view with the progress..

  • I know that I can get data back by using by using the SwingWorkers publish() method but this I think forces me to write the code for the doLongProcess() method within doInBackground().


For reference the MVC implementation I have a looks a little like this:

http://www.leepoint.net/notes-java/GUI/structure/40mvc.html

/ structure/calc-mvc/CalcMVC.java -- Calculator in MVC pattern.
// Fred Swartz -- December 2004

import javax.swing.*;

public class CalcMVC {
    //... Create model, view, and controller.  They are
    //    created once here and passed to the parts that
    //    need them so there is only one copy of each.
    public static void main(String[] args) {

        CalcModel      model      = new CalcModel();
        CalcView       view       = new CalcView(model);
        CalcController controller = new CalcController(model, view);

        view.setVisible(true);
    }
}

I have one Model Class which wraps a number of other classes together to a form simple interface for the controller.

I really don't want to have to move all/some/any of the code from these Classes into the controller - It doesn't belong there.


Update:

Here is the approach that I am taking - Its not the cleanest solution and It could be perceived as an abuse of PropertyChangeSupport.. on a semantic level.

Basically all the low-level classes that have long running methods will have a propertyChangeSupport field. The long running methods call the firePropertyChange() periodically to update on the status of the method and not necessarily to report the change of a property - that is what I mean by semantic abuse!.

Then the Model class which wraps the low level classes catches these events and issues its own highlevel firePropertyChange .. which the controller can listen for...

Edit:

To clarify, when I call firePropertyChange(propertyName, oldValue, newValue);

  • propertyName ---> I abuse the propertyName to represent a topicname
  • oldValue =null
  • newValue = the message that I want to broadcast

Then the PropertyChangeListener in the model or where ever can discern the message based on the topicname.

So Iv basically bent the system to use it like a publish-subscribe ....


I guess in place of the above method I could add a progress field to the lowlevel classes that gets updated, and then firePropertyChange based on that.. this would fall into line with how its supposed to be used.

like image 834
volting Avatar asked Oct 06 '12 12:10

volting


2 Answers

I think of the publish/process pair as pushing data from the SwingWorker into the GUI. Another way to pass information is by having the GUI or control pull the information out of the SwingWorker by using PropertyChangeSupport and PropertyChangeListeners. Consider

  • giving your model a PropertyChangeSupport field,
  • Giving it add and remove PropertyChangeListener methods
  • Having it notify the support object of changes in state.
  • Having the SwingWorker add a PropertyChangeListener to the model.
  • Then having the SwingWorker notifying control or view of changes in the model's state.
  • The SwingWorker could even use publish/process with the changed information from the model.

Edit
Regarding your update:

Basically all the low-level classes that have long running methods will have a propertyChangeSupport field. The long running methods call the firePropertyChange() periodically to update on the status of the method and not necessarily to report the change of a property - that is what I mean by semantic abuse!.

I don't recommend that you do this. Understand that if the bound property being listened to does not change, none of the PropertyChangeListeners (PCLs) will be notified even if firePC() is called. If you need to poll a property, then I wouldn't use a PCL to do this. I would simply poll it, probably from outside of the class being polled.

like image 111
Hovercraft Full Of Eels Avatar answered Nov 05 '22 20:11

Hovercraft Full Of Eels


Personally, in my SwingWorker I'd create a public publish method, and pass the instance of my SwingWorker to the long running Model method. That way the model pushes updates to the control (SwingWorker), which then pushes to the View.

Here's an example - I threw everything into one file (for simplicity of running), but I'd imagine normally you'd have separate files/packages for these things.

EDIT

To decouple the model from the control, you'd have to have an observer of the model. I would implement a ProgressListener inheriting ActionListener. The model just notifies all registered ProgressListener that progress has been made.

import java.awt.event.*;
import java.util.*;
import javax.swing.*;

public class MVCSwingWorkerExample {

    public static void main(String[] args) {
        CalcModel      model      = new CalcModel();
        CalcView       view       = new CalcView();
        CalcController controller = new CalcController(model, view);
    }

    //Model class - contains long running methods ;)
    public static class CalcModel{

        //Contains registered progress listeners
        ArrayList<ActionListener> progressListeners = new ArrayList<ActionListener>();
        //Contains model's current progress
        public int status;

        //Takes in an instance of my control's Swing Worker
        public boolean longRunningProcess(MVCSwingWorkerExample.CalcController.Worker w){
            for(int i = 0; i < 60; i++){
                try {
                    //Silly calculation to publish some values
                    reportProgress( i==0 ? 0 : i*100/60);
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    System.out.println("Whowsa!");
                    e.printStackTrace();
                }
            }
            return true;
        }

        //Notify all listeners that progress was made
        private void reportProgress(int i){
            status = i;
            ActionEvent e = new ActionEvent(this, ActionEvent.ACTION_FIRST, null);
            for(ActionListener l : progressListeners){
                l.actionPerformed(e);
            }
        }

        //Standard registering of the listeners
        public void addProgressListener(ActionListener l){
            progressListeners.add(l);
        }

        //Standard de-registering of the listeners
        public void removeProgressListener(ActionListener l){
            progressListeners.remove(l);
        }
    }

    //View Class - pretty bare bones (only contains view stuff)
    public static class CalcView{
        Box display;
        JButton actionButton;
        JLabel progress;

        public void buildDisplay(){
            display = Box.createVerticalBox();
            actionButton = new JButton("Press me!");
            display.add(actionButton);

            progress = new JLabel("Progress:");
            display.add(progress);
        }

        public void start(){
            final JFrame frame = new JFrame();
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.add(display);
            frame.pack();
            frame.setLocationRelativeTo(null);
            frame.setVisible(true);
        }
    }

    public static class CalcController{
        CalcModel model;
        CalcView view;

        public CalcController(CalcModel model, CalcView view){
            this.model = model;
            this.view = view;

            //Build the view
            view.buildDisplay();

            //Create an action to add to our view's button (running the swing worker)
            ActionListener buttonAction = new ActionListener(){
                @Override
                public void actionPerformed(ActionEvent e) {
                    Worker w = new Worker();
                    w.execute();
                }
            };
            view.actionButton.addActionListener(buttonAction);

            //Start up the view
            view.start();

        }

        //Notified when the Model updates it's status
        public class ProgressListener implements ActionListener{
            Worker w;

            public ProgressListener(Worker w){
                this.w = w;
            }

            @Override
            public void actionPerformed(ActionEvent e) {
                CalcModel model = (CalcModel)e.getSource();
                w.publishValue(model.status);
            }

        }


        //The worker - usually part of the control
        public class Worker extends SwingWorker<Boolean, Integer>{

            public Worker(){
                //Register a listener to pay attention to the model's status
                CalcController.this.model.addProgressListener(new ProgressListener(this));
            }

            @Override
            protected Boolean doInBackground() throws Exception {
                //Call the model, and pass in this swing worker (so the model can publish updates)
                return model.longRunningProcess(this);
            }

            //Expose a method to publish results
            public void publishValue(int i){
                publish(i);
            }

              @Override
              protected void process(java.util.List<Integer> chunks){
                  view.progress.setText("Progress:" + chunks.get(chunks.size()-1) + "%");
              }

             @Override
               protected void done() {
                   try {
                       view.progress.setText("Done");
                   } catch (Exception ignore) {
                   }
               }
        }
    }

}
like image 1
Nick Rippe Avatar answered Nov 05 '22 20:11

Nick Rippe