基于MVC的GUI中模型之间的通信

时间:2013-06-10 11:04:24

标签: java swing model-view-controller model jtable

我正在根据MVC模式开发我的GUI:

-GUIview:Swing组件(JFrame和几个JTable)。 -GUIcontroller:监听器(在这里添加,在内部类中定义) -GUImodel:修改和存储数据,触发change-events。

模型中的更改通过控制器(而非直接)传递给视图,如this示例中所示。

我还为View类中包含的不同JTable编写了不同的自定义JTableModel(扩展AbstractTableModel)。所有JTableModel都在包“GUImodel”中的不同类中定义。每个JTableModel定义一个ArrayList和一些操作ArrayList的方法。

根据MVC指南,模型应该对视图一无所知。实际上,main()方法定义如下:

GUImodel model = new GUImodel();
GUIcontroller controller = new GUIcontroller();
GUIview view = new GUIview(controller, model);

controller.addView(view);
controller.addModel(model);

view.setVisible(true);
controller.addControllerListerners();

我的问题是: 当我在GUImodel中执行方法时(例如因为按下了JButton并且我需要从外部文件加载数据),我需要修改一些JTableModel(将数据/行添加到其ArrayList)并获取更改反映在JTable中。我的第一个想法是:

ArrayList newArrayList = fileLoader(filePath);  //create ArrayList and load info
guiView.getTable1Model().updateArrayList(newArrayList);  //update JTableModel ArrayList

但是,这种方法无效,因为GUImodel应完全独立于GUIview。

有什么想法吗?

4 个答案:

答案 0 :(得分:3)

可能很高兴认识到MVC主要是与数据封装有关的模式,它使用另一种模式Observer来进行通信更改。作为数据封装器,模型不知道视图和控制器,但作为Observable,它确实知道它有Observers,需要在发生更改时得到通知。

A Description of the Model-View-Controller User Interface Paradigm in the Smalltalk-80 System, page 4解释得很好:

  

为了管理变更通知,开发了依赖项对象的概念。意见和   模型的控制器作为模型的依赖者在列表中注册,以便随时被通知   模型的某些方面发生了变化。模型更改后,将广播消息以进行通知   所有家属都对这一变化有所了解。此消息可以参数化(带参数),所以   可以有许多类型的模型更改消息。每个视图或控制器都响应   适当的模式改变。

为了说明这个概念,你可以从你自己的Observer / Observable类开始:

public interface Observer {
    public void update(int message);
}
public interface Observable {
    public void registerObserver(Observer observer);
}

public class Model implements Observable {
    List<Observer> observers;

    public void registerObserver(Observer observer) {
        observers.add(observer);
    }

    public void loadFile(String path) {
        // load file and change data
        foreach (Observer observer: observers)
            observer.update(READ_NEW_DATA);
    }

    public ArrayList getData() { return data; }
}

public class View implements Observer {
    public void update(int message) {
        doWhateverWith(model.getData());
    }
}

public class Controller implements Observer {
    public void update(int message) {
        doWhateverWith(model.getData());
    }

    public void onClick() {
        model.loadFile("someFile");
    }
}

如您所见,模型对视图和控制器的内部工作原理一无所知。它甚至不知道返回一个ArrayList对他们来说是否特别有用(尽管实际上你也喜欢这样)。所以在这方面,实现了独立。

Obervable和Observers之间的通信没有独立性,但这不是MVC模式要求的一部分。

如果您希望GUI在现有的Swing Observer模式(Listeners)之上搭便车,那么您的类应该从相应的类继承:

public class Model extends AbstractTableModel...

public class View implements TableModelListener...

public class Controller implements CellEditorListener...

等等。由于JTable同时实现了TableModelListener和CellEditorListener,因此它实际上是View和Controller的组合。因此,您可以选择组合ViewController类扩展JTable,也可以单独使用它们。在后一种情况下,View可以扩展JTable,覆盖控件Listener,以便将它们的事件传递给Controller类。但这听起来像是值得付出的工作。

答案 1 :(得分:2)

正如here所讨论的那样,将模型和视图松散耦合是正确的。 JTable实现TableModelListener来监听自己的模型,而AbstractTableModel毫无疑问会触发导致监听表自行更新的事件。

在这种情况下,让从属TableModel将自己添加为TableModelListener给主TableModel。然后,依赖模型可以触发所需的事件,以通知自己的监听器从主服务器传播的更改。

答案 2 :(得分:2)

  

但是,这种方法无效,因为GUImodel应完全独立于GUIview。

Swing组件本身使用MVC模型。模型中的更改必须触发视图中的更改。问题是你是怎么做到的?

一种方法是模型可以访问视图实例,正如您在问题中所说明的那样。

ArrayList newArrayList = fileLoader(filePath);  //create ArrayList and load info
guiView.getTable1Model().updateArrayList(newArrayList);  //update JTableModel ArrayList

另一种方法是控制器更新模型并更新视图。这就是我在Swing应用程序中通常所做的事情。

model.loadArrayList(filePath);
frame.getFrame().getMainPanel().repaint();

另一种方法是解雇行动。这就是Swing组件更新GUI的方式。

ArrayList newArrayList = fileLoader(filePath);  //create ArrayList and load info
fireAction(newArrayLiat);

fireAction方法适用于侦听器。这是我从AbstractListModel复制的火方法。

protected void fireContentsChanged(Object source, int index0, int index1) {

    Object[] listeners = listenerList.getListenerList();
    ListDataEvent e = null;

    for (int i = listeners.length - 2; i >= 0; i -= 2) {
        if (listeners[i] == ListDataListener.class) {
            if (e == null) {
                e = new ListDataEvent(source,
                        ListDataEvent.CONTENTS_CHANGED, index0, index1);
            }
            ((ListDataListener) listeners[i + 1]).contentsChanged(e);
        }
    }
}

您必须在模型类中编写侦听器,视图类可以编写代码来更改视图。

EventListenerList的Javadoc提供了有关侦听器的更多信息。谢谢 Catalina Island

答案 3 :(得分:1)

我在摇摆中的MVC风格是,模型和视图彼此之间以及控制器都不知道,但控制器很清楚地知道视图和模型。这样,我就完成了控制器中的所有逻辑。我只是在视图中留下了UI +复杂布局的长代码,并考虑了应用程序模型和模型所需的所有数据。决定某些数据是否应出现在我的视图中。我通过view.getBtn().setAction(new ActionForThisOrThatInnerClass())种类的东西把控制器添加到控制器的功能

在您的情况下,我同意表格将使用的数据应以理想情况下以List的形式存储在您的主模型中,但我不会打扰自己将新TableModel子类化。 1}}处理这些数据,我认为DefaultTableModel足以做很多事情。

如果我要编写您的要求,这是可运行的示例

public class Sample {
    public static void main(String[] args){
        View view = new View();
        Model model = new Model();
        Controller controller = new Controller(view, model);

        JFrame frame = new JFrame("MVC Demo");
        frame.getContentPane().setLayout(new BorderLayout());
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.getContentPane().add(view.getUI());
        frame.pack();
        frame.setVisible(true);

        view.getBtnFileLoader().doClick();
    }
}

class View{

    private JButton btnFileChooser;
    private JButton btnFileLoader;
    private JTable tblData;
    private JPanel pnlMain;

    public View(){
        pnlMain = new JPanel(new BorderLayout()){
            @Override public Dimension getPreferredSize(){
                return new Dimension(300, 400); 
            }
        };
        JPanel pnlFileLoader = new JPanel();
        pnlFileLoader.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
        pnlFileLoader.setLayout(new BoxLayout(pnlFileLoader, BoxLayout.LINE_AXIS));

        JTextField txtFileDir = new JTextField();
        pnlFileLoader.add(txtFileDir);

        btnFileLoader = new JButton();
        pnlFileLoader.add(btnFileLoader);

        btnFileChooser = new JButton();
        pnlFileLoader.add(btnFileChooser);

        tblData = new JTable();
        JScrollPane pane = new JScrollPane(tblData);

        pnlMain.add(pane);
        pnlMain.add(pnlFileLoader, BorderLayout.PAGE_START);
    }

    public JPanel getUI(){
        return pnlMain;
    }

    public JButton getBtnFileLoader(){
        return btnFileLoader;
    }

    public JButton getBtnFileChooser(){
        return btnFileChooser;
    }

    public JTable getTblData(){
        return tblData;
    }
}

class Controller implements PropertyChangeListener{

    private View view;
    private Model model;
    private DefaultTableModel tmodel;

    public Controller(View view, Model model){
        this.view = view;
        this.model = model;

        model.addModelListener(this);
        setupViewEvents();
        setupTable();
    }
    private void setupTable(){
        tmodel = new DefaultTableModel();

        tmodel.addColumn("First Name");
        tmodel.addColumn("Last Name");
        tmodel.addColumn("Occupation");

        view.getTblData().setModel(tmodel);
    }

    private void setupViewEvents(){
        view.getBtnFileChooser().setAction(new AbstractAction("Choose"){
            @Override
            public void actionPerformed(ActionEvent arg0) {
                //choose the file then put the dir
                //in the txtfield
            }
        });

        view.getBtnFileLoader().setAction(new AbstractAction("Load"){
            @Override
            public void actionPerformed(ActionEvent arg0) {
                //validate if the dir in the textfield exists and the file is loadable
                //load the file specified in the textfield

                //assumming the list is already retrieved from the file
                //and the list contains the following person
                List<Person> list = new ArrayList<Person>();
                Person p1 = new Person("Bernardo", "Santos", "Developer");
                Person p2 = new Person("Robert", "Erasquin", "Architect");
                Person p3 = new Person("Klarrise", "Caparas", "Food Scientist");
                list.add(p1);
                list.add(p2);
                list.add(p3);

                //now update the model of the new value for the list
                model.setTheList(list);

            }
        });

    }

    @Override
    @SuppressWarnings("unchecked")
    public void propertyChange(PropertyChangeEvent evt) {
        if(evt.getPropertyName().equals("theList")){

            List<Person> newVal = (List<Person>) evt.getNewValue();
            DefaultTableModel tmodel = (DefaultTableModel)view.getTblData().getModel();

            for(Person p : newVal){
                tmodel.addRow(new Object[]{p.getFirstName(), p.getLastName(), p.getOccupation()});
            }

        }
    }
}



class Model{

    private List<Person> theList;
    private SwingPropertyChangeSupport propChangeFirer;

    public Model(){
        propChangeFirer = new SwingPropertyChangeSupport(this);
    }

    public void setTheList(List<Person> theList){
        List<Person> oldVal = this.theList;
        this.theList = theList;

        //after the model has been updated, notify its listener about
        //the update, in our case the controller itself listens to the model
        propChangeFirer.firePropertyChange("theList", oldVal, theList);
    }

    public void addModelListener(PropertyChangeListener prop) {
        propChangeFirer.addPropertyChangeListener(prop);
    }

}

class Person{
        private String firstName;
        private String lastName;
        private String occupation;

        public Person(String firstName, String lastName, String occupation){
            this.firstName = firstName;
            this.lastName = lastName;
            this.occupation = occupation;
        }

        public String getFirstName() {
            return firstName;
        }
        public void setFirstName(String firstName) {
            this.firstName = firstName;
        }
        public String getLastName() {
            return lastName;
        }
        public void setLastName(String lastName) {
            this.lastName = lastName;
        }
        public String getOccupation() {
            return occupation;
        }
        public void setOccupation(String occupation) {
            this.occupation = occupation;
        }
    }