Model View Presenter

Model View Presenter

Separates the behavior of a presentation from the view while allowing the view to receive user events.

Even when you practise Presentation/Domain separation Rich client UIs have a lot to do. They need to build the various controls on the screen, handle user events, and decide how to alter the controls in response to these events. Doing this all in one class makes this UI class complex and makes it difficult to test without driving the UI class directly.

Model View Presenter separates the behavior of the presentation out into a separate presenter class. Any user events are forwarded to the presenter and the presenter manipulates the state of the view. As well as separating the responsiblities, this also allows the behavior to be tested without the UI, and for different UI controls to be used with the same basic behavior.

How it Works

The heart of Model View Presenter is to pull all the behavior of the presentation out of view and place it in a separate presenter class. The resulting view will by very dumb - little more than a holder for the gui controls themselves. In this way the separation is very much the same as the classic separation of Model View Controller.

Classic MVC doesn't work well with modern rich client tools because they design things so that the view handles all the user events such as mouse and keyboard clicks. In Model View Presenter the view continues to handle these, but then immediately delegates these to the presenter. The presenter then decides what to do with the event, communicating with the domain and the data in the view's controls.

Figure 3

Figure 3: A simple set of classes handling the classical check box.

Figure 4

Figure 4: Response to clicking the classical check box.

Looking at the album window example, we see classes along the lines of Figure 3 and Figure 4.

This simple form of Model View Presenter fulfills the need to separate the behavior out of the view. However in this style the view still needs to be there for testing and the presenter's knowledge of the view is too great to allow for easy substitution of the view to provide a different flavor of UI because the presenter knows about the actual UI controls in the view.

This can be fixed by providing a separate interface which is control independent Figure 1 and Figure 2. This interface is more work to support, but breaks the the dependency the presenter has on the actual UI controls used. With this scheme it's easy to add a Service Stub to test the presenter without any UI present.

Figure 1

Figure 1: Using an interface to make the presenter independent of the actual UI controls

Figure 2

Figure 2: Respose to clicking the classical checkbox with the view interface.

The scheme I've just shown maximizes the humility of the view by having the presenter push all the data into the view. Another style of Model View Presenter allows the view to update itself from the model directly. In this case events are delegated from the view to the presenter, and the presenter updates the model as before. However instead of having the presenter update the view, the view draws its information directly from the model. This clearly simplifies the presenter and removes the need for the presenter to be dependent upon the view, or need an interface for manipulating the view. However the view now needs to have the logic to update itself fromt he model. If there's a close fit between the view and the model this may be reasonable, but if there's a significant difference then the view is in danger of losing its humility. A way to prevent this is to create an adapter that performs the translation from the base model class to that needs of the view.

[TBD: Need an example for this MVP configuration - should this be a separate pattern?]

When to use it

Model View Presenter is an alternative to Autonomous View and Presentation Model. Like Presentation Model it pulls out presentation logic from the view that holds the GUI controls. This allows support for multiple views with different control layouts, and also supports testing without the view. In addition separating controller behavior from the view allows a Model View Presenter to concentrate on just the behavior.

Compared to Presentation Model the presenter hold no state, as such it avoids having to synchronize multiple copies of the gui control state. Doing this means that the view needs to provide hooks to allow the presenter to manipulate this. In testing this implies that the view needs to be stubbed.

Pulling the behavior out of the view does add an extra class, and it's reasonable to question how much value this separtion provides if you aren't concerned with alternative views and are happy with view based testing approaches. This is particularly relevant since full independence of view can only be done with adding an extra interface and implementing that interface in the view itself.

Most of the time I see Model View Presenter it's under the name of Model-View-Controller and the presenter is called the controller. This is quite reasonable as Model View Presenter is really just a minor variation on MVC. The only difference is that the view is handling the gui events and delegating them to the controller rather than the controller getting the events directly.

[TBD: See if I can hunt down some other descriptions of the Smalltalk Application Model - perhaps in DP Smalltalk Companion.]

Example: Album Window Example (Java Swing)

Here's the running example done in Swing in the Model View Presenter style. For this case I'm following the approach of using a view interface that's independent of the actual controls used in the window itself as in Figure 1

Figure 5

Figure 5: A simple album information window

The code for model is just a simple data object.

class Album...
    private String artist, title,  composer;
    private boolean isClassical = false;
    public String getArtist() {
        return artist;
    }
    public void setArtist(String artist) {
        this.artist = artist;
    }
    public String getTitle() {
        return title;
    }
    public void setTitle(String title) {
        this.title = title;
    }
    public String getComposer() {
        return composer;
    }
    public void setComposer(String composer) {
        this.composer = composer;
    }
    public boolean isClassical() {
        return isClassical;
    }
    public void setClassical(boolean classical) {
        isClassical = classical;
    }

You start the application by creating a presenter

class AlbumPresenter...
    public AlbumPresenter (List albums, AlbumView window) {
        this.albums = albums;
        this.view = window;
        initView();
        view.packAndShow();
    }

My real view implementation is a wrapper around a JFrame.

class AlbumWindow implements AlbumView...
    public AlbumWindow() {
        buildWindow();
    }
    public void packAndShow() {
        window.pack();
        window.setVisible(true);
    }

The buildWindow method does all the assembly of the window's widgetry. I'll not go into the details since it's just regular swing assembly code. The only wrinkle is that I store all the widgets in fields inside AlbumWindow as well as placing them in the composite structure of the main JFrame. This allows me to get at the individual widgets easily without navigating through the swing composite structure. It also means that if I alter the structure, I don't have to alter the view's code.

The presenter is responsible for putting all the data into the window. It does this by pulling data out of the domain class and pushing the data into the window via the view interface.

class AlbumPresenter...
    void loadViewFromModel() {
        if (isListening) {
            isListening = false;
            refreshAlbumList();
            view.setTitle(selectedAlbum().getTitle());
            updateWindowTitle();
            view.setArtist(selectedAlbum().getArtist());
            view.setClassical(selectedAlbum().isClassical());
            if (selectedAlbum().isClassical())
                view.setComposer(selectedAlbum().getComposer());
            else
                view.setComposer("");
            view.setComposerEnabled(selectedAlbum().isClassical());
            enableApplyAndCancel(false);
            isListening = true;
        }
    }

    private void refreshAlbumList() {
        final int currentAlbum = view.getSelectedAlbum();
        view.setAlbums(albumTitles());
        view.setSelectedAlbum(currentAlbum);
    }
    private void updateWindowTitle() {
        view.setWindowTitle("Album: " + view.getTitle());
    }
    private void enableApplyAndCancel(boolean arg) {
        view.setApplyEnabled(arg);
        view.setCancelEnabled(arg);
    }

This method updates all the items of view. Some of these updates cause events to fire which would cause a recursive trigerring of the update method, so I use a guard around the update method to prevent the recursion.

The methods on the view allow access to the underlying controls via the fields.

class AlbumWindow...
    public void setClassical(boolean arg) {
        classicalCheckBox.setSelected(arg);
    }
    public boolean isClassical() {
        return classicalCheckBox.isSelected();
    }

I'm just showing the check box here, but principle is the same for the others.

When the classical check box is clicked the event is received by the window. Listeners for the event do a simple delegation to the presenter. The listeners are inserted when the window and presenter are constructed.

class AlbumWindow...
    public void addClassicalCheckBoxListener(ActionListener listener) {
        classicalCheckBox.addActionListener(listener);
    }
class AlbumPresenter...
    private void addClassicalCheckListener() {
        view.addClassicalCheckBoxListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                dataFieldUpdated();
            }
        });
    }
class AlbumPresenter...
    void dataFieldUpdated() {
        enableApplyAndCancel(true);
        view.setComposerEnabled(view.isClassical());
        updateWindowTitle();
    }

In this case I've arranged so that the presenter actually adds the listeners to the window. The window could add the listeners itself, but in order to that it would require knowledge of the presenter and a failure in the listeners would not be detected by tests that stubbed the view. It's not all roses however, because getting the presenter to do the adding means it has a dependency on the swing listener code - in this case I considered that the lesser evil.

The listener triggers the method to update the view, just the same as what happens on initial load. For simplicity I'm using coarse grained update so changing any field causes an update for all derived information.

I put a similar listener in place to handle the changes in the list selection.

class AlbumPresenter...
    private void setAlbumListListener() {
        view.setAlbumListListener(new ListSelectionListener() {
            public void valueChanged(ListSelectionEvent e) {
                loadViewFromModel();
                //TODO need to pop up dialog to save or lose changes if has changed
            }
        });
    }

In this case the listener does a complete update of all the data in the fields, just like it does on the initial load. [TBD: Consider doing the confirm dialog here]

You save data to the model when the user hits the apply button.

class AlbumPresenter...
    private void addApplyListener() {
        view.addApplyListener(new ActionListener(){
            public void actionPerformed(ActionEvent e) {
                updateModel();
            }
        });
    }
class AlbumPresenter...
    void updateModel() {
        selectedAlbum().setTitle(view.getTitle());
        selectedAlbum().setArtist(view.getArtist());
        selectedAlbum().setClassical(view.isClassical());
        if (view.isClassical())
            selectedAlbum().setComposer(view.getComposer());
        else
            selectedAlbum().setComposer(null);
        enableApplyAndCancel(false);
        loadViewFromModel();
    }

To test this without using the gui controls you need to provide an alternative stub implementation for the view. There's lots of ways to do this, including various libraries for mock objects. In this case, however, I just implemented a simple class with fields and getters and setters for the view's interface.

class StubTester...
    private AlbumPresenter presenter;
    private AlbumViewStub view;
    protected void setUp() throws Exception {
        view = new AlbumViewStub();
        presenter = new AlbumPresenter(Mother.albums(), view);
    }
     public void testCheckClassicalBoxEnablesComposerField() {
        selectAlbumNumber(4);
        view.setClassical(true);
        view.getClassicalCheckBoxListener().actionPerformed(null);
        assertTrue(view.isComposerEnabled());
    }
    private void selectAlbumNumber(int arg) {
        view.setSelectedAlbum(arg);
        view.getAlbumListListener().valueChanged(null);
    }

To trigger the presenter's behavior you can either invoke presenter methods directly, or you can do what I've done here and go via the listeners which are stored in the stub view.

Example: Album Window Example (C#)

(by Jay Fields)

This time I'll implement the running example with C# in the Model View Presenter style. In this example the presenter will observe the view by subscribing to events instead of adding listeners directly to the view. The view will raise events when a child control's state is changed and the presenter will respond by updating the view's state. Implementing the view with events decreases the coupling between the view and the presenter.

Figure 6

Figure 6: Another simple album information window

The code for the model is a simple c# data object.

class Album...
    private string artist, title, composer;
    private bool isClassical;

    public string Title
    {
      get { return title; }
      set { title = value; }
    }

    public bool IsClassical
    {
      get { return isClassical; }
      set { isClassical = value; }
    }

    public string Artist
    {
      get { return artist; } 
      set { artist = value; }
    }

    public string Composer
    {
      get { return composer; }
      set { composer = value; }
    }

The presenter takes the view and model as constructor arguments. This allows for easy stubbing while testing.

class AlbumPresenter...
    public AlbumPresenter(IAlbumView view, IAlbum[] albums)
    {
      this.view = view;
      this.albums = albums;
      subscribeToViewEvents();
      loadViewFromModel();
    }

The view implementation inherits from a windows form and implements IAlbumView. It's constructor contains no behavior and is only responsible for calling the Windows Form Designer generated code.

class AlbumForm : Form, IAlbumView...
    public AlbumForm()
    {
      // Required for Windows Form Designer support
      InitializeComponent();
    }

Again, the presenter is responsible for putting all the data into the window. The guard flag is used again to prevent recursion.

class AlbumPresenter...
    private void loadViewFromModel() 
    {
      if (isListening) 
      {
        isListening = false;
        refreshAlbumList();
        view.Title = selectedAlbum().Title;
        updateWindowTitle();
        view.Artist = selectedAlbum().Artist;
        view.IsClassical = selectedAlbum().IsClassical;
        view.Composer = selectedAlbum().IsClassical?selectedAlbum().Composer:string.Empty;
        view.ComposerEnabled = selectedAlbum().IsClassical;
        enableApplyAndCancel(false);
        isListening = true;
      }
    }

    private void refreshAlbumList() 
    {
      int currentAlbum = view.AlbumIndex!=-1?view.AlbumIndex:0;
      view.Albums = createAlbumStringArray();
      view.AlbumIndex = currentAlbum;
    }

    private void updateWindowTitle() 
    {
      view.WindowTitle = string.Format("Album: {0}",view.Title);
    }

    private void enableApplyAndCancel(bool arg) 
    {
      view.ApplyEnabled = arg;
      view.CancelEnabled = arg;
    }

The view's properties provide access to the child controls. This time I'll use the composer text box as the example; however, each child control property that needs to be set by the presenter will need to be exposed as a property of the view.

class AlbumForm...
    public string Composer
    {
      get { return composerTextBox.Text; }
      set { composerTextBox.Text = value; }
    }

    public bool ComposerEnabled
    {
      set { composerTextBox.Enabled = value; }
    }

I chose the composer this time to show how the Text and Enabled properties of the ComposerTextBox should be exposed as properties of the view. Notice also that the ComposerEnabled property contains only a setter. Limiting the view's interface communicates what state is unimportant to the presenter.

In the constructor of the presenter a call to subscribeToViewEvents occurs. The subscribeToViewEvents method is used to wire presenter methods to the view's events.

class AlbumPresenter...
    private void subscribeToViewEvents()
    {
      view.Reload+=new UserAction(loadViewFromModel);
      view.Updated+=new UserAction(dataFieldUpdated);
      view.Apply+=new UserAction(saveToModel);
    }

The view wires the onUpdated method of AlbumForm to the ComposerTextBox.TextChanged, TitleTextBox.TextChanged, ArtistTextBox.TextChanged, and ClassicalCheckBox.CheckedChanged events. The onUpdated method is responsible for raising the Updated event of the view.

class AlbumForm...
    private void onUpdated(object sender, EventArgs e)
    {
      if (Updated!=null) Updated();
    }

Because of the subscription that occurs in subscribeToViewEvents, dataFieldUpdated is invoked when the Updated event is raised from the view. The dataFieldUpdated method is a coarse grained update.

class AlbumPresenter...
    private void dataFieldUpdated()
    {
      enableApplyAndCancel(true);
      view.ComposerEnabled = view.IsClassical;
      updateWindowTitle();
    }

Changing the selected album or clicking cancel causes the Reload event to be raised by the view. The presenter responds to a Reload event by doing a complete update of the view.

class AlbumForm...
    private void onReload(object sender, EventArgs e)
    {
      if (Reload!=null) Reload();
    }

The presenter responds to a Reload event by doing a complete update of the view. The update is executed because subscribeToViewEvents wires the loadViewFromModel method to the view's Reload event.

Clicking the apply button raises the Apply event of the view.

class AlbumForm...
    private void onApply(object sender, EventArgs e)
    {
      if (Apply!=null) Apply();
    }

The view's Apply event is handled by the presenter's saveToModel method.

class AlbumPresenter...
    private void saveToModel()
    {
      selectedAlbum().Title = view.Title;
      selectedAlbum().Artist = view.Artist;
      selectedAlbum().IsClassical = view.IsClassical;
      selectedAlbum().Composer = view.IsClassical?view.Composer:string.Empty;
      enableApplyAndCancel(false);
      loadViewFromModel();
    }

Since the presenter depends on a view, a view is required for testing. A mock view can be used; however, in practice a stub view works better when the presenter is observing events. Using a stub allows you to create helper methods that raise events.

class StubAlbumView...
    public void RaiseUpdated()
    {
      Updated();
    }
User interaction with the view can be simulated by changing the state of the stub and raising events. When an event is raised the presenter will update the state of the stub. The stub's state can then be used to verify the behavior of the presenter.

 

class AlbumPresenterTest...
    [Test]
    public void UpdateCausesComposerEnabledToBeRefreshed()
    {
      StubAlbumView view = new StubAlbumView();
      new AlbumPresenter(view, new IAlbum[] {createAlbum()} );
      Assert.AreEqual(view.ComposerEnabled,true);
      view.IsClassical = false;
      view.RaiseUpdated();
      Assert.AreEqual(view.ComposerEnabled,false);
    }

Further Reading

Most modern views of Model View Presenter seem to originate from the descriptions in Dolphin Smalltalk. The basic term and idea seemed to originate from Taligent, which inevitably is rather more complex.

Significant Revisions

19 Jul 04: First public release.

13 May 04: First TW version.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值