【问题标题】:Model-View-Presenter passive view: bootstraping - who displays the view initially?Model-View-Presenter 被动视图:引导 - 谁最初显示视图?
【发布时间】:2014-02-26 01:23:57
【问题描述】:

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

我有一个使用 Java Swing 的具体示例。这很简单,但基本上我们有一个SwingCustomersView,它在内部构建了一个JPanel,其中包含一个表格(客户列表)和一个显示当前选定客户年龄的标签。当在表中选择客户时,演示者从模型中检索选定的客户年龄。我认为该示例是 MVP Passive View 的正确实现,但如果我错了,请纠正我。

问题是我们如何引导这些类?例如,如果我们想在 JFrame 中显示SwingCustomersView。如何做到这一点?我想像这样的事情:

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

这是初始接线,但尚未显示任何内容。我们如何实际显示视图? (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
}

但是,在这种情况下,我们不会将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
}

但是,与 (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
}

这将解决构建后不使用它的问题。但是如果不更改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);
    }
}

【问题讨论】:

    标签: java swing mvp


    【解决方案1】:

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

    工作示例:

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

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


    司机:

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

    • Model 类是独立的,不了解 Views 或 Presenters。

    • View 接口由一个独立的 GUI 类实现,它们都不了解模型或演示者。

    • Presenter 类了解模型和视图。

    代码:

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

    以及我们将用于游戏的 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;
      }
    }
    

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

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

    视图:这里的想法是通过尽可能多地剥离应用程序逻辑来使其尽可能“愚蠢”(目标是没有)。优点:

    • 应用程序现在可以 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);
    }
    

    和图形用户界面:

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

    View 界面可以有多大可能不是那么明显。对于 GUI 上的每个 Swing 组件,您可能希望:

    • 为组件添加/移除监听器,有多种类型(ActionListener、FocusListener、MouseListener等)
    • 获取/设置组件上的数据
    • 设置组件的“可用性”状态(启用、可见、可编辑、可聚焦等)

    这很快就会变得笨拙。作为一种解决方案(本示例中未显示),为每个字段创建一个键,并且 GUI 使用它的键注册每个组件(使用 HashMap)。然后,代替 View 定义方法,例如:

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

    你将有一个单一的方法:

    public void addActionListener(SomeEnum someField, ActionListener listener);
    

    其中“SomeEnum”是一个enum,它定义了给定用户界面上的所有字段。然后,当 GUI 接收到该调用时,它会查找适当的组件来调用该方法。所有这些繁重的工作都将在实现 View 的抽象超类中完成。


    演示者:职责是:

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

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

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

    【讨论】:

    • 好对比!我的example 实现了关系图here;你的遵循here 显示的那个。
    • 嘿,有一个异常:/reset() 应该是“selection =pieces[randomInt(0,pieces.length - 1)];” (添加了-1)
    猜你喜欢
    • 1970-01-01
    • 2011-09-09
    • 2013-11-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-04
    • 1970-01-01
    • 2013-09-21
    相关资源
    最近更新 更多