I need to keep the data of a model class updated with UI components, and at same time UI components updated with changes in the data object. With a detail that a considerable ammount of data is dependent of other data. e.a.: SUM of A and B. The SUM need to be displayed at UI and stored in the Model Class.
In the real case I have around 58 editable fields, mixed of texts and numbers. And half as much calculated fields.
Thinkering around I've come with many solutions. My problem is that I have no experience to decide or judge what's the best way to go, if any at all. The two main candidates are:
- The first was just add DocumentListeners to all editable UI fields. When changed, they update the data in the Model and call a method to update all fields in the UI. The drawback - my crude opinion - is that I have more than 50 fields. I don't know how to code it without writting a specific Listener for each UI Component. Wich also may make dificult to handle changes in code later.
- Create an array of a class that register each editable or calculated UI component. The class will register not only the UI Component but, using reflection, the method to be called to Set or Retrieve information from the Model Object. A Document Lister will still handle the changes, but now it can be the same for all UI Components since the array can handle the changes. A good point is that all the translation between Model and UI can be coded in a single class. The drawback is reflections, people always seems to advice avoiding it.
Whats the best, or a good, way to handle the situation?
Code I'm using to test:
public class Comunication {
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
public void run() {
//Main Window
JFrame frame = new JFrame();
frame.setTitle("SumTest");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setMinimumSize(new Dimension(500,200));
frame.setVisible(true);
//Main Panel
JPanel pane = new JPanel();
frame.setContentPane(pane);
//Component
JTextField valueA = new JTextField("VALUE A");
JTextField valueB = new JTextField("VALUE B");
JTextField valueSum = new JTextField("VALUE SUM");
pane.add(valueA);
pane.add(valueB);
pane.add(valueSum);
}
});
}
}
class Data {
private int a;
private int b;
private int sum;
public Data() {
a = 1;
b = 2;
Calculate();
}
public void Calculate() {
sum = a + b;
}
public int getA() { return a; }
public int getB() { return b; }
public int getSUM() { return sum; }
public void setA(int i) { a = i; }
public void setB(int i) { b = i; }
}
Part 2:
Experimenting with information provided bu the users, I've took the freedom to try something else.
One solution would be creating a listener class that links the View and the Model. This listener should be changed a bit each time its added to a field(view), it's the only way o found to link a field with a method in the model without using reflection.
So, the algorithm for updates is: When changed, a view update the model.
Afterwards: a gobal controller update all views with the new information on the model.
Problems I've found, probably due lack of experience:
1) Since all views have document change listeners: when the global controller update all views/fields they call the listener again. A workaround I've found was to add a "silent" flag to the Listener.
2) When a view updates the model and call the global controller to update all other views, it cannot update itself. I've not found the reason yet, I think may be causing a loop. The workaround was tell the global controller who called it, and update every view except the caller.
The full working code with the solution and workarounds is below.
Opinions would be very welcome, i want to do it right, or better.
import java.awt.*;
import java.util.ArrayList;
import javax.swing.*;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
public class Comunication {
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
//Main Window
JFrame frame = new JFrame();
frame.setTitle("SumTest");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setMinimumSize(new Dimension(500,200));
frame.setVisible(true);
//Main Panel
JPanel pane = new JPanel();
frame.setContentPane(pane);
//Data Model
DataModel model = new DataModel();
GlobalUpdateController viewUpdateController = new GlobalUpdateController();
//Component
JTextField valueA = new JTextField("");
JTextField valueB = new JTextField("");
JTextField valueSum = new JTextField("");
valueA.setPreferredSize(new Dimension(30, 20));
valueB.setPreferredSize(new Dimension(30, 20));
valueSum.setPreferredSize(new Dimension(30, 20));
pane.add(valueA);
pane.add(valueB);
pane.add(valueSum);
//Listeners
valueA.getDocument().addDocumentListener(new StealthListener(valueA , viewUpdateController) {
@Override
public void updateView() {
this.view.setText( Integer.toString( model.getA() ) );
}
@Override
public void updateModel() {
model.setA( Integer.parseInt( this.view.getText() ) );
}
});
valueB.getDocument().addDocumentListener(new StealthListener(valueB , viewUpdateController) {
@Override
public void updateView() {
this.view.setText( Integer.toString( model.getB() ) );
}
@Override
public void updateModel() {
model.setB( Integer.parseInt( this.view.getText() ) );
}
});
valueSum.getDocument().addDocumentListener(new StealthListener(valueSum , viewUpdateController) {
@Override
public void updateView() {
this.view.setText( Integer.toString( model.getSUM() ) );
}
@Override
public void updateModel() {
//Do nothing
}
});
//Initial Update
viewUpdateController.updateAllViews(null);
}
});
}
}
class DataModel {
private int a;
private int b;
private int sum;
public DataModel() {
a = 3;
b = 5;
Calculate();
}
public void Calculate() {
sum = a + b;
}
public int getA() { return a; }
public int getB() { return b; }
public int getSUM() { return sum; }
public void setA(int i) { a = i; Calculate(); }
public void setB(int i) { b = i; Calculate(); }
}
class StealthListener implements DocumentListener {
JTextField view;
GlobalUpdateController viewList;
private boolean silent;
public StealthListener(JTextField view, GlobalUpdateController viewList) {
this.view = view;
this.viewList = viewList;
this.silent = false;
this.viewList.add(this);
}
public void setSilent(boolean val) {
this.silent = val;
}
public void updateView() {
// Unique to each view, to be Overriden
}
public void updateModel() {
// Unique to each view, to be Overriden
}
public void update() {
//The silent flag is meant to avoid ListenerLoop when changing the document.
//When the silent is true is meant to listen to internal changes.
if(this.silent == false) {
updateModel();
this.viewList.updateAllViews(this);
}
}
@Override
public void insertUpdate(DocumentEvent e) {
update();
}
@Override
public void removeUpdate(DocumentEvent e) {
update();
}
@Override
public void changedUpdate(DocumentEvent e) {
update();
}
}
class GlobalUpdateController {
private ArrayList<StealthListener> viewList;
public GlobalUpdateController() {
this.viewList = new ArrayList<StealthListener>();
}
public void add(StealthListener control) {
this.viewList.add(control);
}
public void updateAllViews(StealthListener caller) {
for( StealthListener view : viewList) {
if( caller==null || view != caller ) {
view.setSilent(true);
view.updateView();
view.setSilent(false);
}
}
}
}
See Question&Answers more detail:
os