Although there are many examples available demonstrating Blazor views it is also possible to separate code and presentation by using Razor pages with backing model. This blog post is based on my Blazor presentation and focuses on separating code and presentations of Blazor pages.
Sample form
For this post I’m using sample edit form from my Blazor presentation. It’s simple one and allows to add/edit few details of books. It uses simple server-side API to retrieve and save details of books. The following screenshot illustrates it.
I started with samples found from web search and worked out something that is more manageable and easier to control.
Source code available!. Please visit my GitHub repository gpeipman/BlazorDemo for full source code of my Blazor demo application.
Starting with popular examples
Most of examples we can see would build the form above by keeping code in the file with view. Although it’s a small view with not much logics we still end up with lengthy file like shown above.
@using Microsoft.AspNetCore.Blazor.Services
@using BlazorDemo.Shared
@page "/edit"
@page "/edit/{Id}"
@inject HttpClient Http
@inject IUriHelper UriHelper
<h1>@PageTitle</h1>
@if (CurrentBook != null)
{
<div class="row">
<div class="col-md-9">
<form class="form form-horizontal">
<div class="form-group">
<label class="col-md-2 control-label">Title:</label>
<div class="col-md-7">
<input type="text" bind=@CurrentBook.Title class="form-control" />
</div>
</div>
<div class="form-group">
<label class="col-md-2 control-label">ISBN:</label>
<div class="col-md-7">
<input type="text" bind=@CurrentBook.ISBN class="form-control" />
</div>
</div>
<div class="form-group">
<label class="col-md-2 control-label"></label>
<div class="col-md-7">
<button class="btn btn-primary" onclick=@Save>Save</button>
</div>
</div>
</form>
</div>
</div>
}
else
{
<p><em>Loading...</em></p>
}
@functions
{
[Parameter]
string Id { get; set; }
string PageTitle { get; set; }
Book CurrentBook { get; set; }
protected override async Task OnParametersSetAsync()
{
if (Id == null || Id == "0")
{
PageTitle = "Add book";
CurrentBook = new Book();
}
else
{
PageTitle = "Edit book";
await LoadBook(int.Parse(Id));
}
}
private async Task LoadBook(int id)
{
CurrentBook = await Http.GetJsonAsync<Book>("/Books/Get/" + id);
}
private async Task Save()
{
await Http.PostJsonAsync("/Books/Save", CurrentBook);
UriHelper.NavigateTo("/");
}
}
.It works but it is far from being nice and far from being testable or reusable.It’s just like legacy form from previous eras but it’s using modern technologies.
Separating code and presentation
- Keep code and presentation separated
- Use dependency injection
- Avoid repeating code
First change is the biggest one. We can rename the current page and change it’s route to something else so it’s not the same with the new page. After this we add new page and give it same name as old view had before.
In Visual Studio we have to add Razor Page (not Razor View) as Razor Page is created with backing model file like shown on image on right.
Now we can take code from old view and move it to model class.
It leaves us with clean view like here.
@page "/edit"
@page "/edit/{Id}"
@inherits EditBookModel
<h1>@PageTitle</h1>
@if (CurrentBook != null)
{
<div class="row">
<div class="col-md-9">
<form class="form form-horizontal">
<div class="form-group">
<label class="col-md-2 control-label">Title:</label>
<div class="col-md-7">
<input type="text" bind=@CurrentBook.Title class="form-control" />
</div>
</div>
<div class="form-group">
<label class="col-md-2 control-label">ISBN:</label>
<div class="col-md-7">
<input type="text" bind=@CurrentBook.ISBN class="form-control" />
</div>
</div>
<div class="form-group">
<label class="col-md-2 control-label"></label>
<div class="col-md-7">
<button class="btn btn-primary" onclick=@Save>Save</button>
</div>
</div>
</form>
</div>
</div>
}
else
{
<p><em>Loading...</em></p>
}
When dealing with code we have to understand that variables used in view cannot be private anymore as they are located in base class that view is extending. So we have to take care of visibility of model class members when moving code to model class.
public class EditBookModel : BlazorComponent
{
[Inject]
protected IUriHelper UriHelper { get; set; }
[Inject]
protected HttpClient Http { get; set; }
[Parameter]
protected string Id { get; private set; } = "0";
protected string PageTitle { get; private set; }
protected Book CurrentBook { get; set; }
protected override async Task OnParametersSetAsync()
{
if (Id == null || Id == "0")
{
PageTitle = "Add book";
CurrentBook = new Book();
}
else
{
PageTitle = "Edit book";
await LoadBook(int.Parse(Id));
}
}
protected async Task LoadBook(int id)
{
CurrentBook = await Http.GetJsonAsync<Book>("/Books/Get/" + id);
}
protected async Task Save()
{
await Http.PostJsonAsync("/Books/Save", CurrentBook);
UriHelper.NavigateTo("/");
}
}
We had two members injected to view: Http and UrlHelper. For Razor views the regular view injection is used. For models behind Razor pages we cannot use similar injection of instances. We cannot also use constructor injection and this is why we use InjectAttribute by Blazor. Also notice that models for Blazor pages must inherit from BlazorComponent.
Now we have view and presentation separated. Our edit form still works but it is more controllable.
Moving to base component
It’s highly probable that our Blazor applications use some back-end API to work with data. It means we need HttpClient available behind other pages too. Also we probably need IUrlHelper to make some routing. Depending on application there can be also some other shared functionalities needed by number of forms.
public abstract class BaseComponent : BlazorComponent
{
[Inject]
protected IUriHelper UriHelper { get; set; }
[Inject]
protected HttpClient Http { get; set; }
}
Now we can change our edit form model to extend the base component.
public class EditBookModel : BaseComponent
{
[Parameter]
protected string Id { get; private set; } = "0";
protected string PageTitle { get; private set; }
protected Book CurrentBook { get; set; }
protected override async Task OnParametersSetAsync()
{
if (Id == null || Id == "0")
{
PageTitle = "Add book";
CurrentBook = new Book();
}
else
{
PageTitle = "Edit book";
await LoadBook(int.Parse(Id));
}
}
protected async Task LoadBook(int id)
{
CurrentBook = await Http.GetJsonAsync<Book>("/Books/Get/" + id);
}
protected async Task Save()
{
await Http.PostJsonAsync("/Books/Save", CurrentBook);
UriHelper.NavigateTo("/");
}
}
We are where we wanted to go no – presentation and code of Blazor form are separated. We still have dependency injection and other functionalities we had before but we have also way better control over code right now. There are more things to improve in this code but these topics I will leave for my future posts.
Wrapping up
Many examples from web show how to build Blazor views and they usually put presentation and code together to same file. Although it works it is not architecturally correct. It makes it harder to work on more complex views and it is harder to debug our code, not to mention lost testability. Blazor supports using page models like we know from Razor Pages. Changes to existing views are not big and can be done easily. Although we have more code artifacts as a result, we still gain better control over application.
View Comments (10)
How would one go about mocking dependencies supplied by [Inject]?
Good job, is the code on github?
Rich, it is possible to define dependencies when application starts. I'm sure there is some way how to invoke all this manually for testing right now but I don't recommend to rush - Blazor is still very young platform and there is a lot of work to do. I'm sure they don't miss testing.
Alan, here we go: https://github.com/gpeipman/BlazorDemo
Much appreciated, thank you
Thank you Gunnar for putting this out. Wish the official documentation would not mix c# code and UI code. But from a marketing perspective I guess it's easier to compete with things like vue.js and such, where "with a few simple lines of code/html" you get magic :)
PS: sent a PR (https://github.com/gpeipman/BlazorDemo/pull/1) to fix a broken reference.
Thanks, Sean! Merged your pull request to master. This reference is weird - I was able to make it through Visual Studio without unloading this project and changing project file.
This is so beautiful, I can't believe it took so many years to be done, but I'm all aboard Blazor board.
Does this article still hold true for the latest (.NET Core 3.0 Preview 6+) update of Blazor?
There's one change - Blazor views have now .razor extension (not cshtml like before) and code-behind files should be names like ViewName.razor.cs.