MVC模式与Swing

87

我认为在“真实的Swing生活”中最难理解的设计模式之一是MVC模式。我已经阅读了该站点上许多讨论该模式的帖子,但我仍然不清楚如何在我的Java Swing应用程序中利用该模式。

假设我有一个包含表格、几个文本字段和一些按钮的JFrame。我可能会使用TableModel将JTable与底层数据模型“桥接”起来。然而,所有负责清除字段、验证字段、锁定字段以及按钮操作的函数通常都会直接放在JFrame中。但是,这难道不会混淆模式的控制器和视图吗?

据我所见,当我查看JTable(和模型)时,我成功地实现了MVC模式,但是当我将整个JFrame作为一个整体来看时,情况就变得混乱了。

我很想听听其他人在使用MVC模式显示表格、几个字段和一些按钮时是如何处理的。


3
这是一个相关的例子 - trashgod
1
对于其他参与者 - Swing不是纯MVC - 它从概念中借鉴了很多,但是“视图和控制器”被“合并”在一起。 - MadProgrammer
7个回答

114
我强烈推荐您阅读Freeman和Freeman的《Head First设计模式》这本书,它对MVC有非常全面的解释。
简要概述如下:

您是用户,与视图交互。 视图是您查看模型的窗口。当您对视图执行某些操作(例如单击“播放”按钮)时,视图会将此操作告诉控制器。控制器负责处理该操作。

控制器请求模型更改其状态。 控制器接受您的操作并解释它们。如果您单击一个按钮,控制器的工作就是弄清楚这意味着什么,以及基于该操作应如何操作模型。

控制器也可以要求视图更改。 当控制器从视图接收到一个操作时,可能需要告诉视图进行相应的更改。例如,控制器可以在界面中启用或禁用某些按钮或菜单项。

模型在其状态发生更改时通知视图。 当模型发生更改时,基于您采取的某些操作(例如单击按钮)或某些其他内部更改(例如播放列表中的下一首歌曲已开始),模型会通知视图其状态已更改。

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

enter image description here

源代码(如果您想知道“奶油控制器”是什么,可以想象一下奥利奥饼干,其中控制器是奶油中心,视图是上面的饼干,模型是下面的饼干。)
嗯,如果您感兴趣,您可以从这里下载一首关于MVC模式的相当有趣的歌!
Swing编程可能会遇到的一个问题涉及将SwingWorker和EventDispatch线程与MVC模式结合起来。根据您的程序,您的视图或控制器可能必须扩展SwingWorker并覆盖doInBackground()方法,在其中放置资源密集型逻辑。这可以很容易地与典型的MVC模式融合在一起,并且是Swing应用程序的典型。
此外,重要的是将MVC视为各种模式的组合。例如,您的模型可以使用观察者模式实现(需要将View注册为观察者),而您的控制器可能使用策略模式。
我还想具体回答您的问题。您应该在View中显示表格按钮等内容,它显然会实现ActionListener。在您的actionPerformed()方法中,检测事件并将其发送到控制器相关方法(记住-视图持有对控制器的引用)。因此,当单击按钮时,事件被视图检测到,发送到控制器的方法,控制器可能直接要求视图禁用按钮或其他操作。接下来,控制器将与模型交互并修改它(大多数情况下将有getter和setter方法以及一些其他方法来注册和通知观察者等)。只要模型被修改,就会调用已注册观察者的更新(在您的情况下为View)。因此,视图现在将更新自己。

1
我确实读过这本书,但我发现很难将这个模式应用到SWING上。我也读过一些地方提到JFrame也可以被看作是代表视图和控制器的双重身份。 - sbrattla
1
JFrame是一个组件,而不是叶子。通常,控制器所做的更新会发送到JFrame,它会处理其余的部分,因此,这可能会让人误以为它是一个控制器,但实际上并非如此,因为它没有改变模型,只是视图。如果您的JFrame直接改变了模型-那么您就错了。 - Dhruv Gairola
再强调一遍,这里的关键词是“直接”。在您的情况下,您可以监听表格上的鼠标点击,并将逻辑发送到控制器中修改表格模型的方法。 - Dhruv Gairola
2
@DhruvGairola 第二点描述是为了第三点,而第三和第四点有相同的重复描述。请你纠正一下。 - Naruto Biju Mode
那首歌是经典之作!=D - aaiezza

38

我不太喜欢模型更改数据后需要通知视图的想法,我会将这个功能委托给控制器。这样,如果您更改了应用程序的逻辑,就不需要干预视图的代码。视图的任务仅仅是为应用程序组件和布局服务而已。在swing中进行布局已经是一个冗长的任务了,为什么要让它干扰应用程序的逻辑呢?

我的MVC思想(我目前正在使用,效果不错)是:

  1. 视图是三者中最简单的。它对控制器和模型一无所知,只关心Swing组件的外观和布局。
  2. 模型也很简单,但比视图稍微复杂一些。它执行以下功能:
  • a. 当控制器调用其setter之一时,它将向其侦听器/观察者发出通知(如我所说,我会将此角色委托给控制器)。我喜欢使用SwingPropertyChangeSupport来实现这一点,因为它已经针对此目的进行了优化。
  • b. 数据库交互功能。
  1. 非常聪明的控制器。它非常了解视图和模型。控制器具有两个功能:
  • a. 它定义视图在用户交互时执行的操作。
  • b. 它侦听模型。就像我所说的,当调用模型的setter时,模型将向控制器发出通知。这是控制器的工作来解释此通知。它可能需要将更改反映到视图中。

代码示例

视图:

就像我说的,创建视图已经够冗长的了,所以请自己编写实现吧 :)

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

为了测试的目的,将这三者连接起来是理想的。我仅提供了我的 Model 和 Controller 实现。

Model:

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被设置:

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
展示单一实体模型在多个视图中时,会变得有趣但效率较低。因此你的设计可能会导致一个“大控制器”来处理单一模型并管理所有相关视图。如果您尝试重用一组“小模型”,将其聚合成一个“大模型”,因为某个视图显示了分散在多个“小模型”实体中的信息,则情况会变得更加棘手。 - Yves Martin
1
@onepotato 我刚试过你的代码。当我按下一个按钮时,setUpViewEvents() 中的代码可以正常执行。但是,当我调用 model.setSomething(123) 时,propertyChange 中的代码没有被触发。我甚至在 Object newVal = evt.getNewValue(); 下面直接加了一个 println 语句,但并未打印出来。 - AmuletxHeart
16
这不是MVC架构模式,而是与之密切相关的MVP(Model-View-Presenter)模式。在典型的MVC中,正是模型的工作将更改通知视图,这正是你“不喜欢”的部分。请查看此图表以了解典型MVC中的交互如何工作。 - MaxAxeHax

32
MVC模式是用户界面结构的一种模型。因此,它定义了三个元素:模型、视图和控制器。
  • 模型 模型是向用户呈现的某个东西的抽象表示。在swing中,你有GUI模型和数据模型的区别。GUI模型抽象了UI组件状态,例如ButtonModel。数据模型抽象了UI向用户呈现的结构化数据,例如TableModel
  • 视图 视图是负责向用户呈现数据的UI组件。因此,它负责所有与UI相关的问题,例如布局、绘制等。例如JTable
  • 控制器 控制器封装了执行应用程序代码的用户交互(鼠标移动、鼠标点击、按键等)。控制器可能需要输入来执行其操作,并生成输出。它们从模型中读取输入并在执行的结果中更新模型。它们还可以重构UI(例如替换UI组件或显示完全新的视图)。但是,它们不必知道UI组件,因为您可以将重构封装在单独的接口中,控制器只调用该接口。在swing中,控制器通常由ActionListenerAction实现。

例子

  • 红色 = 模型(model)
  • 绿色 = 视图(view)
  • 蓝色 = 控制器(controller)

enter image description here

当单击“按钮”时,它会调用“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/

不到260行的MVC基础知识:

import java.awt.BorderLayout;
import java.awt.Container;
import java.awt.event.ActionEvent;
import java.util.ArrayList;
import java.util.List;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.DefaultListModel;
import javax.swing.DefaultListSelectionModel;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JList;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.ListSelectionModel;
import javax.swing.WindowConstants;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.PlainDocument;

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;
    }
}

MVC Basics Screencast


5
我喜欢这个回答+1,因为它将“Action”称为“控制器”,事实上我认为所有的“EventListener”都是控制器。 - nachokk
@nachokk 是的,确实如此。正如我所说,“控制器封装了应用程序代码,以便执行用户交互”。移动鼠标、单击组件、按键等都是用户交互。为了更清晰明了,我更新了我的答案。 - René Link

2
您可以在一个单独的普通Java类中创建模型,而在另一个类中创建控制器。然后您可以在其上方放置Swing组件。其中之一是JTable视图(表格模型实际上是视图的一部分 - 它只会从“共享模型”转换为JTable)。
每当编辑表格时,其表格模型会告诉“主控制器”更新某些内容。但是,控制器不应了解表格。因此,调用应该更像:updateCustomer(customer, newValue),而不是updateCustomer(row, column, newValue)
为共享模型添加一个监听器(观察者)接口。一些组件(例如您的表格)可以直接实现它。另一个观察者可以是协调按钮可用性等的控制器。
这是一种方法,但如果对于您的用例来说过于复杂,则当然可以简化或扩展它。
您可以将控制器与模型合并,并使用相同的类处理更新并维护组件可用性。您甚至可以使“共享模型”成为一个TableModel(尽管如果它不仅被表格使用,我建议提供一个友好的API,不要泄漏表格抽象)。
另一方面,您可以针对更新(CustomerUpdateListenerOrderItemListenerOrderCancellationListener)拥有复杂的接口,并且专门为不同视图的协调器(或中介者)。
这取决于您的问题有多么复杂。

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

2
为了适当分离,通常会有一个控制器类来委托Frame类。有多种设置类之间关系的方法 - 你可以实现一个控制器并将其与主视图类扩展,或者使用独立的控制器类,当事件发生时Frame调用它。视图通常通过实现监听器接口从控制器接收事件。
有时,MVC模式中的一个或多个部分是微不足道的,或者“纤薄”到程度,以至于将它们分离出来会增加不必要的复杂性。例如,如果您处理的所有事件都与TableModel相关,并且是简单的添加和删除操作,您可能选择在该模型内实现所有表操作函数(以及在JTable中显示所需的回调)。这不是真正的MVC,但它避免了在不需要的地方增加复杂性。
无论您如何实现它,请记得JavaDoc您的类、方法和包,以便正确描述组件及其关系!

@AndyT,虽然你的解释大多数是正确的,但是我对你将模型与控制器结合的建议有所不同。如果我突然想要更改控制器怎么办?现在我发现你已经把模型和控制器耦合在一起,需要修改模型。这使得你的代码不再可扩展。无论控制器多短,我也不会将其与模型或视图结合在一起。 - Dhruv Gairola
我不会反对 - 这在很大程度上取决于您的应用程序。如果您的模型不比List对象更复杂,并且您的控制器只是添加和删除元素,那么创建三个单独的类(List模型、控制器和适配器,使您的模型与JTable一起工作)就有些过头了。如果您需要不同的控制器,将其重构出来要容易得多,而不是为了某些未知的未来需求而 churn out shim 类。 - AndyT
@AndyT同意,也许如果你的应用程序很小,这可能是最快的方法。但为了可扩展性(考虑到添加不是由同一程序员完成),这可能会带来不利影响。 - Dhruv Gairola
2
@AndyT:我不知道你开发软件有多久,但你的帖子表明你已经接受了KISS原则。太多聪明但缺乏经验的Java开发人员像信奉圣经一样信奉设计模式(设计模式只是高级剪贴板编程)。在大多数情况下,通过构建单独的控制器和视图类来采用纯粹主义方法只会使维护比代码行数超过几百行的程序对于除原始开发人员以外的任何人都成为噩梦。当你不确定时,保持简单,愚蠢! - bit-twiddler
1
@AndyT:通往启蒙之路上充满着坑洼、骗子和自以为是的人。然而,没有什么比长期陷入自己的排泄物中更能教人保持简单了。设计模式并没有错。然而,知道设计模式并不等于知道软件设计。没有任何突破性的软件产品是使用食谱式方法构建的。设计高性能、符合需求且易于维护的软件仍然是一门需要多年时间才能掌握的艺术。 - bit-twiddler
显示剩余2条评论

0

0

如果你使用GUI开发程序,那么MVC模式几乎已经到位但是有些模糊。

分解模型、视图和控制器代码很困难,通常不仅仅是一个重构任务。

当你的代码可重用时,你就知道你已经正确实现了MVC。如果你正确地实现了MVC,那么使用相同功能应该很容易实现TUICLIRWD移动优先设计。看起来比实际操作容易,特别是在现有代码上。

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

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

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接