Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / ASP.NET / ASP.NET-Core

The Model-View-Presenter pattern and its implementation in ASP.NET

5.00/5 (1 vote)
1 Oct 2024CPOL11 min read 1.4K   31  
How to implement the MVP pattern in ASP.NET ?
Beginning today, we are launching a short series focused on the Model-View-Presenter (MVP) pattern in ASP.NET. Our goal is to offer a thorough overview, explaining what it entails, why it's worth considering for improved maintainability and testability, and outlining its potential limitations.

Introduction

Automated testing has become crucial in modern applications, and developers are increasingly aware of its importance throughout the software lifecycle. However, not all aspects of an application can be thoroughly tested, with UI components and pages often requiring manual creation. This process is both time-consuming and prone to errors. To address this challenge, several design patterns emerged in the early 1990s, aimed at reducing these difficulties and providing alternative solutions.

In this brief series, we will explore one of these key design patterns: Model-View-Presenter (MVP). We will examine how this pattern shifts most of the business logic to specific objects (known as presenters), while treating the UI (the view) as a simple, passive element. Our aim is to demonstrate these concepts through a straightforward Blazor application.

The following textbooks prove useful on this topic.

This article was originally published here: Model-View-Presenter and its implementation in ASP.NET

Why are tests essential in a software application ?

We won't go into great detail on this question, as the importance of automated tests is widely acknowledged. Tests help ensure non-regression, allowing us to release new versions more frequently and with greater confidence.

Consider, for example, a basic requirement for a Payment class where we need to calculate the VAT for a given price. The corresponding C# code might look like this.

C#
public class Payment
{
    public decimal Price { get; private set; }

    public Payment(decimal price)
    {
        Price = price;
    }
    
    public decimal PriceVATIncluded => Price * 1.2;
}

The corresponding test class should ressemble to the following (in NUnit).

C#
public class PaymentTests
{    
    [Test]
    public void CheckVATIsCorrectlyCalculated()
    { 
        // Arrange
        var payment = new Payment(100.0);
        
        // Act
        var priceWithVAT = payment.PriceVATIncluded;
        
        // Assert
        Assert.AreEqual(120.0, priceWithVAT);
    }
}

This code is quite simple and, in itself, not particularly interesting, but it's something we will inevitably encounter at some point in the software lifecycle. In this example, it's evident that tests for DOMAIN OBJECTS can be relatively easy to maintain.

Why is UI so difficult to test ?

We've just seen that testing domain classes is often straightforward (though not necessarily easy). In contrast, testing UI classes is frequently quite challenging.

  • User interfaces often involve intricate interactions between elements, such as buttons, forms, and dynamic content.

  • UIs can change frequently due to design updates, which might break existing tests or require constant adjustments to test scripts.

  • Testing UIs requires managing and validating different states of the application, including user inputs, error messages, and loading conditions.

  • Automated tests often struggle with visual verification, such as ensuring that elements are correctly positioned, styled, and responsive.

For example, consider a simple login page where users attempt to authenticate. The code might resemble the following.

C#
public class LoginView
{
    // ...
    
    public Task btnLoginButton_Submit(EventArgs e)
    {
        var username = txtUserName.Text;
        var password = txtPassword.Text;
        
        if (_userRepository.Authenticate(userName, password))
        {
            txtWelcome = "Welcome, you are logged in !";
            btnSeeMoreFeatures.Enabled = true;
        }
        else
        {
            txtWelcome = "Sorry !";
        }
    }
}

To properly test this code, we need to simulate a submit event (which can be complex) and then verify that the various components have updated correctly. Even if we had a tool that could extract this data directly from the HTML, we would still face race conditions, as checking the new values too soon could lead to inaccurate results.

Additionally, we are not performing visual verification. Are the textboxes rendered correctly ? Is the design responsive ? These questions require careful attention. How to address them ?

Model-View-Presenter to the rescue

In the previous description of UI tests, it's important to recognize that there are two types of tests: one purely focuses on business logic (e.g., when I click the Submit button and authentication is successful, a welcome message is displayed; otherwise, an error message is shown), while the other deals with visual effects (e.g., the button should be 200px wide and positioned next to the text box).
The second type (dealing with visual effects), is more challenging to handle and typically requires specialized tools like Selenium or Katalon (see our article here for more details). However, the first type, which focuses on business logic, can be encapsulated in specific classes designed for maintainability and testability. This is where the MVP pattern comes into play.

Definition
The MVP design pattern involves delegating all business logic that would typically be part of a UI component (the view) to a dedicated class known as the presenter. This presenter interacts with the model to validate rules or invoke more complex services, and then updates the UI as needed. As a result, the view becomes a simple entity that only communicates with the presenter when necessary: it is an humble object.

Humble Object is a way to bring the logic of these hard-toinstantiate objects under test in a cost-effective manner.
Meszaros xUnit Test Patterns

What is the model ?

There's no need to go into detail on this topic: the model is simply the representation of the domain within a programming language. It encompasses the business rules and the complex logic that exists between different classes.Image 1

Important
In a microservices architecture, each bounded context (microservice) typically has its own model. Consequently, a UI component might need to display data from multiple models.

The model is often quite straightforward to test.

What is the view ?

A model serves little purpose if it is not displayed or utilized by another client. In this case, it is the view's responsibility to allow the user to interact with the model through various visual elements such as text boxes, buttons, and forms. These components collectively constitute the UI.

Everything is intertwined
 

The view can contain complex logic: even in our simple example, the Submit button must retrieve values from two text boxes, validate authentication, and display a message accordingly. This business logic is intertwined with the code that manages visual effects.

Image 3

We could conclude here and proceed with this approach: for small applications or proofs of concept, having just a model and a view interacting with it might suffice. However, it’s important to be aware of the issues this approach can introduce, as mentioned earlier. Other concerns also fall under this topic: for example, the view often has to handle multithreading. These factors contribute to making the UI highly unpredictable and, consequently, very difficult to test.

What is the presenter ?

The role of the presenter is specifically to offload the business logic into a separate class, making testing more streamlined. The visual layout remains within the view and should still be tested using traditional UI testing tools.

To be remembered
The presenter allows the view to offload the business logic that complicates it, leaving it focused solely on visual effects.

 

Image 4

 

The presenter serves as an intermediary between the model and the view. Additionally, the presenter can update the view as needed, whether based on its own decisions or changes in the model. Importantly, the presenter can be instantiated in test classes, facilitating testing and ensuring that business logic is properly validated.

Important
The view no longer interacts directly with the model and is unaware of it. Instead, the presenter handles communication with the model and extracts the necessary information.

Having provided a brief overview of the MVP pattern, let's now examine it in greater detail and see it in action.

we will demonstrate how to implement the MVP pattern and follow a step-by-step approach to provide a thorough and comprehensive overview.

This MVP pattern will be implemented using a Blazor application, though it can naturally be applied to any other programming language as well.

Establishing the environment

We will proceed to configure a standard Blazor environment within the Visual Studio 2022 IDE. This basic application will serve as our foundation for gradually elucidating the underlying concepts.

  • Create a new solution named EOCS.ModelViewPresenter for example and a new Blazor Web App project in it named EOCS.ModelViewPresenter.UI. When adding information, ensure to select "None" as the Authentication type, "Server" for the interactive render mode and to check "Include sample pages".
Image 5

 

  • Run the program and verify that it is possible to access all routes.
Image 6

Information
In this series, we are utilizing the .NET 8 version of the .NET framework.

The default samples provided by Microsoft are notably well-suited to demonstrate how the MVP pattern can be effectively implemented to enhance maintainability and testability. Two URLs, in particular, are of great interest, and we will be refactoring the code based on them.

The Counter page

The counter page is quite straightforward: it features a single button that increments a counter each time it is clicked.

Image 7

The code is also very simple.

ASP.NET
@page "/counter"
@rendermode InteractiveServer

<PageTitle>Counter</PageTitle>

<h1>Counter</h1>

<p role="status">Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;

    private void IncrementCount()
    {
        currentCount++;
    }
}

There is nothing inherently wrong with this code except that it intertwines the general layout (with a button positioned on the left and a label below it showing a message) and the business logic (specifically, when the button is clicked, the label must display the increment). As a result, it becomes challenging to test this business logic independently and guarantee that potential regressions will be avoided if the code is modified by another developer.

The Weather page

The Weather page is more complex, yet remains relatively simple. The key addition here is the introduction of data retrieval from a datastore (a common operation in web applications). However, as with the Counter page, the business logic is still intertwined with the general layout.

 

Image 8

We will start by refactoring the code for the Counter page, and the refactoring for the Weather page will be addressed in the next post.

Adding the view

  • Duplicate the existing Counter.razor file and rename the copy to CounterModified.razor.

  • Add a new class and name it CounterModified.razor.cs The file should be located in Visual Studio directly beneath the corresponding class.

    Image 9

     

  • In the CounterModified.razor.cs file, add the following code.

C#
public partial class CounterModifiedView : ComponentBase
{  
    public int CurrentCount { get; set; }

    protected override async Task OnInitializedAsync()
    {
        
    }

    public void IncrementCount()
    {        
    }    
}

This code will serve as the view, and as we can see, it is very minimalist.

Tip
We are utilizing Visual Studio's features to organize our project while also leveraging the specific ASP.NET page lifecycle (the OnInitializedAsync method) to enhance our development process. This code should be tailored to each programming language; however, the underlying philosophy remains consistent.

  • Modify the CounterModified.razor file.
ASP.NET
@page "/countermodified"
@rendermode InteractiveServer
@inherits CounterModifiedView

<PageTitle>Counter</PageTitle>

<h1>Counter</h1>

<p role="status">Current count: @CurrentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
<p>@WarningMessage</p>

Information
In our implementation, the view ultimately consists of two files: one focuses on the general layout, including styles and javaScript (CounterModified.razor), while the other is dedicated to the business logic (CounterModified.razor.cs).

It is now time to dive into the core of the matter and explore how the presenter can structure the business logic.

Adding the presenter

At the moment, our code is ineffective because clicking the button doesn't trigger any action.

  • Add in the CounterModified.razor.cs file a new class named CounterModifiedPresenter.
C#
public class CounterModifiedPresenter
{
    private CounterModifiedView _view;

    private readonly object _lock = new object();

    public CounterModifiedPresenter(CounterModifiedView view)
    {
        _view = view;
    }

    public int CurrentCount { get; private set; } = 0;

    public void IncrementCount()
    {
        lock (_lock)
        {
            CurrentCount++;
            _view.SetCurrentCount(CurrentCount);
        }
    }
}

As mentioned in our previous post, the presenter is responsible for organizing the business logic. Specifically, in our scenario, it must ensure that the counter is properly incremented when the button is clicked. This task is handled by the IncrementCount method. In this case, the logic is quite simple: the IncrementCount method only needs to add one to the CurrentCount property.
Additionally, we can observe that after the presenter increments the counter, it also updates the view. For this reason, the presenter must have a reference to the view, which is passed through the constructor. As a result, the view’s code must be modified to properly initialize the presenter and ensure it is notified when an update is required.

C#
public partial class CounterModifiedView : ComponentBase
{
    protected CounterModifiedPresenter _presenter;

    public int CurrentCount { get; set; }

    protected override async Task OnInitializedAsync()
    {
        _presenter = new CounterModifiedPresenter(this);
    }

    public void IncrementCount()
    {
        _presenter.IncrementCount();
    }

    public void SetCurrentCount(int currentCount)
    {
        CurrentCount = currentCount;
    }
}

Information
In this example, we can observe how the view delegates all the business logic to a third party, focusing solely on displaying the components.

  • Run the program.
    Image 10

The behavior remains the same as before, as expected. However, we have now gained a significant feature, which we will demonstrate in the next section.

Adding tests

Checking that the existing feature works as expected

Now comes the reward for our efforts: we can test the business logic of the page without relying on UI tools like Selenium. Instead, we can use the familiar testing tools we have traditionally employed (in our case, NUnit).

  • Add a new NUnit Test project and name it EOCS.ModelViewPresenter.UI.Tests for example.

  • Add a reference to the EOCS.ModelViewPresenter.UI project.

  • Add a new class named CounterModifiedPresenterTests.cs.

    Image 11

     

  • Add the following test in this class.

C#
 public class CounterModifiedPresenterTests
 {
    [Test]
    public void Check_CounterIsIncremented_WhenIncrementButtonIsClicked()
    {
        // Arrange
        var view = new CounterModifiedView();
        var presenter = new CounterModifiedPresenter(view);

        // Act
        presenter.IncrementCount();

        // Assert
        Assert.AreEqual(1, presenter.CurrentCount);
        Assert.AreEqual(1, view.CurrentCount);
    }
}

This test allows us to verify that the IncrementCount method functions as expected. At the same time, we check whether the view correctly displays the updated value.

Coding new tests

We can now adopt a more test-driven development approach by writing tests before implementing the feature. For instance, consider a business rule that requires a warning message to be displayed when the button is clicked twice.

Image 12

 

  • Add the following test in the CounterModifiedPresenterTests.cs file.
C#
[Test]
public void Check_WarningMessageIsShown_WhenIncrementButtonIsClickedTwoTimes()
{
    // Arrange
    var view = new CounterModifiedView();
    var presenter = new CounterModifiedPresenter(view);

    // Act
    presenter.IncrementCount();
    presenter.IncrementCount();

    // Assert
    Assert.AreEqual(2, presenter.CurrentCount);
    Assert.AreEqual(2, view.CurrentCount);
    Assert.AreEqual("Warning", view.WarningMessage);
}

This code simply translates into C# what we have just outlined in English. Naturally, since some methods do not exist at this point, this test will fail.

  • Modify the CounterModifedView class.
C#
public partial class CounterModifiedView : ComponentBase
{
    protected CounterModifiedPresenter _presenter;

    public int CurrentCount { get; set; }
    public string WarningMessage { get; set; }

    protected override async Task OnInitializedAsync()
    {
        _presenter = new CounterModifiedPresenter(this);
    }

    public void IncrementCount()
    {
        _presenter.IncrementCount();
    }

    public void SetCurrentCount(int currentCount)
    {
        CurrentCount = currentCount;
    }

    public void DisplayWarningMessage(string message)
    {
        WarningMessage = message;
    }
}

Note that in the view, we only add some simple getters and setters, and nothing more. Essentially, the view becomes an anemic object.

  • Modify the CounterModified.razor file.
ASP.NET
@page "/countermodified"
@rendermode InteractiveServer
@inherits CounterModifiedView

<PageTitle>Counter</PageTitle>

<h1>Counter</h1>

<p role="status">Current count: @CurrentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
<p>@WarningMessage</p>
  • Modify the CounterModifiedPresenter class.
C#
public class CounterModifiedPresenter
{
    private CounterModifiedView _view;

    private readonly object _lock = new object();

    public CounterModifiedPresenter(CounterModifiedView view)
    {
        _view = view;
    }

    public int CurrentCount { get; private set; } = 0;

    public void IncrementCount()
    {
        lock (_lock)
        {
            CurrentCount++;
            _view.SetCurrentCount(CurrentCount);

            if (CurrentCount >= 2)
            {
                _view.DisplayWarningMessage("Warning");
            }
        }
    }
}

We can see that it is the responsibility of the presenter to enforce the business rule; it is within the presenter that the logic is ultimately implemented.

All the tests are now passing.

Image 13

 

After demonstrating the MVP pattern with a simple example, we will now explore how it can be applied to a more complex, yet still straightforward, scenario on the Weather page. To avoid overloading this article, readers interested in this implementation can find the continuation here.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)