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

joh*_*nrl 5 java mvp swing

在Passive View Model View Presenter模式中,谁负责显示视图?我找到了其他MVP版本的相关答案,但它们似乎不适用于被动视图版本.

我有一个使用Java Swing的具体示例.它非常简单,但基本上我们有一个SwingCustomersView内部构建带有表(客户列表)的JPanel和一个显示当前所选客户年龄的标签.在表格中选择客户后,演示者将从模型中检索选定的客户年龄.我认为这个例子是MVP被动视图的正确实现,但如果我错了,请纠正我.

问题是我们如何引导这些类?例如,如果我们想要SwingCustomersView在JFrame中显示.如何做到这一点?我想象的是:

void launcher() {
    CustomersModel model = new CustomersModel();
    SwingCustomersView view = new SwingCustomersView();
    CustomersPresenter presenter = new CustomersPresenter(view, model);
}
Run Code Online (Sandbox Code Playgroud)

这是初始接线,但尚未显示任何内容.我们如何实际显示视图?(1)launcher(),(2)SwingCustomersView或(3)是否有责任CustomersPresenter显示视图?不幸的是,我不相信这些都是非常好的,你可以从下面的想法中看到.也许还有另一种方式?

(1.a):发射器

制作SwingCustomersView扩展JFrame,并将其内部JPanel添加到自身的内容窗格中.然后我们可以这样做:

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

但是在这种情况下,我们不会将presenter实例用于任何事情.这不奇怪吗?它只是用于布线,我们也可以删除变量而只是做new CustomersPresenter(view, model).

(2):SwingCustomersView

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
}
Run Code Online (Sandbox Code Playgroud)

但是,与(1)相同的问题:presenter实例什么都不做.这看起来很奇怪.此外,使用(1)和(2)两者都可以在演示者连接之前显示视图,我想在某些情况下可能会导致奇怪的结果.

(3):CustomersPresenter

CustomersPresenter负责显示视图somwhow.然后我们可以这样做:

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

这将解决在施工后不使用它的问题.但是,如果不更改CustomersView接口或CustomersPresenter过于依赖底层GUI实现,我不知道如何做到这一点.此外,显示视图听起来不像表示逻辑,因此似乎不属于演示者.

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

spl*_*bob 7

选项(3)一直.这是演示者"控制"视图的工作,包括使其可见.是的,您需要添加到视图的界面以允许这种情况发生,但这不是什么大问题.请记住,您可以使视图尽可能地被动.没有任何逻辑!

工作实例:

我偶然发现了一个使用MVC架构的简单Swing游戏的例子.由于我使用MVP而不是MVC编写我的Swing应用程序,因此我不能说具有权威性,如果这个例子是一个真实而纯粹的MVC示例.对我来说看起来没问题,并且作者trashgod在使用Swing时已经证明了自己在这里,所以我会接受它是合理的.

作为练习,我决定使用MVP架构重写它.


司机:

正如您在下面的代码中看到的,这非常简单.应该跳出来的是关注点的分离(通过检查构造函数):

  • 模型类是独立的,并没有查看或演示的知识.

  • 视图界面是由一个独立的GUI类,这两者都不具有模型或演示的任何知识来实现.

  • 演示类知道这两个模型和视图.

码:

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();
  }
}
Run Code Online (Sandbox Code Playgroud)

以及我们将用于游戏的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;
  }
}
Run Code Online (Sandbox Code Playgroud)

模型:主要是模型的工作是:

  • 为UI提供数据(根据要求)
  • 验证数据(根据要求)
  • 长期存储数据(根据要求)

码:

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;
  }
}
Run Code Online (Sandbox Code Playgroud)

观点:这里的想法是通过尽可能多地剥离应用程序逻辑使其尽可能"愚蠢"(目标是没有).好处:

  • 该应用程序现在是100%JUnit可测试的,因为没有应用程序逻辑与Swing代码混合在一起
  • 您可以在不启动整个应用程序的情况下启动GUI,这样可以更快地进行原型设计

码:

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);
}
Run Code Online (Sandbox Code Playgroud)

和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;
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

立即可能不那么明显的是View界面可以获得多大的效果.对于GUI上的每个Swing组件,您可能希望:

  • 添加/删除组件的侦听器,其中有许多类型(ActionListener,FocusListener,MouseListener等)
  • 获取/设置组件上的数据
  • 设置组件的"可用性"状态(启用,可见,可编辑,可聚焦等)

这可能很快变得笨拙.作为解决方案(在该示例中未示出),为每个字段创建密钥,并且GUI使用其密钥注册每个组件(使用HashMap).然后,而不是视图定义方法,如:

public void addResetActionListener(ActionListener listener);
// and then repeat for every field that needs an ActionListener
Run Code Online (Sandbox Code Playgroud)

你会有一个方法:

public void addActionListener(SomeEnum someField, ActionListener listener);
Run Code Online (Sandbox Code Playgroud)

其中"SomeEnum" enum定义了给定UI上的所有字段.然后,当GUI接收到该调用时,它会查找相应的组件以调用该方法.所有这些繁重的工作都将在一个实现View的抽象超类中完成.


演讲者:职责是:

  • 使用它的起始值初始化视图
  • 通过附加适当的侦听器来响应View上的所有用户交互
  • 必要时更新视图的状态
  • 从View中获取所有数据并传递给Model以进行保存(如有必要)

代码(注意这里没有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.");
  }
}
Run Code Online (Sandbox Code Playgroud)

请记住,MVP体系结构的每个部分都可以/将委派给其他类(隐藏到其他2个部分)以执行其许多任务.Model,View和Presenter类只是代码库层次结构中的上层.

  • 对比度很好!我的[示例](http://stackoverflow.com/a/3072979/230513)实现了[here](http://stackoverflow.com/a/2687871/230513)的关系图; 你跟随[这里](http://en.wikipedia.org/wiki/Model-view-presenter)显示的那个. (2认同)