Suppose you've got an ASP.NET WebForm application that has been running for ages. As it's only run on the .NET Framework, and there will be no more new feature added, Microsoft recommends migrating the app to .NET 5. However, for many reasons, migration wouldn't be that easy. Despite those reasons, what could you do for migration from ASP.NET WebForm to .NET 5? There are several alternatives, but the most pragmatic and feasible way that might be able to minimise the migration cost is to move to Blazor.
When you see the Counter.razor
file as soon as a new Blazor app is created, it looks like:
@page "/counter" | |
<h1>Counter</h1> | |
<p>Current count: @currentCount</p> | |
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button> | |
@code { | |
private int currentCount = 0; | |
private void IncrementCount() | |
{ | |
currentCount++; | |
} | |
} |
As you can see, both the HTML part and code part co-exist in one single .razor
file. It's OK to run the app like that as long as the app works fine. But as the business grows, requirements get complicated, and the size gets bigger, splitting the .razor
file into two – HTML document and its code-behind – would become more beneficial. In fact, modern app development also strongly recommends applying the Separation of Concerns principle. Throughout this post, I'm going to discuss how to divide the .razor
file into two different files.
- This post is based on the doc, ASP.NET Core Razor Components – Partial Class Support.
- The term, Blazor, points to both Blazor Server and Blazor Web Assembly in general unless specifically mentioned.
- You can download the sample code from this GitHub repository.
Building Partial Class
Basically, all .razor
files become the class objects for themselves. In other words, the Counter.razor
file mentioned above becomes the Counter
class once compiled. Therefore, to separate HTML and code from each other, the code-behind must use the partial
modifier when declaring a class (line #1).
public partial class Counter | |
{ | |
... | |
} |
Then move the codes inside the @code
block to the Counter
class.
public partial class Counter | |
{ | |
private int currentCount = 0; | |
private void IncrementCount() | |
{ | |
currentCount++; | |
} | |
} |
Compile the app and run it, and you will see the Blazor app is up and running without an issue.
Inheriting Component Model
Like above, you can use the partial
modifier. There is another approach, inheriting a component model class. It brings up more benefits because the component model already provides properties and events which you can utilise. Create a CounterBase
class by inheriting the ComponentBase
class (line #1).
public class CounterBase : ComponentBase | |
{ | |
private int currentCount = 0; | |
private void IncrementCount() | |
{ | |
currentCount++; | |
} | |
} |
Then, add the @inherits
directive into the Counter.razor
file (line #2):
@page "/counter" | |
@inherits CounterBase | |
<h1>Counter</h1> | |
<p>Current count: @currentCount</p> | |
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button> |
You will find out the red curly underlines on both @currentCount
and IncrementCount
that indicate errors.
It's because both currentCount
field IncrementCount()
method are in the scope of private
. To access both, the scoping must be changed to protected
at least. Change the scope from private
to protected
and update the currentCount
field to the CurrentCount
property (line #3, 5).
public class CounterBase : ComponentBase | |
{ | |
protected int CurrentCount { get; set; } = 0; | |
protected void IncrementCount() | |
{ | |
this.CurrentCount++; | |
} | |
} |
Let's add one more thing here. The ComponentBase
class contains various methods impacting the component's lifecycle. The OnInitializedAsync()
method is one of them. When you look at the method, as a WebForm developer, it looks very similar to either the Page_Init()
or Page_Load()
method. Let's initiate the CurrentCount
property value to 10
(line #10-13).
public class CounterBase : ComponentBase | |
{ | |
protected int CurrentCount { get; set; } = 0; | |
protected void IncrementCount() | |
{ | |
this.CurrentCount++; | |
} | |
protected override async Task OnInitializedAsync() | |
{ | |
this.CurrentCount = 10; | |
} | |
} |
Then, update the Counter.razor
file to refer to the protected property of CurrentCount
(line #6) instead of the private field of currentCount
. Those red curly underlines have disappeared!
@page "/counter" | |
@inherits CounterBase | |
<h1>Counter</h1> | |
<p>Current count: @CurrentCount</p> | |
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button> |
Compile the app and rerun it, and confirm the app works as expected. In addition to that, you will see the Current count
value of 10
by default.
Combination of Partial Class and ComponentBase Inheritance
The downside of using the component model is to use the @inherits
directives within the accompanying .razor
file. It might be small, but to me, that seems redundant. Can I avoid using the directive? Of course, I can! Just use the partial
modifier to the inheriting class like below. Change the class name to Counter
and add the partial
modifier (line #1).
public partial class Counter : ComponentBase | |
{ | |
protected int CurrentCount { get; set; } = 0; | |
protected void IncrementCount() | |
{ | |
this.CurrentCount++; | |
} | |
protected override async Task OnInitializedAsync() | |
{ | |
this.CurrentCount = 10; | |
} | |
} |
Then, remove the @inherits
directive from the Counter.razor
file.
@page "/counter" | |
<h1>Counter</h1> | |
<p>Current count: @CurrentCount</p> | |
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button> |
Let's compile the app and run it. Can you see what you expected?
So far, we've walked through how to extract the codes from HTML in a .razor
file of a Blazor application. In fact, it's not a tip nor a trick, but a suggestion for a better app development approach. Suppose you or your organisation plans to migrate existing ASP.NET WebForm applications to Blazor and wants to keep the same development experience. In that case, this post will be the starting point.