Last time I started covering the design and code for a Blazor setup plugin. This time I’ll wrap that up and walk through how to use the plugin in a Blazor application.
We’ve already looked at the code that starts the plugin up. We’ve also looked at the code that actually creates the setup page. The only things left are the SetupChangedAlert
type and the code that listens for that event and restarts the host whenever the alert is raised. Let’s start by looking at the SetupChangedAlert
class:
public class SetupChangedAlert : AlertEventBase { public static CancellationTokenSource TokenSource { get; set; } = new CancellationTokenSource(); protected override void OnInvoke( params object[] args ) { // Cancel the token source. TokenSource?.Cancel(); // Give the base class a chance. base.OnInvoke(args); } }
The class derives from the AlertEventBase
class. That type is part of my CG.Alerts library, and is the base for all alert type events. If the term Alert is new to you, try reading this article HERE or HERE.
Looking at the source, we see that we override the OnInvoke
method. That method is called whenever the alert is raised. If you’ll recall, in the last article, we raised the SetupChangedAlert
alert whenever the user pressed the submit button, on the generated, to save any changes to the underling setup file.
The only thing we really do in this alert is to cancel the token source object in the TokenSource
property. We’ll see that that does, next …
The last missing piece to pull everything together is some code to restart the Blazor host whenever the user changes the setup. To do that, requires code that runs within the Blazor application itself, not in our plugin. Luckily, there’s really no hard and fast rule that says we can have compile time references to a plugin library, from the calling application. So, we can deal with this by creating a quick class utility in our plugin, that we then call from the Blazor application. Let’s go look at that code now:
public static class HostHelper { public static async Task RunAsSetupObserverAsync( Func<IHostBuilder> builderDelegate, CancellationToken cancellationToken = default ) { // Validate the parameters before attempting to use them. Guard.Instance().ThrowIfNull(builderDelegate, nameof(builderDelegate)); // We'll be notified whenever the setup is changed via the alert service, // which will handle cancelling a token for us. We'll link to that token // here so we can loop and restart the host if a setup change occures, or, // we can exit normally if the host itself exited. // Loop until the incoming token is cancelled. while (false == cancellationToken.IsCancellationRequested) { // Create a linked token, which is what the host will use. var hostToken = CancellationTokenSource.CreateLinkedTokenSource( SetupChangedAlert.TokenSource.Token, cancellationToken ); // Create the host. var host = builderDelegate().Build(); // Get a logger. var logger = host.Services.GetRequiredService<ILogger<IHost>>(); // Tell the world what we are doing. logger.LogInformation( $"~~~~~ Starting the host. ~~~~~" ); // Run the host. await host.RunAsync( hostToken.Token ).ConfigureAwait(false); // Did the host stop naturally? if (!SetupChangedAlert.TokenSource.IsCancellationRequested) { // Tell the world what we are doing. logger.LogInformation( $"~~~~~ Exiting the process. ~~~~~" ); // If we get here then the host stopped on it's own so we // want to exit gracefully. break; } // Tell the world what we are doing. logger.LogInformation( $"~~~~~ Restarting the host. ~~~~~" ); // If we get here then the host stopped due to a setup change // so we want to loop and restart it. // Reset the token source. SetupChangedAlert.TokenSource = new CancellationTokenSource(); } } }
I wanted to make RunAsSetupObserverAsync
an extension method, but, I couldn’t think of a good type to hang it from. As a result, I had to expose the HostHelper
type, outside the plugin. There’s no point to me saying that, other than to complain about it … Grumble, grumble …
Anyway, the first thing we do, inside RunAsSetupObserverAsync
, is validate the incoming parameter, which in this case, is a delegate that returns an IHostBuilder
instance. I originally wanted to hang this method off the IHostBuilder
type, and then create and run the IHost
instance(s) inside my method. That wouldn’t work though because – apparently – the host builder itself can’t be reused. In other words, once the builder is used to create an IHost
instance, it then needs to be garbage collected. Personally, I think that’s a design problem, in that I think a builder should be able to create as many instances as needed. But, that’s just my opinion. I’ll let Microsoft deal with that little issue.
The reality is, we need a new builder each time we create an IHost
and run it. Because of that, we have the incoming delegate that returns an IHostBuilder
instance.
The next thing we do is enter a loop where we keep checking the incoming cancellation token. That way, we don’t restart the host if it’s trying to stop on it’s own.
Inside that loop, we create a linked cancellation token source, using the incoming cancellation token and the TokenSource
property of the SetupChangedEventArgs
class. That linked token source will be cancelled if either of the other two tokens are cancelled. We use that linked cancellation token in our call to RunAsync
, on the IHost
instance. That means the host will stop if the linked token is cancelled, which will happen if either of the other two tokens are cancelled.
After creating the linked cancellation token, we call the delegate to get an IHostBuilder
instance, which we then use to create the IHost
instance. From there, we call RunAsync
on the IHost
, to run the host, which runs the Blazor application.
After the host exits, we check the token source inside the TokenSource
property of the SetupChangedAlert
class. If that was NOT the source of the cancellation, we exit the process – since the only other thing that could have stopped the process is the incoming cancellation token that we linked, earlier. On the other hand, if the host DID stop because of a setup change, we loop around and do everything I just mentioned over again.
The end result of this is, the host restarts whenever the SetupChangedAlert
alert is raised. But, only if the developer uses the RunAsSetupObserverAsync
method, in their project. So, for instance, if you don’t care anything about restarting the host (for whatever reason), you could just not use the RunAsSetupObserverAsync
method and handle restarts yourself.
Assuming you did want my code to handle the restarts, you could just do this in your code:
public class Program { public static async Task Main(string[] args) { // Run the host as a setup observer. await HostHelper.RunAsSetupObserverAsync( () => CreateHostBuilder(args) ); } public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); }); }
Where most of that code is already generated for any Blazor application, including the CreateHostBuilder
method. The only thing we’ve modified is how the CreateHostBuilder
method is called, which we now do inside the RunAsSetupObserverAsync
method, as a delegate.
So that’s about it for the setup plugin. There is a working sample application as part of the GITHUB repository, HERE.
Quickly, the sample project is a bare bones Blazor application created with the standard Blazor templates, in Visual Studio. I then added the CG.Blazor.Setup library, as a reference. In addition, since I wanted a MudBlazor UI with fluent style validations, I also added references to:
- MudBlazor
- CG.Blazor.Forms._MudBlazor
- CG.Blazor.Forms._FluentValidations
I then added this section to the appSettings.json:
{ "Plugins": { "Modules": [ { "AssemblyNameOrPath": "CG.Blazor.Setup", "Routed": true, "ModelType": "CG.Blazor.Setup.QuickStart.ViewModels.SetupVM, CG.Blazor.Setup.QuickStart" } ] } }
That JSON directs the application to load the CG.Blazor.Setup library, as a plugin. It also tells plugin framework that the plugin requires routing support (for the setup page). Finally, we also use the ModelType
field to specify our POCO model type, for the setup form.
There are additional steps required to integrate plugins with the Blazor application. Those are documented in articles HERE, HERE, and HERE so I won’t go over them again, today. Those changes are not specific to the setup plugin, they are required to get any plugin to work, in Blazor.
The only things we have yet to look at is the POCO class itself, and the associated validation class. Let’s do that next time.
The source code for the CG.Blazor.Setup project is available, for free, HERE.
The CG.Blazor.Setup NUGET package is available, for free, HERE.
Photo by Womanizer Toys on Unsplash