The OpenEdge Reference Architecture (OERA) defines a Presentation Layer and lays out a design overview for the layer on PSDN. The OERA recommends the use of the Model-View-Presenter (MVP) pattern for the design of a Presentation Layer's code. The MVP pattern is a user interface design pattern engineered to improve the separation of concerns in presentation logic; references to detailed descriptions of the pattern are here.
This document describes a design for and reference implementation of the Presentation Layer using the Supervising Presenter variant of the Model-View-Presenter pattern. The Supervising Presenter pattern is described below.
The MVP pattern has evolved into two variants : the Supervising Presenter (sometimes Supervising Controller) and the Passive View patterns. Some consider these variants to be separate, new, patterns rather than variants; this document refers to them as separate patterns.
A Supervising Presenter has two primary responsibilities: (user) input response and partial synchronization of data between the View and Model.
For input response the controller operates in the presenter style, which means that the user gestures are handled initially by the screen widgets; however all they do in response is to hand these events off to the Presenter, which handles all further logic.
The Presenter defers as much of View/Model synchronization as reasonable to the View, which typically uses some form of Data Binding to populate much of the information for its fields. Where more complex interactions are required than Data Binding alone can handle, the Presenter steps in.
1: MVP - Supervising Presenter From this document
As with the Supervising Presenter pattern, the UI is split between a View that handles display and a Presenter that responds to user gestures. The significant change with Passive View is that the View is made completely passive and is no longer responsible for updating itself from the Model. As a result, all of the View logic is in the Presenter. As a result, there are no dependencies in either direction between the View and the Model.
2: MVP - Passive View From this document
While the decision of design patterns to use should be technology-agnostic as far as possible, the ABL's heritage of providing strong or tight data binding constructs in the language - going back to the DISPLAY and UPDATE statements and continued in the OpenEdge ProBindingSource object - mean that the Supervising Presenter is a pattern well-suited to ABL applications' presentation layers.
Let's take a moment to define some of the pieces that make up the pattern.
Basically, an application is a screen or a collection of screens with a common purpose. An application can contain other applications, and can invoke them. An application is the set of a Presenter, its View and Models taken together. An application can invoke other applications. The term "application" alone is somewhat confusing, since it is often used to describe the entire deployed product. In this document we'll use the term Presenter Application instead, to coin a phrase. In this document, "application" alone will refer to the whole session/product.
The Model contains business logic and data, with the Model pertaining to a particular portion of the business domain. In OERA terms, the Model can be thought of as a Client-Side Business Component (this concept is explored on PSDN and in Exchange presentations).
A Presenter Application can have more than one Model. The Model provides data to a View, via data binding, and retrieves data and submits updates to Business Components through a communication mechanism such as a Service Adapter.
The Model executes any client-side business logic. This may duplicate some logic that is executed on the server; this is necessary since we don't want to go to the server for simple, localized validation (such as field population or format masks), and since business components accept requests from all services, not just the UI, this logic must happen on the server too.
The Model communicates with the Common Infrastructure, for example interacting with an authorization service to determine whether the current user has access to certain functions in the Model (fetching or saving data). The Common Infrastructure also contains the Service Adapter that the Model for its data requests.
The View is a visual representation of one or more models and is made up of the screens, forms, controls, widgets and/or windows within an application. It handles user input (by means of capturing events representing actual actions being performed) and displays the results of those actions back to the user. The View should be as thin as possible - it should only take user input and pass it on to its Presenter. Any logic that's not specifically related to the UI technology must be handled by the View's Presenter. So when the Presenter determines that the background colour of a field should be green, the View knows that it needs to set the BackColor property on the TextEdit control, or the BGCOLOR attribute on the fill-in or whatever. The View has no idea what the rationale is for the background to be green; it merely knows how to make it so.
The View has direct knowledge of its Presenter and also of the UI elements of which it is composed. For example, if the View uses the GUI for .NET technology, the View is composed of the Form and the Controls and UserControls which are on it. The View may also have indirect knowledge of any Models via data binding (in GUI for .NET terms this would be done using a ProBindingSource; there's no direct analog in the ABL GUI). Any interaction that the View has with a Model will be done via the Presenter. Note that the View is not required to use a ProBindingSource for data binding: the Presenter's API allows the View to (effectively) follow the Passive View pattern.
Any events fired in the View as a result of user action are handed off to the Presenter for processing. This includes events that may be data-related, such as the OffEnd or NewBatch event. While the Model provides data to the View, the Presenter will receive events that filter or sort the data from the View, and communicate these to the Model.
The View needs to know a Model's logical (or abstract) name in order to do its data binding, given that a View may represent data from more than one Model. The View will also know field (column) names in the Model, which it uses to perform its binding. The field name may also be abstracted, but will usually correspond directly with the Model's column names.
To reiterate, other than data binding (if used), the View only communicates with the Presenter. It does not talk to the Common Infrastructure directly, but rather relies on the Presenter to do such communication.
The Presenter contains presentation and application logic. It creates the View and any Models it needs, and processes events received from the View. The Presenter is the focal point of the Presenter Application - it deals with the interaction between components, performs the UI logic and creates and destroys Views, Models and other Presenters. The Presenter also interact with any Common Infrastructure elements. Everything starts and ends with the Presenter (and not the View, which is pretty dumb).
A (single) Presenter must be able to deal with Views built on multiple different UI technologies, such as .NET, ABL GUI, ChUI etc, without having any knowledge of those technologies. For instance, in a .NET GUI View, event handlers typically have a System.Object and System.EventArgs objects passed in as parameters. These objects are .NET GUI specific and have no meaning outside of that particular UI technology. Since we define a Presenter as being UI-technology agnostic, methods that processes events should not accept these objects as parameters, since they are not available in an ABL CHUI or ABL GUI environment. Any communication between the Views and Presenters must be at the highest common denominator (HCD): the most complex data-types or objects supported by both the Views' UI technologies and the Presenter. Arguments passed from a View to a Presenter don't necessarily need to be ABL primitive data types such as CHARACTER, DECIMAL or INTEGER; they could be a set of classes, as long as all Views can use them. And since all ABL code can, this is an example of where the ABL's ability to invoke OO code from procedural (and vice versa) really shines.
A Presenter can manage zero or more Models. The Presenter is the component with the knowledge of the Models' logical and physical (class) names. This separation of names is desirable because it allows the View (primarily) to reference a Model by name, and allows the implementation of that Model to change without requiring changes to the View.
The Presenter may simply act as a pass-through from the View to the Model, or it may apply some rules to the input before passing the data on to the Model.
The Presenter is always the starting point for an application. As part of its initialization it will create and initialize the View and Model(s). This may be a simple NEW() or may involve a call to the Common Infrastructure into a caching API. Generally speaking, we will use the Factory pattern to create Views and Models from the Presenter (see later).
The relationships between components may vary between instances (ie. running versions) and types (the class).
A Presenter can provide support to different Views representing different UI technologies. For example, so a Customer Presenter will support a Customer View for the GUI for .NET, for the ABL GUI and for the Web, as well as TTY. These Views may be implemented as CustomerForm.cls, customerw.w and customerframe.p respectively.
A Presenter may have zero, one or many Models. In some cases a Presenter only provides behavior - for instance to support a navigation panel - and so will not need a Model. In the opposite case, a CustomerMaintenancePresenter may get data from a MenuModel (which would provide menu and toolbar data) and also from a CustomerModel (which would provide the data being updated).
At runtime a Presenter instance supports only a single View instance, and many Model instances.
A Model can be used by many Presenters. A MenuModel would likely be used by many Presenters.
A Model instance can be used by many Presenter instances: effectively this turns the Model into a cache or a shared Model. This would be useful for the MenuModel example from above, since we might not want the MenuModel to fetch the menu data more than once. The Model's code would likely determine whether it was shared or otherwise, although this is an implementation detail.
A Presenter can have any number of child Presenters, which in turn know about their parent Presenter. These child Presenters can be added at design time or at runtime; the only difference between them is when they are added. Once a Presenter is running, it's running.
An example of a child Presenter that is added at runtime would be in a maintenance window's Presenter. The maintenance window has a navigation panel, which is its own Presenter Application. The maintenance window Presenter knows about and starts the navigation panel's Presenter. Note that the Presenter does not know how the navigation panel is composed or contained; it only knows that the navigation panel is a child. Taken to the extreme, one could see an application as a Presenter Application containing many nested Child Presenters, each with their own (nested) Child Presenters.
A View can contain any number of other Views. The nature of these child Views depends on the UI technology used, but in the GUI for .NET for example, a contained View could be an Inherited Control or a User Control. The parent View would be a Form, and it is responsible for determining where the Views are laid out, and how they are sized etc.
These contained Views will have a corresponding child Presenter; these two are typically connected at runtime.
This allows us to compose applications that have loosely-coupled View and Presenter components.
A View can be managed by multiple Presenters.
This diagram shows the main MVP interfaces including specializations and dedicated interfaces used as assemblies or composites.
(See attached mvp.png)
Creating a presenter application
So now we have described the various Foundation Classes, let's see how we can use them.
As noted above, the Presenter is the focus of our application. It contains generalized behavior for some functionality - for example Customer Maintenance. The Presenter deals with the communication between the View and the Model, as well as any child Presenters it may have (it has no knowledge of those children's Models etc).
A Presenter can inherit from the OpenEdge.PresentationLayer.Presenter.Presenter or OpenEdge.PresentationLayer.Presenter.DataboundPresenter class, depending on whether the Presenter needs to deal with data (a Model, in other words). Most Presenters will inherit from DataboundPresenter, but there are certain cases where the Presenter only contains behavior (and no data); the NavigationPanelPresenter is an example of this. Typically, application infrastructure elements would be more likely candidates for non-databound components.
A Presenter receives View, Model and child Presenter information via injection (using InjectABLin our case), in the form of an instance or instances. The component View, Model(s) and Presenter(s) a particular Presenter requires are determined by the signature of the Presenter's constructor, in the form of interfaces. Currently, the injection mapping determines which actual Views, Models and child Presenters are used. The Model in particular has no idea of which Presenter(s) is using it. If a Model needs to communicate with a Presenter, it will publish events to do so. A View has a Presenter (of type IPresenter) property which is used to invoke UI logic and the Presenter sets that in the View, rather than the View deciding which Presenter to use. Since the Presenter has no idea of how the UI is composed, the Views in certain cases need to know who their Presenters are.
A Model consists - at its simplest - of a collection of (named) queries on some set of data. This data can be contained within any ABL-accessible construct: a ProDataSet, an ABL temp-table, an ADM2 SmartDataObject, etc. The queries are represented by individual Queryobjects.
The top-level Model class (together with its associated IModel interface) provides access to the data accessible via these queries: most of the methods take a query name and perform some operation on that query (navigation, updates etc). The generic Model class does not specify the data storage; it only provides the data manipulation/navigation interface (in the API sense).
There are certain specialized Model classes provided. Some of these are more fully-implemented than others. The most important is:
Certain Models - those that provide data to menus spring to mind - are candidates for sharing or caching. An application will typically only want or need to retrieve data once per session; also the data would likely be fairly stable in terms of additions and/or updates.
The InjectABL Container allows any components' lifecycles' scopes to be managed, so that a Model can potentially be a Singleton or scoped to another object's lifecycle (a manager, for instance).
By default, a Model is updatable. By implementing the IModelReadOnly interface, a model can be marked as read-only. This applies to all tables or buffers (in the case of a DatasetModel) that the Model knows about.
The Model stores table info like page size and where the previous and next page starts in ITableInfo instances. The ITableInfo inherits general table information from ITableContext and holds other table info for the Model on the client. The TableInfo is only publicly exposed through methods in the Model.
The ITableInfo only defines one property in addition to the ones inherited from ITableContext
Update actions on a Model can either be local (to the Model, that is), or remote. Local actions are performed on the contents of the Model only - so typically on a ProDataSet, temp-table or some other non-permanent data store. Such actions include Save, Add and Delete.
Remote actions include the Commit action, which writes the contents of the Model to its permanent store which may be an XML document, a WebService or an OpenEdge database, or a Service Interface. Note that remote in this context means remote to the Model's data store, and not necessarily a remote session.
Views are broadly-speaking of two types: container (IContainerView) and contained Views (IContainedView). All Views also implement IView.
The reference implementation also provides some super classes which extend the built-in Progress.Windows classes.OpenEdge.PresentationLayer.View.GuiForDotNet.MVPFormOpenEdge.PresentationLayer.View.GuiForDotNet.MVPUserControl
There are also some ABL wrappers for ABL GUI Views.
The actual UI events are captured by the View. The exact mechanism will depend on the UI technology (ABL and .NET GUI will have different physical events for selecting a button, for instance). The View passes the event to its Presenter who will do the work, and apply the UI logic required.
The examples below are taken from the NavigationPanelPresenter and GuiForDotNet.NavigationPanel and AblGui.NavigationPanel and associated navpanel.w (all Views). In both cases, the actual UI determines how the event is handled. In OpenEdge.PresentationLayer.View.GuiForDotNet.NavigationPanel.cls, we have the following event handler subscription and event handling code.
method override public void SubscribeEvents():
method private void FirstButtonClick(sender as System.Object,
e as System.EventArgs ):
method protected void SelectAction (poAction as EnumMember):
define variable oArgs as ActionEventArgs no-undo.
oArgs = new ActionEventArgs(string(piAction), ActionTypeEnum:Event).
oArgs:SetArgValue('Action', string(piAction), DataTypeEnum:Integer).
In the ABL GUI example, OpenEdge/PresentationLayer/View/AblGui/navpanel.w does the event subscription and handling all as one piece of code below.
ON CHOOSE OF Btn-First IN FRAME F-Main
/* First */
OR CHOOSE OF Btn-Prev or
CHOOSE OF Btn-Next or
CHOOSE OF Btn-Last
define variable iButton as integer no-undo.
define variable oArgs as ActionEventArgs no-undo.
when 'First' then iButton = NavigationActionEnum:First.
when 'Prev' then iButton = NavigationActionEnum:Prev.
when 'Next' then iButton = NavigationActionEnum:Next.
when 'Last' then iButton = NavigationActionEnum:Last.
oArgs = new ActionEventArgs(string(iButton), ActionTypeEnum:Event).
oArgs:SetArgValue('Action', string(iButton), DataTypeEnum:Integer).
cast(goPresenter, ISelectAction):SelectAction (oArgs).
In both Views, the event handler does nothing more than make a request to the Presenter, passing in an EventArgs object. This is a purely ABL object (even though it may be named the same as the .NET EventArgs). ABL can be used in both procedural and class-based programming.
The Presenter's (OpenEdge.PresentationLayer.Presenter.NavigationPanelPresenter.cls) SelectAction() method appears below. This method performs various actions depending on the action selected (ie the button pressed or the menu item selected). Note that the code in the Presenter is completely agnostic to the UI technology used, and so only knows about the action performed, and not how it was performed.
Also note that while this method doesn't have a return type, there's no reason why the method could not return a type, or an exception, or make a callback to the View The Presenter knows who its View is, or fire an event. The messaging from Presenter to View is completely flexible, and left to the implementer.
method public void SelectAction (poArgs as ActionEventArgs):
define variable iAction as integer no-undo.
poArgs:GetArgValue('Action', output iAction).
when NavigationActionEnum:First then
when NavigationActionEnum:Prev then
when NavigationActionEnum:Next then
when NavigationActionEnum:Last then
Presenter applications are invoked via the InjectABLkernel's Get() method. This code will be contained in a .p, which will be run via the command-line, a (Windows) shortcut or an Architect launch config. This is because the AVM cannot run a class from the command-line (ie prowin32.exe).
There is, however, a case to be made for launching an individual screen in "standalone" mode while developing the screen.
In order to facilitate the correct running of the screen within the MVP environment, the developer needs to call the InitializeDesignTimePresenter method in the View's default constructor, passing in the value of a Presenter that is capable of managing the View. This method must be explicitly called in each View that requires it. Also, the calling of the method must be the last action in the constructor, since the call causes Initialize() to be called in the View, which assumes that the object is fully-instantiated.
Note that this Presenter is only used for this purpose; at runtime a Presenter will determine whether to run this View or not. Additionally specifying this Presenter does not preclude the use of any other Presenter with the View.
/* other stuff */