java - Model-View-Presenter被动 View : bootstraping - who displays the view initially?

标签 java swing mvp

在“被动 View 模型 View 呈现器”模式中,谁负责显示 View ?我已经找到了其他MVP版本的相关答案,但是它们似乎不适用于被动 View 版本。

我有一个使用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);
}

这是初始接线,但什么也没显示。我们如何实际显示 View ?显示 View 是(1)launcher(),(2)SwingCustomersView或(3)CustomersPresenter的责任吗?不幸的是,我不认为这些都是很好的,从下面我的想法中可以看出。也许还有另一种方式?

(1.a):启动器

使SwingCustomersView扩展JFrame并将其内部JPanel添加到其自身的内容 Pane 中。然后我们可以这样做:
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在应该向其添加内部JPanel的构造函数中采用Container:
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)都可以在演示者挂机之前显示 View ,我想这在某些情况下可能会导致奇怪的结果。

(3):客户演示者

使CustomersPresenter负责显示 View 。然后我们可以这样做:
void launcher() {
    CustomersModel model = new CustomersModel();
    SwingCustomersView view = new SwingCustomersView();
    CustomersPresenter presenter = new CustomersPresenter(view, model);
    presenter.show() // Displays the view
}

这样可以解决构造后不将其用于任何东西的问题。但是我不更改CustomersView接口(interface)或使CustomersPresenter过于依赖底层GUI实现,就看不到该怎么做。此外,显示 View 听起来不像演示逻辑,因此似乎不属于演示者。

例子
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);
    }
}

最佳答案

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

工作示例:

我偶然发现了使用MVC架构的简单Swing游戏的this example。由于我是使用MVP而不是MVC编写Swing应用程序的,因此如果这个例子是MVC的真实和纯粹的例子,我不能授权。在我看来,这还可以,并且作者trashgod在使用Swing的SO方面已经证明了自己,所以我会认为它是合理的。

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

驱动程序:

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

  • 模型类是独立的,不了解 View 或演示者。
  • View 接口(interface)是由独立的GUI类实现的,它们都不了解模型或演示者。
  • Presenter 类同时了解模型和 View 。

  • 代码:
    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;
      }
    }
    

    View :此处的想法是通过尽可能多地剥离应用程序逻辑来使它尽可能“笨拙”(目标是不使用任何逻辑)。好处:
  • 该应用程序现在可以100%进行JUnit的测试,因为Swing代码
  • 中未混入任何应用程序逻辑
  • 您可以在不启动整个应用程序的情况下启动GUI,这使得原型(prototype)制作的速度更快

  • 代码:
    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);
    }
    

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

    立即可能看不到的是View界面有多大。对于GUI上的每个Swing组件,您可能需要:
  • 添加/删除组件的监听器,该监听器的类型很多(ActionListener,FocusListener,MouseListener等)。
  • 获取/设置组件
  • 上的数据
  • 设置组件的“可用性”状态(启用,可见,可编辑,可聚焦等)。

  • 这会很快变得笨拙。作为解决方案(此示例中未显示),将为每个字段创建一个 key ,并且GUI用其 key 注册每个组件(使用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,它定义了给定UI上的所有字段。然后,当GUI收到该调用时,它会查找适当的组件以对该方法进行调用。所有这些繁重的工作都将在实现View的抽象父类(super class)中完成。

    演示者:的职责是:
  • 使用其初始值初始化 View
  • 通过附加适当的监听器
  • 来响应View上的所有用户交互
  • 必要时更新View的状态
  • 从 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类只是代码库层次结构中的高级部分。

    关于java - Model-View-Presenter被动 View : bootstraping - who displays the view initially?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/22029808/

    相关文章:

    java - 使用 Mockito 对具有被动 View 的演示者进行单元测试

    java - 以编程方式向 TextView 添加边框

    java - 如何通过流计算值

    java - paintComponent() 不会绘制

    iOS - 在 MVP 的 Presenter 中单元测试异步私有(private)函数

    android - MVP RxJava2 - 管理网络错误

    java - 如何限制 NestedScrollView 内 RecyclerView 的高度

    java - 代码在使用 && 循环时不起作用

    java - 如何使jtable中的一行不可选择?

    java - JTable 工作但 JTable 子类不会