Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
401 views
in Technique[技术] by (71.8m points)

java - Transit data between JComponent and a model Object

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:

  1. 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.
  2. 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

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)
  1. In this related example each of an arbitrary number of editable text fields adds an instance of a PropertyChangeListener and a FocusListener. Each listener invokes a common update() method to recompute a derived sum. In the variation below, the fields share a single instance of an UpdateListener that is both a FocusListener and a PropertyChangeListener. Let UpdateListener also implement DocumentListener only if you need to update() with each keystroke.

    In effect, the model is the data in a List<JFormattedTextField>, and the controller is the common update() method that enforces the relationship among the input fields. The scalability of this approach hinges on the resulting complexity of update(). The example uses JFormattedTextField for convenient formatting of related fields.

  2. Whenever I consider using reflection, I also consider the strategy pattern in conjunction with enum as an alternative. Examples are seen here and here.

image

import java.awt.EventQueue;
import java.awt.GridLayout;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JFormattedTextField;
import javax.swing.JFrame;
import javax.swing.JPanel;

/**
 * @see https://stackoverflow.com/a/31764798/230513
 * @see https://stackoverflow.com/q/8703464/230513
 * @see https://stackoverflow.com/questions/6803976
 */
public class Adder extends JPanel {

    private static final int MAX = 3;
    private final List<JFormattedTextField> fields = new ArrayList<>();
    private final NumberFormat format = NumberFormat.getNumberInstance();
    private final JFormattedTextField sum = new JFormattedTextField(format);
    private final UpdateListener listener = new UpdateListener();

    private class UpdateListener extends FocusAdapter implements PropertyChangeListener {

        @Override
        public void propertyChange(PropertyChangeEvent e) {
            update();
        }

        @Override
        public void focusLost(FocusEvent e) {
            EventQueue.invokeLater(new Runnable() {

                @Override
                public void run() {
                    update();
                }
            });
        }
    }

    public Adder() {
        this.setLayout(new GridLayout(0, 1));
        for (int i = 0; i < MAX; i++) {
            JFormattedTextField tf = init();
            fields.add(tf);
            this.add(tf);
        }
        sum.setHorizontalAlignment(JFormattedTextField.RIGHT);
        sum.setEditable(false);
        sum.setFocusable(false);
        this.add(sum);
    }

    private JFormattedTextField init() {
        JFormattedTextField jtf = new JFormattedTextField(format);
        jtf.setValue(0);
        jtf.setHorizontalAlignment(JFormattedTextField.RIGHT);
        jtf.addFocusListener(listener);
        jtf.addPropertyChangeListener("value", listener);
        return jtf;
    }

    private void update() {
        long total = 0;
        for (JFormattedTextField tf : fields) {
            Number v = (Number) tf.getValue();
            total += v.longValue();
        }
        sum.setValue(total);
    }

    private void display() {
        JFrame f = new JFrame("Adder");
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.add(this);
        f.pack();
        f.setLocationRelativeTo(null);
        f.setVisible(true);
    }

    public static void main(String[] args) {
        EventQueue.invokeLater(new Runnable() {

            @Override
            public void run() {
                new Adder().display();
            }
        });
    }
}

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...