I've been doing a few videos recently on ASP.NET
Blazor and the distinction between Blazor WebAssembly and Blazor Server: Blazor WebAssembly
where everything is processed in the browser and Blazor Server when the processing is done back on
the server. But what I'm going to talk about this week is a way to combine the two to solve quite
a common problem in web development - across all different technologies, when you're doing SPAs,
Single Page applications - the problem of the initial load. So let's have a look at what we mean
by that. And so what I've got here is just a very simple Blazor WebAssembly application. And if I
run that up, then we can see that when the page initially appears, we're getting that status
display to show that the page is loading, once the page ha s loaded, then we can navigate
within it to the different tabs and that sort of thing. And that's very, very quick, because
all of that happens within the browser itself. So there's no more calls made to the server for that
sort of thing to happen. But if we do something like a refresh … so in navigating to it again,
again we have this wait of a few seconds, and the indication that something's happening. And that's
the problem that we get whether you're doing this sort of thing with Angular, or with React.js or
here with Blazor, that when you've got this idea of what they call a single page web application,
it has this odd way of starting up. Because if we actually take a look at the page source, this is
what was initially downloaded when we navigated to the site. So it's just a very simple HTML page
with very little on it. Basically, just there we've got the way that it was displaying that
circle for the progress. But then the real trick that we've got here is that we're pulling in
various scripts - like the one we've got there, and that one there. And that pulls in the whole of
the Blazor WebAssembly framework that's required, plus it pulls in all of our DLLs that we've
written in our particular application. But that's what slows it down, because this initial
page is what we request. That comes down and then that makes further requests for the other
files, which then have to be pulled in, then have to do the processing within the browser.
And only then does the page actually appear. So that's why we have those various steps and that
delay. And although it's not an awful experience, sometimes it'd be nicer if you just immediately
got what we're seeing here, the actual thing we have on the page. And with a server-side
application, like written in something like MVC, that's exactly what would happen. But then, of
course, with something like MVC, every request goes through that. So what we want is to have
the very first view of the page processed on the server and delivered to us whole, and then after
we've done that have the client-side processing in WebAssembly that is going to do the work for
us. And it's very common problem in all of these SPAs, if you're using something like React, then
there's also a library called Next which will help with this sort of thing. But we're going to see
how we can do this with Blazor. Now it's worth pointing out immediately one slight disadvantage
to what we're going to do. If you look at this simple Blazor application, as I said, there's
no server-side processing at all. All the web server does for an application like this is simply
does what a web server did back 30 years ago: it just serves up files. So the browser requests the
files - gets them. No processing on the server, so no high-powered computing needed. So it
can be very much cheaper for hosting than what we're going to see here, where you do need some
server-side processing. So what I'm going to do is do a new project. And this one is going to be
a Blazor WebAssembly app again. So we'll click on that one. And I'll call this ServerSideStartup.
And then the really important thing I've got to do here is click that box there. 'Cause if I didn't
do that, I'd get exactly the same sort of project I had before, which is pure WebAssembly - only
going to run the browser. Putting this in here, we're now saying ‘ASP.NET Core Hosted’ that
is going to create a server project as well as the client-side project. You can see we've got
that there. So there is our server-side project, so everything in there is going to run on the
server. Here we've got our client project. So that's what's going to run in the browser under
WebAssembly. And one of the nice things about this, you can also have classes that are shared
between the two. So that's what I've got in that separate library that we have there. Now the
reason initially that you do this sort of thing is typically if on the server, you want to be running
a web service that's going to give you data. And in fact, that's already configured here because
if we look on the server, you can see you've got the traditional WeatherForecastController that you
see in a lot of these applications. And that just gives us this weather forecasting. And something
to just note while we're looking at that is the way this works is it randomises the data so
the temperatures we get there are randomized. Remember that, because we'll see that later
on. What I'm actually going to do here, though is just to simulate the real world, when we'd
be running on the internet, not just locally on my machine - let's just put a delay in here, so it's
a little bit slow to get this data. So I'm going to turn that into an ‘async Task’ of all of that.
And then we'll just in here say ‘await Task.Delay’ and then we'll make it two seconds. And the
reason I put that in is just to show a further slight problem we get when we're doing rendering
in the browser. That is, if I run that up again, then of course, we still get the delay on
startup and then finally get that. But if I go to this Fetch Data, you can see there's an
additional delay that's mostly due to that two seconds I deliberately put in - but it will
still be there in the real world. And so if I actually do a refresh on this page, we get the
one delay for downloading the application and then once we're in there, we get the second delay
of waiting for the data to come. So ideally, we'd like that page just to appear all there
all ready for us. So that's one of the things we're going to solve. So how do we go about doing this
- to make this so that we don't get that circle to indicate that we're waiting, we just get the
data straightaway? Well, there's a number of steps we've got to do for that. First thing we have to do is
… at the moment, the thing - as we said - that's being downloaded is this Index page. So that's
really what I was just showing you in the browser, where we had the <div> for the loading progress
and that sort of thing. We need to replace that by a server-side page. So in the server project,
under Pages, I'm going to give this a new Razor page - not a Blazor page, a Razor page - I'm
going to have that as an empty one and I'm going to call this underscore ‘_Host’ – call it what
you like, really, but that's quite a common thing to call it. And so we're on that, we just get
that basic Razor page in there. So remember, this is running server-side. Then what I do is
I go back to that Index, and I take the whole of that and paste it into this new .cshtml page
that we've got there. And the only thing I need to leave in there is just the ‘@page’ at the top
so that it knows it's a Razor page. And then I can also delete a couple of things. I can delete the
Index.html, because we're going to be effectively generating that from our _Host. And then I'm also
going to delete the code behind because we don't actually need any code here so I can delete that
as well just to simplify things. So now, this is going to be the page that has some server-side
processing, but then gets delivered to the browser as the whole thing. So what we want to do there is
firstly, we just need to enable tag helpers. So if you're familiar with MVC, then you'll be familiar
with tag helpers, but that's what we're going to do here. So I'm going to say ‘@addTagHelper’
and then I want all the tag helpers from Microsoft.AspNetCore.Mvc.TagHelpers. So these are
just the standard tag helpers that come with MVC. And then what I'm going to do is in here, I'm
going to get rid of that loading progress and I'm going to put in there a ‘<component>’. So
we're going to use this idea of a <component>, so this is what we can do in server-side
rendering. And that <component> is going to be of the ‘type’ and that's going to be ‘typeof’
and then we're going to use the application component in the client-side application. So in
the client-side application, you can see we've got this thing called ‘App.razor’. And that
actually is the starting point for our site, so that is what we want to render, but server side
in here. So what we need to do is ‘typeof’ and then just the fully qualified name. So that's
going to be ‘ServerSideStartup.Client.App’. And so that's the thing that we're going
to be rendering server-side. And then we're going to do the thing that's really key
to the whole of this, we are going to set a ‘render-mode’. And that render-mode is going to
be ‘WebAssemblyPrerendered’. So does exactly what it says on the tin - this is what we're trying to
do prerender it. So that's basically just saying do it the once on the initial load, but not then
subsequently. So that's what we put in there. So this is going to run on the server and generate
the entire of our first page as if it would be doing it when it runs on the browser normally.
Got to do a little bit more there. We've also then got to go to the program - again the server
program. And in here, if we just go down here, you can see that at the bottom here, it's saying
‘app.MapFallbackToFile(“index.html”)’. So that's saying that's what we should initially download,
which of course we've just deleted because we don't want that anymore. So we change that
not to map to a file, but to map to a page, because we're going to have to do some
processing on here. And then we put in what we just had, which is going to be a ‘/_Host’,
the name of the file that we just wrote. One other thing we're going to do, just to avoid some
errors, is if we go to the program in the client, you can see what it does there is it looks
for a <div> with an id of ‘app’. Well, that <div> we actually just deleted because back
in the _Host, when - if I just control-Z that a bit - when we deleted all of the code that we
had here, there is our <div> with the id of ‘app’, so it's not going to be able to get hold of
that anymore, because we've basically replaced it by putting this in here. So we just need to
remove that line of code - we can also get rid of that ‘using’ as well. So that should really be
all we need to do. So if we now run that up, then we can see that the page appeared
immediately, if I click refresh, you can see, although obviously, it's still got some slight
delay, it's not got the progress bar. It is just taking it straight there. And we can see that
difference slightly more concretely, because if we look at the page source, you can see the page
that was actually delivered to it had the things we want. So you can see, we've got the ‘Welcome to
your new app’, we've got all that sort of thing in there, because that was generated on the server.
And that's what's been handed over. We do still, of course, have the various JavaScript files
that we need for the WebAssembly, because once it's delivered that initial page, all the other
interactions are now occurring within the browser. So it's still got to download that as well.
But it means it can display as the initial view and then download those things in the background
rather than us waiting for those downloads before anything happens. There is however, still a slight
problem. If I go to the Fetch Data … Now that one, we had the slight delay that I deliberately put
in there when it was delivering the data on the server, but it still worked perfectly well because
when I went there by navigating within the menu, that was still a client-side navigation.
But the problem is if I refresh this, now what it's going to do is go back to the
server, try and get the server to generate this page server-side and give it us back. And if
we do that, we get this horrible error. And the reason is - you can make itout basically from
what's happening in there - is that we haven't configured dependency injection. Because if you
look at how that client-side page works … so here we are in the client project, go to FetchData
and we are using HttpClient to make that work, which is done with the injection of HttpClient,
which is configured again in the client-side program. And you can see there, that's where we're
configuring it to work. But remember, this one is only run the client-side. So when that same
page gets run for the prerendering on the server, it can't find the injection. So all we've got to
do is remember to inject it in both. So we'll go to the original one and we'll copy that and then
go to the server-side program and we'll just go up here and paste that in. We then hit a slight
problem, because this HostEnvironment only makes sense when we're doing Blazor WebAssembly,
because it's telling us what's happening on the backend server. We don't have it here, so we
can't get the address. Now, there's lots of ways we could do this, but I'm going to really just
do it in the slightly hacky way. I'm going to go to the LaunchSettings and let's just get hold of that
one there, which is the one we're using, and paste that in. So in a real application, you'd obviously
have to have some way of configuring that. But for this quick demo, I'm simply going to get
rid of that and put it in there. And now we have a server-side configuration for that
injection. And so now if I run that up, obviously that all runs up okay, but now if we go
to the Fetch Data … well, that works fine, because that's the navigation within the browser. But
the important thing is now if I do the refresh, then it takes a moment because we've got that two
second delay still happening there. Obviously, we'll finally remove that. But then you see it did
something a bit odd. Let's just watch that again. So it's delivered us directly the data, but then
it does another ‘Loading…’. And then it gives us back - and in this case - some different data. And
it's different data because of that randomization. So remember, the weather forecast is random
each time. And the problem is we're requesting it twice. So we do the refresh that requests it
on the server, so gets the data from itself puts that into the completed page. But then once the
page has been downloaded, then all of our Blazor WebAssembly kicks off. And the code running in
the browser makes another request for the data. And that's why we're seeing what happens there,
that it initially loads it - we've got that first temperature of 17 - and then it loads it again.
And in this case, we've got a completely different temperature. Now, in most cases, actually,
making two requests at approximately the same time - within a second or two - to a web service
would give you back the same results, so the data won't actually change. But it's still inefficient,
once you've already got the data, to be wasting time getting it again. And this is something that
was solved only as recently as .NET 6. So this is a fairly new feature that we've got in Blazor that
can get us around that. So what we have to do is this, firstly, we go back to that _Host page that
we just created and we add another tag helper, and that is called ‘<persist-component-state>’.
So don't have to do anything special, that is just going to be there. And that just configured
the system to do a few of the things that we need. Then what we need to do is go to the actual
Blazor component that's causing the problem, so that is our FetchData.razor’. And what I'm
going to have to do is a couple of things. Firstly, I'm going to do another injection,
so ‘@inject’ and then this is a thing called ‘PersistentComponentState’ and so that's
what we'll inject. And we'll just call that ‘ApplicationState’. So that's the way that we can
do dependency injection in a Blazor component, and so that means we've now got an additional
property that's generated there called ‘ApplicationState’. The other thing I'm going
to do is I'm going to implement an interface. And that's simply going to be ‘IDisposable’,
because we've got to do a little bit of tidying up. So then let's go down to the
code. So all of that HTML can stay the same, but in the code we've got to do a few more
things. So the next thing I'm going to do, after we have that array of WeatherForecasts
that we're getting back here on the HTTP request, I'm going to put in there another class. It's
going to be a ‘private’ and this one is called ‘PersistingComponentStateSubscription’ -
and we'll see what all these do once it all fits together. And let's just call
this thing ‘persistingSubscription’. And then in our OnInitializedAsync, where we
are loading the data from the web service, this is where we've got to make a decision.
So we've got to discover whether that data has already been displayed as part of the prerender,
in which case, we can just use the existing data again - we don't need to make the HTTP request.
Or if we haven't had that data, because we're navigating to this page within the browser
rather than refreshing from the server, then we are going to need to get that data. So first
thing I'm actually going to do is put in here, a ‘private’ method, returning a ‘Task’ and
is going to be called ‘PersistForecasts’. And so this is where we are going to do
the storage that we can retrieve later on. And it's very simple, we can just use that
‘ApplicationState’ - which remember was the property that I just configured for injection
- and from that we can say ‘PersistAsJson’. So the data we persist has to be something that's in
a format that can be turned to JSON. And then we just pass in there the key that we're going to
give it just so we can identify it in future. So we'll call this ‘weatherData’ - so just any
label that you want - and then the actual data, which is going to be our ‘forecasts’. So that
will store that information there for us. Notice this is returning a Task, but we didn't have anything
async to do, so we just have to say ‘return' and then ‘Task.CompletedTask’. So that just keeps that
happy. Then the last thing we've got to do is in our ‘OnInitializedAsync’, where we're getting that
data, we then have that ‘persistingSubscription’ that I just created. And we initialise that to,
again using the ApplicationState, but this time we can say ‘RegisterOnPersisting’. And then we
give it that function that I've just written, that ‘PersistForecasts’. So that PersistForecasts’
isn't going to be called immediately - we're registering it so the ApplicationState can call
it if necessary. And then having done that, we're going to say ‘if’ and then ‘ApplicationState’
again ‘.TryTakeFromJson’. So what we're doing here is trying to see whether we've had that data
persisted. That then is a generic for the type of data that we're storing. So the thing that we
persisted – ‘forecasts’ - we know is this array of WeatherForecasts. So we'll put that in as the
generic type, so that it can deserialize it. Then the next parameter we have in there is the key. So
that was the ‘weatherData’ that we had in there. And then as an ‘out’ parameter, we can have the
actual data we're fetching out, which we'll just give the name ‘restored’. So what we're doing here
is we're basically saying, if this PersistAsJason has been called, we'll get back a true from here
and we'll be given that data that was stored. And so in that case, we can simply say ‘forecasts
= restored’. So just put that restored data into the forecasts. Otherwise, if it hasn't been
stored, then we'll just do what we did before and we'll actually get the data. One final thing we've
got to do there, which is we have to implement IDisposable, because, remember, we declared that
at the top. So we'll have a ‘public void Dispose’. And then what we have to do with
that is simply we've got to Dispose this persistingSubscription - so that is also
IDisposable. So when the component is disposed, we've got to dispose that as well. Now
let's run that up - see what happens. Obviously, we get that there quickly. We go
to the Fetch Data, that first one it did, by going to the page in the browser and
then calling the server. That's why we got the delay. But now let's see what should
have changed. Now, if I do the refresh, there we get the new data coming back. But
that's it. That is now staying where it should be. It's not doing that second call, and us seeing
different weather forecast data and all of that. Let's just debug that a little bit so we can
see what's going on. So all I'm going to do is put a break in the PersistForecasts
and a break in the OnInitializedAsync. And what we should see here, if we navigate
to that page, then we're coming in here, doing the OnInitializedAsync and when we try to
get that data, there's no data there, because we haven't done any server-side rendering. So we go
and fetch the data. And then we'll get - with a two second delay - that coming back. However, if
I click refresh here, then we hit that. And the thing to remember now is this bit of code that we
can see at the moment is running in the server. Okay, so this code is running in the server.
And as we step around in that we haven't got the data yet. So on the server, it calls its
own web service to do that. And then still on the server - and you can see we're still on the
server because we're in there at the very root of it all - it takes the data and it converts it
to JSON and persists it. And then that JSON data will be sent off with all the other stuff down
to the browser. Now, if I do another continue, now we can see it's about to restart. So now on
the browser - as you can see here in the stack, that's clearly running on the browser - now as
part of WASM. It's now that it comes into the OnInitializedAsync. It has a look to see if
it's got persisted data and it has. So that restored data is the deserialisation of the JSON
data that was serialised by the server and sent along with everything else. And so there we've
got our data displayed without the refresh. But if now we look at the page source - so this is
what was generated on that first request by the prerendering - and look down here, there we've
got the ‘Blazor-Component-State’. So that is that weather forecast data. We'd said we’d encoded
to JSON, but it's then further encoded to Base64, just to make it more compact. But that's where the
client-side component reads the data from that it got back from that persisted application state.
So quite a complicated set of steps there, but really quite an important one that you want to do.
Otherwise, you'd have lots of these situations, whenever you went to a page that made an HTTP
request back to the server, it would deliver the initial data through the prerendering and then
waste time - and possibly have differing results like we had here - by making that second call. So
quite a lot of things that we need to do there, but it's fairly straightforward. If you follow
the steps and do all the things I've done there then you will have got yourself a prerendered
Blazor WebAssembly application. So I hope that was useful. If you enjoyed it, do click like.
Do subscribe, and I'll see you next time.