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

How to get the separation of the view from the model when using SwingWorker with a lengthy process that should send updates back to the controller?

  • I can use SwingWorkers doInBackground() to support the EDT response by calling, for example, model.doLongProcess() where it was great!

  • I have an attempt to return data before the process is completed in order to update the view with progress.

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


For reference: my MVC implementation looks something 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 that combines a number of other classes into a simple interface for the controller.

I really do not want to move all / some / any code from these classes to the controller - it does not belong there.


Update:

This is the approach that I take - this is not the purest decision, and it can be taken as an abuse of PropertyChangeSupport .. at a semantic level.

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

Then the Model class, which wraps the low-level classes, catches these events and throws its own high level firePropertyChange .., which the controller can listen to ...

Edit:

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

  • propertyName ---> I am abusing the propertyName property to represent the topic name
  • oldValue = null
  • newValue = the message I want to convey

Then, the PropertyChangeListener property in the model or sometime can recognize the message based on the topic name.

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


I assume that instead of this method, I could add a progress field to the lowlevel classes that will be updated, and then firePropertyChange based on this .. this will correspond to how it is supposed to be used.

+6
source share
3 answers

I think of a publishing / process pair as pushing data from SwingWorker into a GUI. Another way to convey information is to have a GUI or control that displays information from SwingWorker using PropertyChangeSupport and PropertyChangeListeners. Consider

  • giving your model the PropertyChangeSupport field,
  • Giving them Add and Remove PropertyChangeListener Methods
  • Notification of a state change support object.
  • Having a SwingWorker adds a PropertyChangeListener to the model.
  • Then, having SwingWorker notifying the control or viewing changes in model state.
  • SwingWorker can even use a publication / process with modified information from a model.

Edit
Regarding your update:

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

I do not recommend you to do this. Understand that if the associated property that is being listened to does not change, none of the PropertyChangeListeners (PCL) will be notified, even if firePC () is called. If you need to poll a property, I would not use PCL for this. I would just interview him, possibly outside the survey class.

+4
source

Personally, in my SwingWorker I would create a public publish method and pass an instance of my SwingWorker to the Long Model method. Thus, the model pushes updates to the control ( SwingWorker ), which then pushes the view.

Here is an example - I threw everything into one file (for ease of operation), but I would assume that you would have separate files / packages for these things.

EDIT

To separate the model from the control, you will have to have a model observer. I would do the ProgressListener inheritance of the ActionListener . The model simply 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 current progress public int status; //Takes in an instance of my control 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 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 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 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) { } } } } } 
+1
source

For a long process under Swing, you must create a new thread for this purpose, so when this process is complete, you must update the MVC inside the "Swing thread", remember that there is only one for each application.

Try to find a way to tell the user what your application is processing, and do not let it โ€œmultiplyโ€ again until you finish.

 public class CalcController { ////////////////////////////////////////// inner class MultiplyListener /** * When a mulitplication is requested. 1. Get the user input number from the * View. 2. Call the model to mulitply by this number. 3. Get the result * from the Model. 4. Tell the View to display the result. If there was an * error, tell the View to display it. */ class MultiplyListener implements ActionListener { public void actionPerformed(ActionEvent e) { final String userInput = m_view.getUserInput(); new Thread(new Runnable() { @Override public void run() { try { m_model.multiplyBy(userInput); } catch (NumberFormatException nfex) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { m_view.showError("Bad input: '" + userInput + "'"); } }); } SwingUtilities.invokeLater(new Runnable() { @Override public void run() { m_view.setTotal(m_model.getValue()); } }); } }).start(); } }//end inner class MultiplyListener } 
0
source

Source: https://habr.com/ru/post/927095/


All Articles