MVC模式和Swing


79

我发现在“真正的Swing生活”中最难掌握的设计模式之一是MVC模式。我浏览了该站点上的很多文章,讨论了该模式,但是我仍然不了解如何在Java Swing应用程序中利用该模式。

假设我有一个JFrame,其中包含一个表,几个文本字段和一些按钮。我可能会使用TableModel将JTable与基础数据模型“桥接”。但是,负责清除字段,验证字段,锁定字段以及按钮操作的所有函数通常都直接在JFrame中。但是,这不是将模式的Controller和View混合了吗?

据我所知,当查看JTable(和模型)时,我设法“正确地”实现了MVC模式,但是当我整体查看整个JFrame时,事情就变得混乱了。

我真的很想听听其他人对此的看法。当需要使用MVC模式向用户显示表格,几个字段和一些按钮时,如何处理?


2
这是一个相关的例子
垃圾神2011年

对于其他人来此聚会- Swing是不是一个单纯的MVC -它借用重从概念,但“崩溃”的“视图和控制器”在一起
MadProgrammer

Answers:


106

我强烈推荐给MVC的书籍是Freeman和Freeman撰写的“ Head First Design Patterns”。他们对MVC有高度全面的解释。

简要总结

  1. 您就是用户-您与视图进行交互。视图是您进入模型的窗口。当您对视图执行某项操作(例如单击“播放”按钮)时,视图将告诉控制器您做了什么。处理该任务是控制器的工作。

  2. 控制器要求模型更改其状态。控制器执行您的动作并解释它们。如果单击按钮,控制器的工作就是弄清楚这意味着什么,以及如何基于该操作来操纵模型。

  3. 控制器也可能要求视图改变。当控制器从视图接收到一个动作时,它可能需要告诉视图作为结果进行更改。例如,控制器可以启用或禁用界面中的某些按钮或菜单项。

  4. 当状态改变时,模型会通知视图。当模型中的某些内容发生更改时,根据您执行的某些操作(例如单击按钮)或其他内部更改(例如播放列表中的下一首歌曲已开始),模型将通知视图其状态已更改。

  5. 视图向模型询问状态。该视图直接从模型获取其显示的状态。例如,当模型通知视图新歌曲已开始播放时,该视图向模型请求歌曲名称并显示它。由于控制器请求对视图进行某些更改,因此视图可能还会向模型询问状态。

在此处输入图片说明 来源(如果您想知道“奶油控制器”是什么,请考虑一个奥利奥饼干,其中控制器是奶油中心,视图是顶部饼干,模型是底部饼干。)

嗯,如果您有兴趣,可以从这里下载有关MVC模式的相当有趣的歌曲!

Swing编程可能会遇到的一个问题是将SwingWorker和EventDispatch线程与MVC模式合并。根据您的程序,您的视图或控制器可能必须扩展SwingWorker并覆盖doInBackground()放置资源密集型逻辑的方法。这可以很容易地与典型的MVC模式融合,这是Swing应用程序的典型特征。

编辑#1

此外,将MVC视为各种模式的组合很重要。例如,您的模型可以使用Observer模式实现(要求将View注册为模型的观察者),而控制器则可以使用Strategy模式。

编辑#2

另外,我想特别回答您的问题。您应该在视图中显示表格按钮等,这显然会实现一个ActionListener。在您的actionPerformed()方法中,您将检测到事件并将其发送到控制器中的相关方法(请记住,该视图包含对控制器的引用)。因此,当单击按钮时,视图将检测到事件,并将其发送到控制器的方法,控制器可能会直接要求视图禁用按钮或其他操作。接下来,控制器将与模型进行交互并修改模型(模型将主要具有getter和setter方法,以及一些其他方法来注册和通知观察者,依此类推)。修改模型后,它将立即调用已注册观察者的更新(在您的情况下,将是视图)。因此,视图现在将自行更新。


我实际上已经读过这本书,但发现很难将模式应用于SWING。我还阅读了一些地方的文章,即JFrame也可以看作代表视图和控制器。
sbrattla 2011年

... JFrame是一个组件,而不是叶子。通常,由控制器进行的更新将发送到JFrame,其余的工作将由JFrame负责,因此,这可能会使人幻想它是控制器,但实际上并非如此,因为它没有更改模型,只有视图。如果您的JFrame以某种方式直接更改了模型-您做错了。
2011年

...同样,这里的关键字是“直接”。在您的情况下,您可能会听取对表的鼠标单击,并将逻辑发送到控制器中用于修改表模型的方法。
2011年

2
@DhruvGairola第二点说明适用于第三点,第三点和针对点具有相同的重复说明。你能纠正他们吗?
火影忍者Biju模式

那首歌是经典!=D
aaiezza

36

我不认为视图是模型数据更改时会通知该视图的想法。我会将该功能委托给控制器。在这种情况下,如果更改应用程序逻辑,则无需干预视图的代码。该视图的任务仅适用于应用程序组件+布局,仅此而已。摇摆式布局已经是一项繁琐的任务,为什么要让它干扰应用程序逻辑呢?

我对MVC的想法(到目前为止,我正在与之合作)是:

  1. 该视图是三者中最愚蠢的。它对控制器和模型一无所知。它只关心挥杆部件的专业性和布局。
  2. 该模型也很笨,但不像视图那么笨。它执行以下功能。
    • 一种。当控制器调用其setter之一时,它将向其侦听器/观察者发出通知(如我所说,我将把这个角色委托给控制器)​​。我更喜欢SwingPropertyChangeSupport来实现这一点,因为它已经为此目的进行了优化。
    • b。数据库交互功能。
  3. 一个非常聪明的控制器。非常了解视图和模型。控制器具有两个功能:
    • 一种。它定义了视图在用户与之交互时将执行的动作。
    • b。它监听模型。就像我说过的那样,当调用模型的setter时,模型将向控制器发出通知。解释此通知是控制器的工作。它可能需要将更改反映到视图。

代码样例

风景 :

就像我说的那样,创建视图已经很冗长,因此只需创建自己的实现即可:)

interface View{
    JTextField getTxtFirstName();
    JTextField getTxtLastName();
    JTextField getTxtAddress();
}

出于可测试性目的,最好将这三个接口连接起来。我只提供了Model和Controller的实现。

该模型 :

public class MyImplementationOfModel implements Model{
    ...
    private SwingPropertyChangeSupport propChangeFirer;
    private String address;
    private String firstName;
    private String lastName;

    public MyImplementationOfModel() {
        propChangeFirer = new SwingPropertyChangeSupport(this);
    }
    public void addListener(PropertyChangeListener prop) {
        propChangeFirer.addPropertyChangeListener(prop);
    }
    public void setAddress(String address){
        String oldVal = this.address;
        this.address = address;

        //after executing this, the controller will be notified that the new address has been set. Its then the controller's
        //task to decide what to do when the address in the model has changed. Ideally, the controller will update the view about this
        propChangeFirer.firePropertyChange("address", oldVal, address);
    }
    ...
    //some other setters for other properties & code for database interaction
    ...
}

控制器:

public class MyImplementationOfController implements PropertyChangeListener, Controller{

    private View view;
    private Model model;

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

        //register the controller as the listener of the model
        this.model.addListener(this);

        setUpViewEvents();
    }

    //code for setting the actions to be performed when the user interacts to the view.
    private void setUpViewEvents(){
        view.getBtnClear().setAction(new AbstractAction("Clear") { 
            @Override
            public void actionPerformed(ActionEvent arg0) {
                model.setFirstName("");
                model.setLastName("");
                model.setAddress("");
            }
        });

        view.getBtnSave().setAction(new AbstractAction("Save") { 
            @Override
            public void actionPerformed(ActionEvent arg0) {
                ...
                //validate etc.
                ...
                model.setFirstName(view.getTxtFName().getText());
                model.setLastName(view.getTxtLName().getText());
                model.setAddress(view.getTxtAddress().getText());
                model.save();
            }
        });
    }

    public void propertyChange(PropertyChangeEvent evt){
        String propName = evt.getPropertyName();
        Object newVal = evt.getNewValue();

        if("address".equalsIgnoreCase(propName)){
            view.getTxtAddress().setText((String)newVal);
        }
        //else  if property (name) that fired the change event is first name property
        //else  if property (name) that fired the change event is last name property
    }
}

安装MVC的Main:

public class Main{
    public static void main(String[] args){
        View view = new YourImplementationOfView();
        Model model = new MyImplementationOfModel();

        ...
        //create jframe
        //frame.add(view.getUI());
        ...

        //make sure the view and model is fully initialized before letting the controller control them.
        Controller controller = new MyImplementationOfController(view, model);

        ...
        //frame.setVisible(true);
        ...
    }
}

4
有趣的是,当在多个视图中显示单个实体模型时,效率较低。然后,您的设计可能会导致“大型控制器”处理单个模型但管理所有相关视图。如果您尝试重用一组“小型模型”,则变得更加棘手,这要归功于聚合成“大型模型”,因为视图显示了在多个“小型模型”实体中调度的信息。
伊夫·马丁

1
@onepotato我刚刚尝试了您的代码。当我按下一个按钮时,我可以触发setUpViewEvents()中的代码。但是,当我执行model.setSomething(123)时,不会触发propertyChange中的代码。我什至将println直接放在Object newVal = evt.getNewValue();下;它不会打印。
AmuletxHeart

10
这不是MVC架构模式,而是紧密相关的 MVP(模型-视图-演示器)模式。在典型的MVC中,正是模型的工作是在视图更改时通知视图,这正是您“不喜欢”的。查看此图以查看典型MVC中的交互如何工作。
MaxAxeHax

24

MVC模式是如何构造用户界面的模型。因此,它定义了3个元素:模型,视图,控制器:

  • 模型模型是呈现给用户的事物的抽象。在摇摆中,您可以区分gui模型和数据模型。GUI模型抽象了UI组件(如ButtonModel)的状态。数据模型抽象了ui呈现给用户的结构化数据,如TableModel
  • 视图视图是一个ui组件,负责向用户显示数据。因此,它负责所有与UI相关的问题,例如布局,图形等。例如JTable
  • 控制器控制器封装了为了与用户交互(鼠标移动,鼠标单击,按键等)执行的应用程序代码。控制器可能需要输入才能执行,并产生输出。他们从模型读取输入,并根据执行结果更新模型。他们还可能重组ui(例如,更换ui组件或显示一个完整的新视图)。但是,他们必须不知道ui组件,因为您可以将重组封装在控制器仅调用的单独接口中。在摆动中,控制器通常由ActionListenerAction实现

  • 红色=型号
  • 绿色=视图
  • 蓝色=控制器

在此处输入图片说明

Button点击它调用ActionListener。该ActionListener只依赖于其他车型。它使用一些模型作为输入,使用其他模型作为结果或输出。就像方法参数和返回值一样。模型在更新时会通知ui。因此,控制器逻辑无需知道ui组件。模型对象不知道ui。通知是通过观察者模式完成的。因此,模型对象仅知道有人希望在模型更改时得到通知。

在Java swing中,有些组件也实现了模型和控制器。例如javax.swing.Action。它实现了ui模型(属性:启用,小图标,名称等),并且是一个控制器,因为它扩展了ActionListener

详细说明,示例应用程序和源代码https://www.link-intersystems.com/blog/2013/07/20/the-mvc-pattern-implemented-with-java-swing/

少于240行的MVC基础知识:

public class Main {

    public static void main(String[] args) {
        JFrame mainFrame = new JFrame("MVC example");
        mainFrame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        mainFrame.setSize(640, 300);
        mainFrame.setLocationRelativeTo(null);

        PersonService personService = new PersonServiceMock();

        DefaultListModel searchResultListModel = new DefaultListModel();
        DefaultListSelectionModel searchResultSelectionModel = new DefaultListSelectionModel();
        searchResultSelectionModel
                .setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
        Document searchInput = new PlainDocument();

        PersonDetailsAction personDetailsAction = new PersonDetailsAction(
                searchResultSelectionModel, searchResultListModel);
        personDetailsAction.putValue(Action.NAME, "Person Details");

        Action searchPersonAction = new SearchPersonAction(searchInput,
                searchResultListModel, personService);
        searchPersonAction.putValue(Action.NAME, "Search");

        Container contentPane = mainFrame.getContentPane();

        JPanel searchInputPanel = new JPanel();
        searchInputPanel.setLayout(new BorderLayout());

        JTextField searchField = new JTextField(searchInput, null, 0);
        searchInputPanel.add(searchField, BorderLayout.CENTER);
        searchField.addActionListener(searchPersonAction);

        JButton searchButton = new JButton(searchPersonAction);
        searchInputPanel.add(searchButton, BorderLayout.EAST);

        JList searchResultList = new JList();
        searchResultList.setModel(searchResultListModel);
        searchResultList.setSelectionModel(searchResultSelectionModel);

        JPanel searchResultPanel = new JPanel();
        searchResultPanel.setLayout(new BorderLayout());
        JScrollPane scrollableSearchResult = new JScrollPane(searchResultList);
        searchResultPanel.add(scrollableSearchResult, BorderLayout.CENTER);

        JPanel selectionOptionsPanel = new JPanel();

        JButton showPersonDetailsButton = new JButton(personDetailsAction);
        selectionOptionsPanel.add(showPersonDetailsButton);

        contentPane.add(searchInputPanel, BorderLayout.NORTH);
        contentPane.add(searchResultPanel, BorderLayout.CENTER);
        contentPane.add(selectionOptionsPanel, BorderLayout.SOUTH);

        mainFrame.setVisible(true);
    }

}

class PersonDetailsAction extends AbstractAction {

    private static final long serialVersionUID = -8816163868526676625L;

    private ListSelectionModel personSelectionModel;
    private DefaultListModel personListModel;

    public PersonDetailsAction(ListSelectionModel personSelectionModel,
            DefaultListModel personListModel) {
        boolean unsupportedSelectionMode = personSelectionModel
                .getSelectionMode() != ListSelectionModel.SINGLE_SELECTION;
        if (unsupportedSelectionMode) {
            throw new IllegalArgumentException(
                    "PersonDetailAction can only handle single list selections. "
                            + "Please set the list selection mode to ListSelectionModel.SINGLE_SELECTION");
        }
        this.personSelectionModel = personSelectionModel;
        this.personListModel = personListModel;
        personSelectionModel
                .addListSelectionListener(new ListSelectionListener() {

                    public void valueChanged(ListSelectionEvent e) {
                        ListSelectionModel listSelectionModel = (ListSelectionModel) e
                                .getSource();
                        updateEnablement(listSelectionModel);
                    }
                });
        updateEnablement(personSelectionModel);
    }

    public void actionPerformed(ActionEvent e) {
        int selectionIndex = personSelectionModel.getMinSelectionIndex();
        PersonElementModel personElementModel = (PersonElementModel) personListModel
                .get(selectionIndex);

        Person person = personElementModel.getPerson();
        String personDetials = createPersonDetails(person);

        JOptionPane.showMessageDialog(null, personDetials);
    }

    private String createPersonDetails(Person person) {
        return person.getId() + ": " + person.getFirstName() + " "
                + person.getLastName();
    }

    private void updateEnablement(ListSelectionModel listSelectionModel) {
        boolean emptySelection = listSelectionModel.isSelectionEmpty();
        setEnabled(!emptySelection);
    }

}

class SearchPersonAction extends AbstractAction {

    private static final long serialVersionUID = 4083406832930707444L;

    private Document searchInput;
    private DefaultListModel searchResult;
    private PersonService personService;

    public SearchPersonAction(Document searchInput,
            DefaultListModel searchResult, PersonService personService) {
        this.searchInput = searchInput;
        this.searchResult = searchResult;
        this.personService = personService;
    }

    public void actionPerformed(ActionEvent e) {
        String searchString = getSearchString();

        List<Person> matchedPersons = personService.searchPersons(searchString);

        searchResult.clear();
        for (Person person : matchedPersons) {
            Object elementModel = new PersonElementModel(person);
            searchResult.addElement(elementModel);
        }
    }

    private String getSearchString() {
        try {
            return searchInput.getText(0, searchInput.getLength());
        } catch (BadLocationException e) {
            return null;
        }
    }

}

class PersonElementModel {

    private Person person;

    public PersonElementModel(Person person) {
        this.person = person;
    }

    public Person getPerson() {
        return person;
    }

    @Override
    public String toString() {
        return person.getFirstName() + ", " + person.getLastName();
    }
}

interface PersonService {

    List<Person> searchPersons(String searchString);
}

class Person {

    private int id;
    private String firstName;
    private String lastName;

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

    public int getId() {
        return id;
    }

    public String getFirstName() {
        return firstName;
    }

    public String getLastName() {
        return lastName;
    }

}

class PersonServiceMock implements PersonService {

    private List<Person> personDB;

    public PersonServiceMock() {
        personDB = new ArrayList<Person>();
        personDB.add(new Person(1, "Graham", "Parrish"));
        personDB.add(new Person(2, "Daniel", "Hendrix"));
        personDB.add(new Person(3, "Rachel", "Holman"));
        personDB.add(new Person(4, "Sarah", "Todd"));
        personDB.add(new Person(5, "Talon", "Wolf"));
        personDB.add(new Person(6, "Josephine", "Dunn"));
        personDB.add(new Person(7, "Benjamin", "Hebert"));
        personDB.add(new Person(8, "Lacota", "Browning "));
        personDB.add(new Person(9, "Sydney", "Ayers"));
        personDB.add(new Person(10, "Dustin", "Stephens"));
        personDB.add(new Person(11, "Cara", "Moss"));
        personDB.add(new Person(12, "Teegan", "Dillard"));
        personDB.add(new Person(13, "Dai", "Yates"));
        personDB.add(new Person(14, "Nora", "Garza"));
    }

    public List<Person> searchPersons(String searchString) {
        List<Person> matches = new ArrayList<Person>();

        if (searchString == null) {
            return matches;
        }

        for (Person person : personDB) {
            if (person.getFirstName().contains(searchString)
                    || person.getLastName().contains(searchString)) {
                matches.add(person);
            }

        }
        return matches;
    }

}

4
我喜欢这个答案+1,要提一下,Action因为Controller实际上我猜EventListener都是控制器
。–

@nachokk是的,的确如此。正如我所说A controller encapsulates the application code that is executed in order to an user interaction。移动鼠标,单击组件,按下键等都是用户交互。为了清楚起见,我更新了答案。
勒内链接

2

您可以在单独的简单Java类中创建模型,并在另一个Java类中创建控制器。

然后,您可以在其之上具有Swing组件。JTable将是视图之一(表模型实际上将成为视图的一部分-它只会从“共享模型”转换为JTable)。

每当编辑表时,其表模型就会告诉“主控制器”进行更新。但是,控制器对表一无所知。因此,呼叫看起来应该更像:updateCustomer(customer, newValue),而不是updateCustomer(row, column, newValue)

为共享模型添加一个侦听器(观察者)接口。某些组件(例如您的表)可以直接实现它。另一个观察者可以是协调按钮可用性等的控制器。


这是做到这一点的一种方法,但是如果它对您的用例来说过大,您当然可以简化或扩展它。

您可以将控制器与模型合并,并具有相同的类过程更新并维护组件可用性。您甚至可以将“共享模型”设置为a TableModel(尽管如果不仅仅用于表,我建议至少提供一个不会泄漏表抽象的更友好的API)

在另一方面,你可以有更新的复杂接口(CustomerUpdateListenerOrderItemListenerOrderCancellationListener)和专用控制器(或中介)仅适用于不同的意见协调。

这取决于您的问题有多复杂。


所有视图中大约90%由一个表组成,用户可以在其中选择要编辑的元素。到目前为止,我所做的是我拥有一个数据模型,所有CRUD操作都通过该数据模型进行。我使用TableModel使数据模型适应JTable。因此,要更新元素,我将调用table.getModel()。getModel()。update(Element e)。换句话说,JTable现在是Controller。所有按钮动作都放置在单独的类中(我在不同的上下文中重复使用它们),并通过基础模型的方法完成其工作。这是可行的设计吗?
sbrattla 2011年

1

为了进行适当的分离,您通常会有一个Frame类将委派给的控制器类。可以通过多种方法来建立类之间的关系-您可以实现一个控制器并将其与主视图类一起扩展,或者使用事件发生时Frame调用的独立控制器类。该视图通常将通过实现侦听器接口从控制器接收事件。

有时,MVC模式的一个或多个部分是微不足道的,或者如此“薄”,以至于增加了不必要的复杂性以将它们分开。如果您的控制器充满了一个线路调用,则将其放在单独的类中可能会最终混淆基本行为。例如,如果您要处理的所有事件都与TableModel有关,并且都是简单的添加和删除操作,则您可以选择在该模型中实现所有表操作功能(以及将其显示在模型中的回调JTable)。这不是真正的MVC,但是可以避免在不需要的地方增加复杂性。

无论您如何实现它,都请记住使用JavaDoc来记录您的类,方法和包,以便正确描述组件及其关系!


@AndyT虽然您的大多数解释都很好,但您对将模型与控制器结合使用的建议还是有疑问的。如果我想突然更换控制器怎么办?现在,我发现您已将模型与控制器耦合在一起,并且还需要修改模型。您的代码不再可扩展。但是简短的控制器,我不会将其与模型相结合。或视图。
2011年

我不会不同意-这在很大程度上取决于您的应用程序。如果您的模型仅比List对象复杂,并且您的控制器仅做添加和删除元素的事情,那么创建三个单独的类(List模型,Controller和您的模型与JTable配合使用的适配器)就太过分了。在不太可能的情况下,您需要一个不同的控制器来重构它,比出于某些未知的未来需求而产生垫片类要容易得多。
AndyT 2011年

@AndyT同意,也许如果您的应用程序很小,这可能是最快的方法。但是出于可扩展性的考虑(考虑是否由同一位程序员进行添加),这可能会不利。
Dhruv Gairola 2011年

2
@AndyT:我不知道您已经开发软件多长时间了,但是您的帖子表明您已经接受了KISS原则。太多聪明但经验不足的Java开发人员像圣经一样拥护设计模式(设计模式只不过是高级剪切和粘贴编程)。在大多数情况下,通过建立单独的控制器和视图类采取纯粹的方法,只会使除原始开发人员之外的任何人都无法维护比数百行代码大的程序。如有疑问,请保持简单,愚蠢!
比特币2011年

1
@AndyT:开悟的道路上布满了坑洞,蛇油推销员和自以为是。然而,没有什么比在一个持续的时间里沉迷于自己的排便要教人保持简单的了。设计模式没有错。但是,了解设计模式与了解软件设计不同。从未使用菜谱方法构建突破性的软件产品。设计满足要求且易于维护的高性能软件仍然是一种需要多年掌握的艺术形式。
比特商2011年


0

如果使用GUI开发程序,则mvc模式几乎存在,但模糊不清。

剖析模型,视图和控制器代码很困难,并且通常不仅是重构任务。

您知道代码可重用时就拥有它。如果您已正确实施MVC,则应易于实施具有相同功能TUICLIRWD移动优先设计。在现有代码上,很容易看到比实际完成的事情。

实际上,模型,视图和控制器之间的交互是使用其他隔离模式(例如观察者或侦听器)进行的

我猜这篇文章详细解释了它,从直接的非MVC模式(就像您在Q&D上所做的那样)到最终的可重用实现:

http://www.austintek.com/mvc/

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.