Software Development Kit
to the Delphi-Win32 and Free Pascal compilers
Home > Wiki > MVP

MVP

english (en) | português (br)

The Model-View-Presenter (MVP) framework provided is based on the MVP design pattern.

Reference for background reading can be found at this Taligent paper and this Martin Fowler's article.

Contents

Presenter classes

Any form that presents business objects needs a Presenter. The Presenter informs the MVP framework which business class it represents and provides the mapping between each control and its associated attribute.

There are two mandatory tasks to configure a Presenter:

  • configure its sub-presenters, one for each attribute presented
  • register the Presenter

Configuring sub-presenters

As an example, a Presenter to the following TClient class:

TClient = class(TPressObject)
  Name: String;
  City: Reference(TCity);

TCity = class(TPressObject)
  Name: String;

has the following syntax:

TClientPresenter = class(TPressMVPFormPresenter)
protected
  procedure InitPresenter; override;
end;

...

procedure TClientPresenter.InitPresenter;
begin
  CreateSubPresenter('Name', 'NameEdit');
  CreateSubPresenter('City', 'CityComboBox', 'Name');
end;

That's all. CreateSubPresenter will create a Presenter to manage the presentation of the NameEdit and CityComboBox controls. Its first parameter is the name of the attribute, the second is the name of the form's control and the last one, mandatory only to Structure attributes (compositions, aggregations and collections), is which attribute of the target class you want to present in the form's control.

Here is another example:

TInvoice = class(TPressObject)
  IssueDate: Date;
  Client: Reference(TClient);
  Items: Parts(TInvoiceItem);
  Total: Currency;

TInvoiceItem = class(TPressObject)
  Quantity: Integer;
  Product: Reference(TProduct);
  Total: Currency;

TProduct = class(TPressObject)
  Name: String;

A TInvoicePresenter.InitPresenter method implementation for the TInvoice class would be:

procedure TInvoicePresenter.InitPresenter;
begin
  CreateSubPresenter('IssueDate', 'IssueDateEdit');
  CreateSubPresenter('Client', 'ClientComboBox','Name');
  CreateSubPresenter('Items', 'ItemsStringGrid','Quantity;Product.Name;Total');
  CreateSubPresenter('Total', 'TotalEdit');
end;

The third parameter describes which attributes from the Items objects need to be presented in the grid. You can also define the width (in pixels) and the header caption in this parameter as follows:

'Quantity(10,"Qty");Product.Name(200,"Product Name");Total(80)'

Registering presenters

Presenters need to be registered with the MVP framework. Registration provides the mapping between the business objects, forms and their related Presenters.

An example:

within MVP class unit

initialization
  TClientPresenter.RegisterBO(TClient, [fpExisting]);

within the Form unit

implementation
   uses ClientMVP, PressVCLBroker; // Delphi
   uses ClientMVP, PressLCLBroker; // Lazarus
   ...
initialization
  PressVCLForm(TClientPresenter, TClientEditForm); // Delphi
  PressLCLForm(TClientPresenter, TClientEditForm); // Lazarus

The first parameter of the RegisterBO method indicates the class of the business object, the second is a set of options indicating the purpose for which this Presenter is used. These options are fpNew, fpExisting and fpQuery.

[fpNew] - all presenters registered with this option will match searches when the framework needs to create a form for a new object. For example this option would be used to present a form when adding a new invoice item in a grid control;

[fpExisting] - all presenters registered with this option will match searches when the framework needs to create a form for an existing object. For example this option would be used to present a form for editing an existing invoice item;

[fpQuery] - used to match presenters that show options to populate references attributes, eg:

TJob = class(TPressObject)
  Name: String;
  PrintingColors: References(TColor);

So, when the user needs to include more items in the PrintingColors attribute, presented via a StringGrid control, a fpQuery presenter will be called.

[fpNew, fpExisting] - the same form will be used for both new and existing objects. This is the default parameter setting.

[] - the presenter will never match the framework searches initiated by user actions. Useful when the programmer needs to map a form to a business class and will call the form manually.

Presenter of the main form

A Presenter for the main form is only necessary if the form is associated with a business object or if the framework is used to bind commands to buttons or menu items.

Creating a main Presenter class is as simple as creating the other Presenters:

TMainPresenter = class(TPressMVPMainFormPresenter)
protected
  procedure InitPresenter; override;
end;

...

procedure TMainPresenter.InitPresenter;
begin
  BindCommand(TNewUserCommand, 'NewUserMenuItem');
end;

The Presenter of the main form doesn't need to be registered, however it must be called in the main project file.

Note: If there is no main form Presenter, PressApp.Run should be called instead in the main project file.

See Creating application for further details about configuring a PressObjects application.

Model classes

The model class controls the presentation of the business object and its attributes:

TClientModel = class(TPressMVPObjectModel)
protected
  procedure InitCommands; override;
end;

...

procedure TClientModel.InitCommands;
begin
  inherited;
  AddCommand(TClientReport);
end;

...

initialization
  TClientPresenter.RegisterFormPresenter(
   TClient, TClientForm, [fpNew, fpExisting], TClientModel);

This piece of code (above) is used to include a command in the context menu of the client editing form.

Another example, that creates a Model for an attribute:

TClientDueDateModel = class(TPressMVPDateModel)
protected
  procedure InitCommands; override;
end;

...

procedure TClientDueDateModel.InitCommands;
begin
  AddCommands([TClientDueDateCommand, nil]);
  inherited;
end;

...

procedure TClientPresenter.InitPresenter;
begin
  ...
  CreateSubPresenter('DueDate', 'DueDateEdit', '', TClientDueDateModel);
end;

This example introduces a command as the very first item of the context menu of the DueDateEdit control. The nil parameter includes a menu separator after the command.

View classes

Views are used by the MVP framework to provide the behavior and information transfer for visual components. For example:

TListViewView = class(TPressMVPItemsView)
  // methods and members used to manage a TListView control
  // <<see Source/Core/PressMVPView.pas file for implementation samples>>

...

initialization
  TListViewView.RegisterView;

After a View is registered, the framework will be able to use it whenever a form, containing the control that it manages, is presented.

It is possible to create and register another View that manages a control already managed by the framework. To do this it is necessary to inherit directly from the view class that is already registered to manage the control then override the appropriate methods to modify its behavior as desired. Not using this process will result in an exception being raised because of ambiguity.

As an example, if the following is declared:

TMyOwnEditView = class(TPressMVPWinView)
  ...
end;

and registered, an exception will be raised because

TPressMVPEditView

is already implemented and registered by the framework to manage a TEdit control. There are two Views managing the same control but the framework has no way of knowing which one to use. But if the following is declared:

TMyOwnEditView = class(TPressMVPEditView)

all is well now as the framework checks that the new class has inherited from TPressMVPEditView and uses it instead.

Command classes

Commands are widely used to create new functionalities for forms.

TMarkClientAsGoodClientCommand = class(TPressMVPObjectCommand)
protected
  function GetCaption: string; override;
  function GetShortCut: TShortCut; override;
  procedure InternalExecute; override;
  function InternalIsEnabled: Boolean; override;
end;

...

function TMarkClientAsGoodClientCommand.GetCaption: string;
begin
  // only useful when creating commands for models (context menu)
  Result := 'Mark this client as a good one!';
end;

function TMarkClientAsGoodClientCommand.GetShortCut: TShortCut;
begin
  // only useful when creating commands for models (context menu)
  Result := VK_F9;
end;

procedure TMarkClientAsGoodClientCommand.InternalExecute;
begin
  inherited;
  (Model.Subject as TClient).IsGood := True;
end;

function TMarkClientAsGoodClientCommand.InternalIsEnabled: Boolean;
begin
  Result := not (Model.Subject as TClient).IsGood;
end;

The model property inside a command points to the Model that it belongs to. The Subject property of the Model points to the business object or an attribute, corresponding to the type of the command's owner (a Model).

Commands can be assigned to menu items in main menus, "clickable" controls, or in the Model, presented as context menu items:

procedure TClientPresenter.InitPresenter;
begin
  ...
  BindCommand(TMarkClientAsGoodClientCommand, 'ClientIsGoodSpeedButton');
  BindCommand(TMarkClientAsGoodClientCommand, 'ClientIsGoodMenuItem');
end;

Interactor classes

An Interactor is used to modify a Presenter's behavior:

TKeyboardInteractions = class(TPressMVPinteractor)
protected
  procedure InitInteractor; override;
  procedure Notify(AEvent: TPressEvent); override;
public
  class function Apply(APresenter: TPressMVPPresenter): Boolean; override;
end;

...

class function TKeyboardInteractions.Apply(APresenter: TPressMVPPresenter):
  Boolean;
begin
  // Is this interactor interested in this Presenter?

  // yes, any presenter
  Result := True;

  // or depends, only if the presenter manages a CheckBox view
  Result := APresenter.View is TPressMVPCheckBoxView;
end;

procedure TKeyboardInteractions.InitInteractor;
begin
  // what does the interactor need to do just after it is created?

  // let's listen for keyboard events from the view
  Notifier.AddNotificationItem(Owner.View, [TPressMVPViewKeyDownEvent]);
end;

procedure TKeyboardInteractions.Notify(AEvent: TPressEvent);
begin
  // what does the interactor need to do when the notification is called?
  // Note: multiple events in the second parameter of the
  // AddNotificationItem means multiple AEvent classes

  if AEvent is TPressMVPViewKeyDownEvent then
  begin
    TPressMVPViewKeyDownEvent(AEvent).Key;    // has the key code
    TPressMVPViewKeyDownEvent(AEvent).Shift;  // has the shift state
  end;
  Owner;                // points to the presenter
  Owner.Model;          // points to the model
  Owner.Model.Subject;  // points to the business object
  Owner.View;           // points to the view
end;

...

initialization
  TKeyboardInteractions.RegisterInteractor;