Presenters
model the user interactions. A non-visual blue-print of the user interface.
This section describes how they are implemented in this architecture.
Unlike the ViewModels
, which model the data shown on a screen, the Presenters
model the actions a user can perform on the screen.
Each View
can get its own Presenter
, for instance:
class LoginPresenter { }
class ProductListPresenter { }
class ProductEditPresenter { }
class ProductDetailsPresenter { }
class CategorySelectionPresenter { }
Each user action would be represented by a method, like these:
class ProductEditPresenter
{
void Show() { }
void Save() { }
}
Our action methods of the Presenter
work by a ViewModel-in
, ViewModel-out
principle. An action method returns a ViewModel
that contains the data to display on screen:
class ProductEditPresenter
{
ProductEditViewModel Show() { }
}
Action methods can also receive a ViewModel
parameter containing the data the user has edited:
class ProductEditPresenter
{
void Save(ProductEditViewModel userInput) { }
}
Other action method parameters are also things the user has chosen:
class ProductEditPresenter
{
void Show(string productNumber) { }
}
The Presenter's
action methods are not limited to returning a single type of ViewModel
. It is possible to return different ViewModel
types as well:
class ProductEditPresenter
{
object Save(ProductEditViewModel userInput)
{
if (!successful)
{
// Stay in the edit view
return new ProductEditViewModel();
}
// Redirect to list
return new ProductListViewModel();
}
}
Following those guidelines, the Presenters
form a structured model for what the user can do with the application.
Sometimes you also pass infra and config parameters to an action method:
class ProductEditPresenter
{
object Save(
ProductEditViewModel userInput,
IAuthenticator authenticator) // Infra-related parameter
{
// ...
}
}
But it is preferred that the main chunk of the infra and settings is passed to the Presenter's
constructor:
class ProductEditPresenter
{
/// <summary>
/// Passing infra-related parameters to the constructor.
/// </summary>
public ProductEditPresenter(
IRepository repository,
IAuthenticator authenticator,
string cultureName)
{
// ...
}
}
Internally a Presenter
can use business logic and Repositories
to access the domain model.
It is preferred that ViewModel
creation is delegated to the ToViewModel
layer (instead of creating them directly within the Presenters
):
public ProductEditViewModel Show(int id)
{
// Delegate to ToViewModel layer
var viewModel = entity.ToEditViewModel();
}
This way, ViewModel
creation is in a centralized spot, so that changes only have to be made in one place.
The responsibility of the Presenter
is not limited to ViewModel
creation alone, as you can see here:
/// <summary>
/// An action method with multiple responsibilities.
/// </summary>
public ProductEditViewModel Save(ProductEditViewModel userInput)
{
// ToEntity
Product entity = userInput.ToEntity(_repository);
// Business Logic
new SideEffect_SetDateModified(entity).Execute();
// Save
_repository.Commit();
// ToViewModel
var viewModel = entity.ToEditViewModel();
return viewModel;
}
This way the Presenter
layer is also responsible for retrieving data, calling business logic and converting ViewModels
back to Entities
. So it isnβt a needless pass-through layer just delegating ViewModel
creation. It is a combinator class
, in that it combines multiple smaller aspects of the logic, by delegating work to various parts of the system.
A Presenter
action method can have the following steps:
Β | Β |
---|---|
Security checks |
Executing the necessary security checks to prevent unauthorized access. |
ViewModel Validation |
If possible, Entity Validation is preferred. But sometimes the data canβt be converted to an Entity yet. Validating a ViewModel might be useful in that case. |
ToEntity |
Converting ViewModel data into Entity data. |
GetEntities |
Retrieving Entity data from the database. |
Business Logic |
Executing the necessary logic for data Validation , Calculations , and decisions based on the data. |
Commit |
Saving changes made to the Entities to the database. |
ToViewModel |
Mapping Entity data to the corresponding ViewModel properties , to prepare it for the view . |
NonPersisted Data |
Copying data that does not need to be stored, from the old ViewModel to the new, such as selections or search criteria. |
Redirect |
Returning a different ViewModel , to trigger the UI to go to the appropriate page like a success message or the home screen. |
This seems a bit of a throw-together of concepts, but thatβs how it is for a combinator class, like our Presenters
. Separating these steps is recommended, so that they do not get intermixed or entangled.
Not all of the steps are needed. ToEntity
/ Business Logic
/ ToViewModel
might be the typical steps. Slight variations in the order of the steps are also possible.
Here is a code sample with most of the discussed steps in it:
public class ProductEditPresenter
{
public object Save(ProductEditViewModel userInput)
{
// Security
SecurityAsserter.AssertLogIn();
// ToEntity
Product entity = userInput.ToEntity(_repository);
// Business Logic
IValidator validator = new ProductValidator(entity);
if (validator.IsValid)
{
new SideEffect_SetDateModified(entity).Execute();
// Save
_repository.Commit();
// Redirect
return new ProductListViewModel();
}
// ToViewModel
var viewModel = entity.ToEditViewModel();
// Non-Persisted Data
viewModel.Validation.Messages = validator.Messages;
return viewModel;
}
}
Even though the actual call to the business logic might be trivial, it may still be necessary to convert from Entity
to ViewModel
and back.
One reason might be the stateless nature of the web. It requires restoring state from the View
to the Entity
model in between requests. This is because the ViewModel
sent to the server may be incomplete, only containing the editable parts of the page. Restoration of Entity
state is also needed to delegate responsibilities to the right parts of the system, like delegate to the business layer, which expects Entities
.
You might save the server some work by doing partial loads instead of full loads or maybe execute client-native code. For more info, see: Full Load β Partial Load β Cient-Native Code.
Some actions might also operate onto ViewModels
directly instead:
public void ExpandNode(TreeViewModel viewModel, int id)
{
var node = viewModel.Nodes.Single(x => x.ID == id);
node.IsExpanded = true;
}
This may not be the first option to consider, but sometimes it makes sense.
The Presenter
pattern is a commonly used design pattern for modeling user interactions in an application. By creating a Presenter
for each View
and working with ViewModels
, we can achieve a clear modularization of our presentation logic and we ensure that each component has a specific responsibility. Delegating ViewModel
creation to the ToViewModel
layer enables separation of concerns and allows the Presenter
to focus on its primary responsibility of modeling user interaction and delegating work to the various parts of the system.
The Presenters
form a platform-independent layer below the actual front-end technology. All logic is hidden under a shell of ViewModels
and user actions. This makes it possible to swap out the front-end while leaving the underlying system intact.