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);
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.
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
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.
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) {
}
}
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With