MVC 模式和 Swing

在“真实的 Swing 生活”中,我发现最难掌握的设计模式之一是 MVC 模式。在这个站点上,我已经阅读了很多讨论模式的文章,但是我仍然觉得自己对如何利用 Java Swing 应用程序中的模式没有一个清晰的理解。

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

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

我真的很想听听别人是怎么看待这件事的。当您需要使用 MVC 模式向用户显示一个表、几个字段和一些按钮时,您是如何做到的呢?

78089 次浏览

You can create model in a separate, plain Java class, and controller in another.

Then you can have Swing components on top of that. JTable would be one of the views (and table model would de facto be part of the view - it would only translate from the "shared model" to JTable).

Whenever the table is edited, its table model tells the "main controller" to update something. However, the controller should know nothing about the table. So the call should look more like: updateCustomer(customer, newValue), not updateCustomer(row, column, newValue).

Add a listener (observer) interface for the shared model. Some components (e.g. your table) could implement it directly. Another observer could be the controller that coordinates button availability etc.


That's one way to do it, but of course you can simplify or extend it if its an overkill for your use case.

You can merge the controller with model and have the same class process updates and maintain component availability. You even can make the "shared model" a TableModel (though if it's not only used by the table, I would recommend at least providing a friendlier API that doesn't leak table abstractions)

On the other hand, you can have complex interfaces for updates (CustomerUpdateListener, OrderItemListener, OrderCancellationListener) and dedicated controller (or mediator) only for coordination of different views.

It depends on how complicated your problem is.

A book I'd highly recommend to you for MVC in swing would be "Head First Design Patterns" by Freeman and Freeman. They have a highly comprehensive explanation of MVC.

Brief Summary

  1. You're the user--you interact with the view. The view is your window to the model. When you do something to the view (like click the Play button) then the view tells the controller what you did. It's the controller's job to handle that.

  2. The controller asks the model to change its state. The controller takes your actions and interprets them. If you click on a button, it's the controller's job to figure out what that means and how the model should be manipulated based on that action.

  3. The controller may also ask the view to change. When the controller receives an action from the view, it may need to tell the view to change as a result. For example, the controller could enable or disable certain buttons or menu items in the interface.

  4. The model notifies the view when its state has changed. When something changes in the model, based either on some action you took (like clicking a button) or some other internal change (like the next song in the playlist has started), the model notifies the view that its state has changed.

  5. The view asks the model for state. The view gets the state it displays directly from the model. For instance, when the model notifies the view that a new song has started playing, the view requests the song name from the model and displays it. The view might also ask the model for state as the result of the controller requesting some change in the view.

enter image description here Source (In case you're wondering what a "creamy controller" is, think of an Oreo cookie, with the controller being the creamy center, the view being the top biscuit and the model being the bottom biscuit.)

Um, in case you're interested, you could download a fairly entertaining song about the MVC pattern from here!

One issue you may face with Swing programming involves amalgamating the SwingWorker and EventDispatch thread with the MVC pattern. Depending on your program, your view or controller might have to extend the SwingWorker and override the doInBackground() method where resource intensive logic is placed. This can be easily fused with the typical MVC pattern, and is typical of Swing applications.

EDIT #1:

Additionally, it is important to consider MVC as a sort of composite of various patterns. For example, your model could be implemented using the Observer pattern (requiring the View to be registered as an observer to the model) while your controller might use the Strategy pattern.

EDIT #2:

I would additionally like to answer specifically your question. You should display your table buttons, etc in the View, which would obviously implement an ActionListener. In your actionPerformed() method, you detect the event and send it to a related method in the controller (remember- the view holds a reference to the controller). So when a button is clicked, the event is detected by the view, sent to the controller's method, the controller might directly ask the view to disable the button or something. Next, the controller will interact with and modify the model (which will mostly have getter and setter methods, and some other ones to register and notify observers and so on). As soon as the model is modified, it will call an update on registered observers (this will be the view in your case). Hence, the view will now update itself.

For proper separation, you would typically have a controller class that the Frame class would delegate to. There are various ways to set up the relationships between the classes - you could implement a controller and extend it with your main view class, or use a standalone controller class that the Frame calls when events occur. The view would typically receive events from the controller by implementing a listener interface.

Sometimes one or more parts of the MVC pattern are trivial, or so 'thin' that it adds unnecessary complexity to separate them out. If your controller is full of one line calls, having it in a separate class can end up obfuscating the underlying behaviour. For instance, if the all of the events you are handling are related to a TableModel and are simple add and delete operations you might choose to implement all of the table manipulation functions within that model (as well as the callbacks necessary to display it in the JTable). It's not true MVC, but it avoids adding complexity where it isn't needed.

However you implement it, remember to JavaDoc your classes, methods and packages so that the components and their relationships are properly described!

I have found some interesting articles about implementing MVC Patterns, which might solve your problem.

Not a fan of the idea that the view should be the one to be notified by the model when its data changes. I would delegate that functionality to the controller. In that case, if you change the application logic, you don't need to interfere to the view's code. The view's task is only for the applications components + layout nothing more nothing less. Layouting in swing is already a verbose task, why let it interfere with the applications logic?

My idea of MVC (which I'm currently working with, so far so good) is :

  1. The view is the dumbest of the three. It doesn't know anything about the controller and the model. Its concern is only the swing components' prostethics and layout.
  2. The model is also dumb, but not as dumb as the view. It performs the following functionalities.
  • a. when one of its setter is called by the controller, it will fire notification to its listeners/observers (like I said, I would deligate this role to the controller). I prefer SwingPropertyChangeSupport for achieving this since its already optimized for this purpose.
  • b. database interaction functionality.
  1. A very smart controller. Knows the view and the model very well. The controller has two functionalities:
  • a. It defines the action that the view will execute when the user interacts to it.
  • b. It listens to the model. Like what I've said, when the setter of the model is called, the model will fire notification to the controller. It's the controller's job to interpret this notification. It might need to reflect the change to the view.

Code Sample

The View :

Like I said creating the view is already verbose so just create your own implementation :)

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

It's ideal to interface the three for testability purposes. I only provided my implementation of Model and Controller.

The 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
...
}

The Controller :

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

The Main, where the MVC is setup :

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

The MVC pattern is a model of how a user interface can be structured. Therefore it defines the 3 elements Model, View, Controller:

  • Model A model is an abstraction of something that is presented to the user. In swing you have a differentiation of gui models and data models. GUI models abstract the state of a ui component like ButtonModel. Data models abstract structured data that the ui presents to the user like TableModel.
  • View The view is a ui component that is responsible for presenting data to the user. Thus it is responsible for all ui dependent issues like layout, drawing, etc. E.g. JTable.
  • Controller A controller encapsulates the application code that is executed in order to an user interaction (mouse motion, mouse click, key press, etc.). Controllers might need input for their execution and they produce output. They read their input from models and update models as result of the execution. They might also restructure the ui (e.g. replace ui components or show a complete new view). However they must not know about the ui compoenents, because you can encapsulate the restructuring in a separate interface that the controller only invokes. In swing a controller is normally implemented by an ActionListener or Action.

Example

  • Red = model
  • Green = view
  • Blue = controller

enter image description here

When the Button is clicked it invokes the ActionListener. The ActionListener only depends on other models. It uses some models as it's input and others as it's result or output. It's like method arguments and return values. The models notify the ui when they get updated. So there is no need for the controller logic to know the ui component. The model objects don't know the ui. The notification is done by an observer pattern. Thus the model objects only know that there is someone who wants to get notified if the model changes.

In java swing there are some components that implement a model and controller as well. E.g. the javax.swing.Action. It implements a ui model (properties: enablement, small icon, name, etc.) and is a controller because it extends ActionListener.

A detailed explanation, example application and source code: https://www.link-intersystems.com/blog/2013/07/20/the-mvc-pattern-implemented-with-java-swing/.

MVC basics in less than 260 lines:

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

If you develop a program with a GUI, mvc pattern is almost there but blurred.

Disecting model, view and controller code is difficult, and normally is not only a refactor task.

You know you have it when your code is reusable. If you have correctly implemented MVC, should be easy to implement a TUI or a CLI or a RWD or a mobile first design with the same functionality. It's easy to see it done than do it actually, moreover on an existing code.

In fact, interactions between model, view and controller happens using other isolation patterns (as Observer or Listener)

I guess this post explains it in detail, from the direct non MVC pattern (as you will do on a Q&D) to the final reusable implementation:

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