I've decided to write a small survey site using Blazor. Part of this is an excuse to learn Blazor.
As I learn with Blazor I will blog about it as a series of articles.
This series of articles is not intended to be a training course for Blazor - rather my thought process as I go through learning to use the product.
All articles in this series:
Your Blazor server side app will be initiated something like this in _Hosts.cshtml:
<app>
<component type="typeof(App)" render-mode="ServerPrerendered" />
</app>
I believe by default, the render-mode
will be set to ServerPrerendered
- and I think most examples will show render-mode
with that option - and I have to admit I didn't really pay much attention to it until I struggled with my automated end-to-end tests.
I had built an end-to-end happy path test using Selenium and orchestrated it to run every hour using Azure DevOps. The test was fairly simple; it ran through every page of the survey with defined responses - then checked against CosmosDB to ensure that the survery results had been recorded.
I could run the test locally and it worked perfectly everytime.
And it would mostly work correctly when being run on the hourly schedule - but it wasn't consistent.
Occassionally I'd get failures. When I investigated it looked as if my Selenuim test simple wasn't pressing the "Next" button on the home page:
I experimented with various techniques to track down the problem - but eventually I discovered the cause.
It was the render-mode
.
What I hadn't taken the time to fully appeciate was how the ServerPrerendered
worked.
From the Microsoft Docs page:
> "Renders the component into static HTML and includes a marker for a Blazor server-side application. When the user-agent starts, it uses this marker to bootstrap a blazor application."
The important part here is the "Renders the component into static HTML".
On first request, the ServerPrerendered
will return the the browser a static HTML representation to the page requested (with all its components). The browser will display this HTML - then run the Blazor Javascript to establish a SignalR connection. Once the SignalR connection is established THEN the Blazor page is re-rendered - this time with all the Blazor goodness.
What you are effectively getting it a two stage render to the browser:
This is a techniques called hydration that has been popular in Javascript Single-Page-Apps (SPAs) for a while. The intention is to provide the user with valuable content (the HTML) while the app is downloading, initializing and starting in the background. Once ready the app would "take over" - replacing the HTML on screen.
Done right it would look to the user that the app is ready much quicker that it actually is (plus the technique provides a considerable benefit for SEO - the clawler doesn't need to execute the Javascript to understand the content).
So my end-to-end tests where failing due to recieving the cosmetic-only HTML and attempting to use it BEFORE the app had rehydrated and became functional.
The test would open the website - get the HTML for the first page and click on the Next button.
But this was BEFORE the app had rehydrated - thus there was no click handler behind the Next button. Thus clicking on it did nothing.
Thus when the end-to-end test asserted that the next page had been loaded, it failed because of course none of the logic had been run.
I could have changed the render-mode
(see below) but I chose to implement a loading spinner while in that pre-rehydrated state;
I felt this was best both for the user and for Selenium. The user wouldn't become frustrated by a button that did nothing and Selenium would wait until the Next button appeared.
For this I created the PreRenderLoadingMessage
component:
@if (HasRendered)
{
@ChildContent
}
else
{
<LoadingSpinner Title="Loading ..." />
}
@code {
[Parameter]
public RenderFragment ChildContent { get; set; }
private bool HasRendered { get; set; }
protected override void OnAfterRender(bool firstRender)
{
if (firstRender)
{
HasRendered = true;
StateHasChanged();
}
}
}
Then I wrapped the entire app within it:
<PreRenderLoadingMessage>
<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
<p>Sorry, there's nothing at this address.</p>
@{ Is404 = true; }
</LayoutView>
</NotFound>
</Router>
</PreRenderLoadingMessage>
Thus the first render (the prerender) shows the LoadingSpinner
, then subsequent (hydrated) renders would show the @ChildContent
.
This technique was taken from this Microsoft Docs page.
This approach solved my problem nicely. The end-to-end tests worked like a charm once I implemented it.
So there are two other modes available to render-mode
- Server
and Static
.
Static
will just render static HTML - it doesn't provide any of the functionality. I've not seen a good reason to use this mode - although I do wonder if it could be used with a static site generator - something like GatsbyJs. I suspect its there for future use.
Server
will operate the same as ServerPrerendered
but without the static HTML version on the first page request. Rather it leaves the app
content empty until the SignalR connection is established and Blazor is running fully.
The problem with Server
mode was that it can take a few seconds to establish that SignalR connection and to get everything "started". I didn't want to provide an empty page to the user. I'd end up adding some form of loading message in the _host.cshtml - only to then have to remove once the Bazlor app was ready.
Thus it made much more sense to use the functionality baked into Blazor with the ServerPrerendered
- and I think that the PreRenderLoadingMessage
is a good way to handle that.