Model-View-Presenter and its implementation in ASP.NET - Part 3
In this post, 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".
- Run the program and verify that it is possible to access all routes.
In this series, we are utilizing the .NET 8 version of the .NET framework.
The Counter page
The counter page is quite straightforward: it features a single button that increments a counter each time it is clicked.
The code is also very simple.
1@page "/counter"
2@rendermode InteractiveServer
3
4<PageTitle>Counter</PageTitle>
5
6<h1>Counter</h1>
7
8<p role="status">Current count: @currentCount</p>
9
10<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
11
12@code {
13 private int currentCount = 0;
14
15 private void IncrementCount()
16 {
17 currentCount++;
18 }
19}
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.
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.
In the CounterModified.razor.cs file, add the following code.
1public partial class CounterModifiedView : ComponentBase
2{
3 public int CurrentCount { get; set; }
4
5 protected override async Task OnInitializedAsync()
6 {
7
8 }
9
10 public void IncrementCount()
11 {
12 }
13}
This code will serve as the view, and as we can see, it is very minimalist.
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.
1@page "/countermodified"
2@rendermode InteractiveServer
3@inherits CounterModifiedView
4
5<PageTitle>Counter</PageTitle>
6
7<h1>Counter</h1>
8
9<p role="status">Current count: @CurrentCount</p>
10
11<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
12<p>@WarningMessage</p>
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.
1public class CounterModifiedPresenter
2{
3 private CounterModifiedView _view;
4
5 private readonly object _lock = new object();
6
7 public CounterModifiedPresenter(CounterModifiedView view)
8 {
9 _view = view;
10 }
11
12 public int CurrentCount { get; private set; } = 0;
13
14 public void IncrementCount()
15 {
16 lock (_lock)
17 {
18 CurrentCount++;
19 _view.SetCurrentCount(CurrentCount);
20 }
21 }
22}
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.
1public partial class CounterModifiedView : ComponentBase
2{
3 protected CounterModifiedPresenter _presenter;
4
5 public int CurrentCount { get; set; }
6
7 protected override async Task OnInitializedAsync()
8 {
9 _presenter = new CounterModifiedPresenter(this);
10 }
11
12 public void IncrementCount()
13 {
14 _presenter.IncrementCount();
15 }
16
17 public void SetCurrentCount(int currentCount)
18 {
19 CurrentCount = currentCount;
20 }
21}
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.
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.
Add the following test in this class.
1 public class CounterModifiedPresenterTests
2 {
3 [Test]
4 public void Check_CounterIsIncremented_WhenIncrementButtonIsClicked()
5 {
6 // Arrange
7 var view = new CounterModifiedView();
8 var presenter = new CounterModifiedPresenter(view);
9
10 // Act
11 presenter.IncrementCount();
12
13 // Assert
14 Assert.AreEqual(1, presenter.CurrentCount);
15 Assert.AreEqual(1, view.CurrentCount);
16 }
17}
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.
- Add the following test in the CounterModifiedPresenterTests.cs file.
1[Test]
2public void Check_WarningMessageIsShown_WhenIncrementButtonIsClickedTwoTimes()
3{
4 // Arrange
5 var view = new CounterModifiedView();
6 var presenter = new CounterModifiedPresenter(view);
7
8 // Act
9 presenter.IncrementCount();
10 presenter.IncrementCount();
11
12 // Assert
13 Assert.AreEqual(2, presenter.CurrentCount);
14 Assert.AreEqual(2, view.CurrentCount);
15 Assert.AreEqual("Warning", view.WarningMessage);
16}
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.
1public partial class CounterModifiedView : ComponentBase
2{
3 protected CounterModifiedPresenter _presenter;
4
5 public int CurrentCount { get; set; }
6 public string WarningMessage { get; set; }
7
8 protected override async Task OnInitializedAsync()
9 {
10 _presenter = new CounterModifiedPresenter(this);
11 }
12
13 public void IncrementCount()
14 {
15 _presenter.IncrementCount();
16 }
17
18 public void SetCurrentCount(int currentCount)
19 {
20 CurrentCount = currentCount;
21 }
22
23 public void DisplayWarningMessage(string message)
24 {
25 WarningMessage = message;
26 }
27}
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.
1@page "/countermodified"
2@rendermode InteractiveServer
3@inherits CounterModifiedView
4
5<PageTitle>Counter</PageTitle>
6
7<h1>Counter</h1>
8
9<p role="status">Current count: @CurrentCount</p>
10
11<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
12<p>@WarningMessage</p>
- Modify the CounterModifiedPresenter class.
1public class CounterModifiedPresenter
2{
3 private CounterModifiedView _view;
4
5 private readonly object _lock = new object();
6
7 public CounterModifiedPresenter(CounterModifiedView view)
8 {
9 _view = view;
10 }
11
12 public int CurrentCount { get; private set; } = 0;
13
14 public void IncrementCount()
15 {
16 lock (_lock)
17 {
18 CurrentCount++;
19 _view.SetCurrentCount(CurrentCount);
20
21 if (CurrentCount >= 2)
22 {
23 _view.DisplayWarningMessage("Warning");
24 }
25 }
26 }
27}
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.
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.
Model-View-Presenter and its implementation in ASP.NET - Part 4