简体   繁体   English

Model-View-Presenter被动视图:bootstraping - 谁最初显示视图?

[英]Model-View-Presenter passive view: bootstraping - who displays the view initially?

In the Passive View Model View Presenter pattern, who has the responsibility for displaying the view? 在Passive View Model View Presenter模式中,谁负责显示视图? I have found related answers for other MVP versions, but they don't seem applicable to the passive view version. 我找到了其他MVP版本的相关答案,但它们似乎不适用于被动视图版本。

I have a concrete example using Java Swing. 我有一个使用Java Swing的具体示例。 It's pretty simple, but basically we have a SwingCustomersView which internally builds a JPanel with a table (list of customers) and a label displaying the currently selected customers age. 它非常简单,但基本上我们有一个SwingCustomersView ,它在内部构建一个JPanel,其中包含一个表(客户列表)和一个显示当前所选客户年龄的标签。 When a customer is selected in the table, the presenter retrieves the selected customer age from the model. 在表格中选择客户后,演示者将从模型中检索选定的客户年龄。 I think the example is a correct implementation of MVP Passive View, but correct me if I'm wrong. 我认为这个例子是MVP被动视图的正确实现,但如果我错了,请纠正我。

The question is how do we bootstrap these classes? 问题是我们如何引导这些类? For example, if we wanted to display the SwingCustomersView in a JFrame. 例如,如果我们想在JFrame中显示SwingCustomersView How would one do that? 如何做到这一点? I imagine something along the lines of: 我想象的是:

void launcher() {
    CustomersModel model = new CustomersModel();
    SwingCustomersView view = new SwingCustomersView();
    CustomersPresenter presenter = new CustomersPresenter(view, model);
}

This is the initial wiring, but nothing is displayed yet. 这是初始接线,但尚未显示任何内容。 How do we actually display the view? 我们如何实际显示视图? Is it the responsibility of (1) launcher() , (2) SwingCustomersView or (3) CustomersPresenter to display the view? (1) launcher() ,(2) SwingCustomersView或(3) CustomersPresenter是否有责任显示视图? Unfortunately I don't believe any of those are very good as you can see from my thoughts below. 不幸的是,我不相信这些都是非常好的,你可以从下面的想法中看到。 Perhaps there's another way? 也许还有另一种方式?

(1.a): launcher (1.a):发射器

Make SwingCustomersView extend JFrame and make it add it's internal JPanel to the content pane of itself. 使SwingCustomersView扩展JFrame并使其将内部JPanel添加到自身的内容窗格中。 Then we can do this: 然后我们可以这样做:

void launcher() {
    CustomersModel model = new CustomersModel();
    SwingCustomersView view = new SwingCustomersView();
    CustomersPresenter presenter = new CustomersPresenter(view, model);
    view.setVisible(true); // Displays the view
}

However in this case we don't use the presenter instance for anything. 但是在这种情况下,我们不会将presenter实例用于任何事情。 Isn't that strange? 这不奇怪吗? It's just there for wiring, we could just as well delete the variable and just do new CustomersPresenter(view, model) . 它只是用于布线,我们也可以删除变量,然后只做new CustomersPresenter(view, model)

(2): SwingCustomersView (2):SwingCustomersView

Make SwingCustomersView take a Container in the constructor to which it should add it's internal JPanel: SwingCustomersView在构造函数中获取一个Container ,它应该添加它的内部JPanel:

void launcher() {
    CustomersModel model = new CustomersModel();
    JFrame frame = new JFrame("Some title");
    SwingCustomersView view = new SwingCustomersView(frame.getContentPane());
    CustomersPresenter presenter = new CustomersPresenter(view, model);
    frame.pack(); 
    frame.setVisible(true) // Displays the view
}

However, same problem as (1): the presenter instance does nothing. 但是,与(1)相同的问题: presenter实例什么都不做。 It seems strange. 这看起来很奇怪。 Furthermore with both (1) and (2) it is possible to display the view before the presenter is hooked up, which I imagine could cause strange results in some situations. 此外,使用(1)和(2)两者都可以在演示者连接之前显示视图,我想在某些情况下可能会导致奇怪的结果。

(3): CustomersPresenter (3):CustomersPresenter

Make CustomersPresenter responsible for displaying the view somwhow. CustomersPresenter负责显示视图somwhow。 Then we could do this: 然后我们可以这样做:

void launcher() {
    CustomersModel model = new CustomersModel();
    SwingCustomersView view = new SwingCustomersView();
    CustomersPresenter presenter = new CustomersPresenter(view, model);
    presenter.show() // Displays the view
}

This would solve the problem of not using it for anything after construction. 这将解决在施工后不使用它的问题。 But I don't see how do to this without either changing the CustomersView interface or making CustomersPresenter too dependent on the underlying GUI implementation. 但是,如果不更改CustomersView接口或使CustomersPresenter过于依赖底层GUI实现,我不知道如何做到这一点。 Furthermore, displaying a view doesn't sound like presentation logic and thus doesn't seem to belong in the presenter. 此外,显示视图听起来不像表示逻辑,因此似乎不属于演示者。

Example

public class CustomersModel {
    private List<Customer> customers;

    public CustomersModel() {
        customers = new ArrayList<Customer>();
        customers.add(new Customer("SomeCustomer", "31"));
        customers.add(new Customer("SomeCustomer", "32"));
    }

    public List<Customer> getCustomers() {
        return customers;
    }
}

public class Customer {
    public String name;
    public String age;

    public Customer(String name, String age) {
        this.name = name;
        this.age = age;
    }
}

public interface CustomersView {
    void addCustomerSelectionChangeListener(ItemListener listener);
    void onNewActiveCustomer(String age);
    void onNewCustomers(List<String> newCustomers);
}

public class SwingCustomersView implements CustomersView {
    // Swing components here all put into a main JPanel

    public void addCustomerSelectionChangeListener(ItemListener listener) {
       // Add event listener to table
    }

    public void onNewActiveCustomer(String age) {
        // Display age in label beneath table
    }

    public void onNewCustomers(List<String> newCustomers) {
        // Display customers in table
    }
}

public class CustomersPresenter {
    private final CustomersView view;
    private final CustomersModel model;

    public CustomersPresenter(CustomersView view, CustomersModel model) {
        this.view = view;
        this.model = model;
        initPresentationLogic();
        populateView();
    }

    private void initPresentationLogic() {
        view.addCustomerSelectionChangeListener(new ItemListener() {
            @Override
            public void itemStateChanged(ItemEvent e) {
                String selectedName = (String)e.getItem();
                List<Customer> customers = model.getCustomers();
                for (Customer c : customers)
                    if (c.name.equals(selectedName))
                        view.onNewActiveCustomer(c.age);
            }
        });
    }

    private void populateView() {
        List<Customer> customers = model.getCustomers();
        List<String> names = new ArrayList<String>();
        for (Customer c : customers)
            names.add(c.name);

        // View will now populate its table, which in turn will call customerSelectionChangeListener
        // so the view 'automagically' updates the selected contact age too
        view.onNewCustomers(names);
    }
}

Option (3) all the way. 选项(3)一直。 It is the presenter's jobs for "controlling" the view, which includes making it visible. 这是演示者“控制”视图的工作,包括使其可见。 Yes, you'll need to add to the view's interface to allow this to happen, but that's not a big deal. 是的,您需要添加到视图的界面以允许这种情况发生,但这不是什么大问题。 Remember, you can make the view is as passive as possible. 请记住,您可以使视图尽可能地被动。 No logic whatsoever! 没有任何逻辑!

Working Example: 工作实例:

I stumbled upon this example of a simple Swing game using an MVC architecture. 我偶然发现了一个使用MVC架构的简单Swing游戏的例子 Since I write my Swing apps using MVP instead of MVC, I can't say with authority if this example is a true and pure example of MVC. 由于我使用MVP而不是MVC来编写我的Swing应用程序,所以我不能说具有权威性,如果这个例子是一个真实而纯粹的MVC例子。 It looks okay to me, and the author trashgod has more than proven himself here on SO using Swing, so I'll accept it as reasonable. 对我来说看起来没问题,并且作者trashgod在使用Swing时已经证明了自己在这里,所以我会接受它是合理的。

As an exercise, I decided to rewrite it using an MVP architecture. 作为练习,我决定使用MVP架构重写它。


The Driver: 司机:

As you can see in the code below, this is pretty simple. 正如您在下面的代码中看到的,这非常简单。 What should jump out at you are the separation of concerns (by inspecting the constructors): 应该跳出来的是关注点的分离(通过检查构造函数):

  • The Model class is standalone and has no knowledge of Views or Presenters. Model类是独立的,不了解Views或Presenters。

  • The View interface is implemented by a standalone GUI class, neither of which have any knowledge of Models or Presenters. View界面由独立的GUI类实现,它们都不具备模型或演示者的知识。

  • The Presenter class knows about both Models and Views. Presenter类了解模型和视图。

Code: 码:

import java.awt.*;

/**
 * MVP version of https://stackoverflow.com/q/3066590/230513
 */
public class MVPGame implements Runnable
{
  public static void main(String[] args)
  {
    EventQueue.invokeLater(new MVPGame());
  }

  @Override
  public void run()
  {
    Model model = new Model();
    View view = new Gui();
    Presenter presenter = new Presenter(model, view);
    presenter.start();
  }
}

and the GamePiece that we'll be using for the game: 以及我们将用于游戏的GamePiece:

import java.awt.*;

public enum GamePiece
{
  Red(Color.red), Green(Color.green), Blue(Color.blue);
  public Color color;

  private GamePiece(Color color)
  {
    this.color = color;
  }
}

The Model: Primarily, the job of the Model is to: 模型:主要是模型的工作是:

  • Provide data for the UI (upon request) 为UI提供数据(根据要求)
  • Validation of data (upon request) 验证数据(根据要求)
  • Long-term storage of data (upon request) 长期存储数据(根据要求)

Code: 码:

import java.util.*;

public class Model
{
  private static final Random rnd = new Random();
  private static final GamePiece[] pieces = GamePiece.values();

  private GamePiece selection;

  public Model()
  {
    reset();
  }

  public void reset()
  {
    selection = pieces[randomInt(0, pieces.length)];
  }

  public boolean check(GamePiece guess)
  {
    return selection.equals(guess);
  }

  public List<GamePiece> getAllPieces()
  {
    return Arrays.asList(GamePiece.values());
  }

  private static int randomInt(int min, int max)
  {
    return rnd.nextInt((max - min) + 1) + min;
  }
}

The View: The idea here is to make it as "dumb" as possible by stripping out as much application logic as you can (the goal is to have none). 观点:这里的想法是通过尽可能多地剥离应用程序逻辑使其尽可能“愚蠢”(目标是没有)。 Advantages: 好处:

  • The app now be 100% JUnit testable since no application logic is mixed in with Swing code 该应用程序现在是100% JUnit可测试的,因为没有应用程序逻辑与Swing代码混合在一起
  • You can launch the GUI without launching the entire app, which makes prototyping much faster 您可以在不启动整个应用程序的情况下启动GUI,这样可以更快地进行原型设计

Code: 码:

import java.awt.*;
import java.awt.event.*;
import java.util.List;

public interface View
{
  public void addPieceActionListener(GamePiece piece, ActionListener listener);
  public void addResetActionListener(ActionListener listener);
  public void setGamePieces(List<GamePiece> pieces);
  public void setResult(Color color, String message);
}

and the GUI: 和GUI:

import java.awt.*;
import java.awt.event.*;
import java.util.List;
import javax.swing.*;

/**
 * View is "dumb". It has no reference to Model or Presenter.
 * No application code - Swing code only!
 */
public class Gui implements View
{
  private JFrame frame;
  private ColorIcon icon;
  private JLabel resultLabel;
  private JButton resetButton;
  private JButton[] pieceButtons;
  private List<GamePiece> pieceChoices;

  public Gui()
  {
    frame = new JFrame();
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    icon = new ColorIcon(80, Color.WHITE);
  }

  public void setGamePieces(List<GamePiece> pieces)
  {
    this.pieceChoices = pieces;

    frame.add(getMainPanel());
    frame.pack();
    frame.setLocationRelativeTo(null);
    frame.setVisible(true);
  }

  public void setResult(Color color, String message)
  {
    icon.color = color;
    resultLabel.setText(message);
    resultLabel.repaint();
  }

  private JPanel getMainPanel()
  {
    JPanel panel = new JPanel(new BorderLayout());
    panel.add(getInstructionPanel(), BorderLayout.NORTH);
    panel.add(getGamePanel(), BorderLayout.CENTER);
    panel.add(getResetPanel(), BorderLayout.SOUTH);
    return panel;
  }

  private JPanel getInstructionPanel()
  {
    JPanel panel = new JPanel();
    panel.add(new JLabel("Guess what color!", JLabel.CENTER));
    return panel;
  }

  private JPanel getGamePanel()
  {
    resultLabel = new JLabel("No selection made", icon, JLabel.CENTER);
    resultLabel.setVerticalTextPosition(JLabel.BOTTOM);
    resultLabel.setHorizontalTextPosition(JLabel.CENTER);

    JPanel piecePanel = new JPanel();
    int pieceCount = pieceChoices.size();
    pieceButtons = new JButton[pieceCount];

    for (int i = 0; i < pieceCount; i++)
    {
      pieceButtons[i] = createPiece(pieceChoices.get(i));
      piecePanel.add(pieceButtons[i]);
    }

    JPanel panel = new JPanel(new BorderLayout());
    panel.add(resultLabel, BorderLayout.CENTER);
    panel.add(piecePanel, BorderLayout.SOUTH);

    return panel;
  }

  private JPanel getResetPanel()
  {
    resetButton = new JButton("Reset");

    JPanel panel = new JPanel();
    panel.add(resetButton);
    return panel;
  }

  private JButton createPiece(GamePiece piece)
  {
    JButton btn = new JButton();
    btn.setIcon(new ColorIcon(16, piece.color));
    btn.setActionCommand(piece.name());
    return btn;
  }

  public void addPieceActionListener(GamePiece piece, ActionListener listener)
  {
    for (JButton button : pieceButtons)
    {
      if (button.getActionCommand().equals(piece.name()))
      {
        button.addActionListener(listener);
        break;
      }
    }
  }

  public void addResetActionListener(ActionListener listener)
  {
    resetButton.addActionListener(listener);
  }

  private class ColorIcon implements Icon
  {
    private int size;
    private Color color;

    public ColorIcon(int size, Color color)
    {
      this.size = size;
      this.color = color;
    }

    @Override
    public void paintIcon(Component c, Graphics g, int x, int y)
    {
      Graphics2D g2d = (Graphics2D) g;
      g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
          RenderingHints.VALUE_ANTIALIAS_ON);
      g2d.setColor(color);
      g2d.fillOval(x, y, size, size);
    }

    @Override
    public int getIconWidth()
    {
      return size;
    }

    @Override
    public int getIconHeight()
    {
      return size;
    }
  }
}

What might not be so obvious right away is how large the View interface can get. 立即可能不那么明显的是View界面可以获得多大的效果。 For each Swing component on the GUI, you may want to: 对于GUI上的每个Swing组件,您可能希望:

  • Add/Remove a listener to the component, of which there are many types (ActionListener, FocusListener, MouseListener, etc.) 添加/删除组件的侦听器,其中有许多类型(ActionListener,FocusListener,MouseListener等)
  • Get/Set the data on the component 获取/设置组件上的数据
  • Set the "usability" state of the component (enabled, visible, editable, focusable, etc.) 设置组件的“可用性”状态(启用,可见,可编辑,可聚焦等)

This can get unwieldy really fast. 这可能很快变得笨拙。 As a solution (not shown in this example), a key is created for each field, and the GUI registers each component with it's key (a HashMap is used). 作为解决方案(在该示例中未示出),为每个字段创建密钥,并且GUI使用其密钥注册每个组件(使用HashMap)。 Then, instead of the View defining methods such as: 然后,而不是视图定义方法,如:

public void addResetActionListener(ActionListener listener);
// and then repeat for every field that needs an ActionListener

you would have a single method: 你会有一个方法:

public void addActionListener(SomeEnum someField, ActionListener listener);

where "SomeEnum" is an enum that defines all fields on a given UI. 其中“SomeEnum”是一个定义给定UI上所有字段的enum Then, when the GUI receives that call, it looks up the appropriate component to call that method on. 然后,当GUI接收到该调用时,它会查找相应的组件以调用该方法。 All of this heavy lifting would get done in an abstract super class that implements View. 所有这些繁重的工作都将在一个实现View的抽象超类中完成。


The Presenter: The responsibilities are: 演讲者:职责是:

  • Initialize the View with it's starting values 使用它的起始值初始化视图
  • Respond to all user interactions on the View by attaching the appropriate listeners 通过附加适当的侦听器来响应View上的所有用户交互
  • Update the state of the View whenever necessary 必要时更新视图的状态
  • Fetch all data from the View and pass to Model for saving (if necessary) 从View中获取所有数据并传递给Model以进行保存(如有必要)

Code (note that there's no Swing in here): 代码(注意这里没有Swing):

import java.awt.*;
import java.awt.event.*;

public class Presenter
{
  private Model model;
  private View view;

  public Presenter()
  {
    System.out.println("ctor");
  }

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

  public void start()
  {
    view.setGamePieces(model.getAllPieces());
    reset();

    view.addResetActionListener(new ActionListener()
    {
      public void actionPerformed(ActionEvent e)
      {
        reset();
      }
    });

    for (int i = 0; i < GamePiece.values().length; i++)
    {
      final GamePiece aPiece = GamePiece.values()[i];
      view.addPieceActionListener(aPiece, new ActionListener()
      {
        public void actionPerformed(ActionEvent e)
        {
          pieceSelected(aPiece);
        }
      });
    }
  }

  private void reset()
  {
    model.reset();
    view.setResult(Color.GRAY, "Click a button.");
  }

  private void pieceSelected(GamePiece piece)
  {
    boolean valid = model.check(piece);
    view.setResult(piece.color, valid ? "Win!" : "Keep trying.");
  }
}

Keep in mind that each portion of the MVP architecture can/will be delegating to other classes (that are hidden to the other 2 portions) to perform many of its tasks. 请记住,MVP体系结构的每个部分都可以/将委派给其他类(隐藏到其他2个部分)以执行其许多任务。 The Model, View, and Presenter classes are just the upper divisions in your code base heirarchy. Model,View和Presenter类只是代码库层次结构中的上层。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM