C# in de frontend? Bouw een web app met .NET Blazor

Microsoft investeert de laatste jaren veel in cross-platform, open source-software. Zo zagen bijvoorbeeld .NET Standard en .NET MAUI het licht. Maar er is nu ook een frontend-framework: Blazor. In dit blog laat ik je zien hoe dat werkt.

Blazor, dat uitkwam in september 2019, is bedoeld als Microsoft-alternatief voor JavaScript-frameworks als React, Angular en Vue. Het is een feature van ASP.NET en maakt deel uit van .NET Core 3.0. 

Op dit moment zijn er 2 varianten van Blazor: Blazor Server en WebAssembly. 

Auteur:

Kenniscentrum Cloud Software Engineering

Cloud Software Engineer

<Verschillende hostingmodellen >

Het verschil tussen die twee zit in het hostingmodel. Blazor Server draait aan de serverkant en stuurt schermen en informatie via een websocket (SignalR)-verbinding naar de browser. Zo gebruik je de rekenkracht van de server volledig en krijg je een webapplicatie die snel opstart. 

Bij Blazor WebAssembly draait de applicatie volledig in de browser. Zodra de gebruiker de pagina aanvraagt, wordt de hele applicatie gedownload. Data haalt de app daarna meestal uit een web-API. Je gebruikt dus de rekenkracht van de computer of het mobiele apparaat waar je applicatie op draait en na de eerste download stuur je alleen nog maar data heen en weer. Dat maakt een WebAssembly-app snel en soepel in gebruik. 

In browsers die geen WebAssembly ondersteunen, zorgt Blazor Server dat je applicatie toch werkt. 

<Volledig in C#: sneller ontwikkelen, sneller leren en sneller collega’s vinden >

Het aan elkaar knopen van componenten in verschillende talen kost altijd extra tijd. Bij Blazor staan frontend en backend op dezelfde stack, dus heb je daar geen last van en kun je je apps sneller bouwen. Het is ook makkelijker jezélf te ontwikkelen: als je werkt in verschillende talen en stacks heb je veel meer kennis nodig om dezelfde functionaliteit te kunnen bouwen. Met Blazor concentreer je je op één taal, groei je dus sneller als developer en bouw je sneller complexe applicaties. En is het tijd om je team uit te breiden? Dan hoef je niet op zoek naar iemand die allerlei verschillende frameworks en talen kent.

Het is bovendien gewoon heerlijk om in je Blazorfrontend te kunnen werken met objecten, modellen en alle andere krachten van C# en .NET die je al kent. Ook het debuggen van Blazor doe je gewoon op de vertrouwde manier, in het ASP.NET-framework. Er zijn daarnaast veel standaardcomponenten die je kunt gebruiken of uitbreiden. 

<Een Blazor Server-project opzetten >

Laten we aan het werk gaan. Als je een Blazor Server-project aanmaakt zie je de volgende structuur:

  • In de folder ‘Data’ zitten classes die de basis vormen voor je applicatiedata. 
  • De Pages-folder bevat alle pagina’s van je applicatie 
  • In de Shared-folder staan alle gedeelde paginacomponenten, zoals bijvoorbeeld de MainLayout (de gedeelde layout voor de meeste pagina’s). 
  •  _Imports.Razor is relatief nieuw in .NET Core-projecten. Dankzij dit bestand hoef je niet meer op elke pagina alle usings toe te voegen. Veelgebruikte usings zet je gewoon in dit bestand. Dan voegt de compiler ze automatisch toe aan de verschillende pagina’s. 
  • De App.Razor bevat de Blazor Router van de applicatie. Die zorgt ervoor dat de app de juiste pagina laadt. 
  • En natuurlijk het startpunt van elke .NET applicatie: Program.cs. Hier laad je de basisconfiguratie in en zet je alle instellingen goed voor de rest van de app. 

<Pagina’s en componenten >

Een Blazor-app lijkt in zekere zin erg op een app in React of Angular. Alle pagina’s zijn bijvoorbeeld componenten. Alleen hebben ze meer attributen dan in andere frameworks. Een pagina heeft bijvoorbeeld altijd minimaal één component en één page directive. Het opbouwen van een pagina uit componenten heeft als voordeel dat je functionaliteit makkelijk kunt hergebruiken. 

<Routing en navigatie in Blazor >

Een Blazor-app is in feite een PWA (progressive web app). Het lijkt dus alsof de app maar één pagina heeft, maar dat hoeft niet zo te zijn. Zowel een component als een pagina kan een page directive hebben. De pagina of component wordt dan automatisch toegevoegd aan de routing table en de gebruiker kan ernaartoe navigeren. Een voorbeeld van een page directive zie je hieronder. Deze zorgt dat de waarde ‘/HelloWorld’ wordt toegevoegd aan de routing table met daarbij een verwijzing naar de huidige component of pagina: 

@page “/HelloWorld” 

Houd er wel rekening mee dat Blazor nog een jong framework is. De razor pages missen dus nog wat van de meer geavanceerde route constraints uit ASP.NET MVC projecten. 

<Blazor Lifecycles >

Een groot en belangrijk concept, dat we bijvoorbeeld ook kennen in React, is de lifecycle van een component of pagina’s: alle stappen die nodig zijn om een component te laden, te tekenen en na gebruik weer weg te gooien. 

@*Add default route to routing table*@
@page “/”

@*Inject the Blazor Javascript interop file which contains the code to connect C# to JS and JS to C#.*@
@inject IJSRuntime JS

@*Title of the page*@
<PageTitle>Javascript interop example</PageTitle>

<h1>Hello, world!</h1>

Welcome to your new app.

 

<p><button @onclick=“TriggerPrompt”>Trigger browser window prompt</button></p>
<p>@result</p>

@code
{

    private IJSObjectReference? module;
    private string? result;

    // OnAfterRenderAsync we can call the Javascript interop
    // Js interop is not available in earlyer stages only after rendering of the component its possible to
    // call js interopt and its usable functions.
    protected override async Task OnAfterRenderAsync(bool firstRender)
    {    
        if ( firstRender )
        {
                // import the scripts.js file with javascript isolation
                module = await JS.InvokeAsync<IJSObjectReference>(“import”, “./scripts.js”);
        }
    }
    private async Task TriggerPrompt()
    {
        result = await Prompt(“Provide some text”);
    }
    // Invoke the JS interop module defined in the scripts.js file
    public async ValueTask <string?> Prompt(string message ) =>
    module is not null ?
                await module.InvokeAsync<string>(“showPrompt”, message) : null;
}

Component lifecycles zijn bedoeld om op een bepaald moment in de lifecycle een actie uit te voeren, zoals het ophalen van data of het tonen van een laadanimatie. Je kunt tijdens een actie ook de properties op je pagina nalopen en logica baseren op die waarden. Voor alles wat je wilt doen is er wel een stap in de lifecycle die je kunt gebruiken. 

@page “/”

<PageTitle>Index</PageTitle>

<h1>Hello, world!</h1>

Welcome to your new app.

<SurveyPrompt Title=”How is Blazor working for you?”></SurveyPrompt>

 

@code
{
    // all protected overrideable functions lined up below here.    

 

    // OnParameterSet will be called when first landing on an component or a change has happend in the parameters and has been detected.
    // For example a query parameter in the url has changed this function will be fired.
    protected override void OnParametersSet()
    {
        // always call the base class method when working with an overrideable method.
        // You can choose to not use it but need to be aware that the code inside this base method will not be fired.
        base.OnParametersSet();

        // After the base class create your own code that works with this lifecycle.
        // In our case on parameter set(Or change).
    }
    protected override void OnInitialized () => base.OnInitialized ();

    protected override async Task OnInitializedAsync() => await base.OnInitializeAsync();

    // Every component has a synchronous and asynchronous method that can be overridden.
    protected override async Task OnParametersSetAsync()
    {
        await base.OnParametersSetAsync();
    }

    // The firstRender property can be used to run a code block when Blazor sees the first render happen.
    // Note here that the firstRender will only be true on a true first render.
    // Rerendering a component will not trigger this boolean to be true.
    protected override void OnAfterRender(bool firstRender) => base.OnAfterRender(firstRender);
    protected override async Task OnAfterRenderAsync(bool firstRender) => await base.OnAfterRenderAsync(firstRender);
}

Hierboven zie je hoe je in de OnInitializedAsync (regel 27 – 35) een call kunt doen naar een service (regel 5) die weerdata ophaalt. 

Je kunt in Blazor heel makkelijk controleren of een variabele aan een conditie voldoet. Het if-statement in regel 14-20 controleert bijvoorbeeld of de boolean “ShowWeatherForecasts” waar is. Verandert een variabele? Dan detecteert het framework zelf of de component opnieuw getekend moet worden. Je kunt een component uiteraard ook handmatig verversen. 

<Javascript Interop >

JavaScript bestaat langer dan Blazor en er zijn veel JavaScript-frameworks en libraries die veel functionaliteit bieden. Die kun je gebruiken door ze vanuit C# aan te roepen met Microsofts JavaScript Interop. 

Je doet dat via de JSRuntime, die Blazor meestuurt bij het starten van de applicatie. Zo krijg je met minimale inspanning JavaScript-ondersteuning in je Blazor-app. Meer informatie vind je in de documentatie van Microsoft. 

Hieronder zie je een eenvoudig voorbeeld van hoe je JavaScript Interop gebruikt. 

@page “/”
@*Dependency injected data service*@
@inject WeatherForecastService WeatherForecastService

<PageTitle>Lifecycle example loading data before rendering component and showing data when data is available.</PageTitle>

<h1>Hello, world!</h1>

Welcome to your new app.

@if ( ShowWeatherForecasts )
{
    foreach (var forecast in AllWeatherForecastsForDate )
   {
        <p>@forecast.Date @forecast.TemperatureC</p>
   }
}

@code
{
    public bool ShowWeatherForecasts { get; set; }
    public WeatherForecast[] AllWeatherForecastsForDate { getset;}

    protected override async Task OnInitializedAsync ();
    {
        await base.OnInitializedAsync ();

        ShowWeatherForecasts = false;
        // on Initialize get weathercast data from di service
        AllWeatherForecastsForDate = await WeatherForecastService.GetForecastAsync(DateTime.Now);

        ShowWeatherForecasts = AllWeatherForecastsForDate?.Any() ?? false;
    }
}

Eerst voegen we de JSRuntime toe (regel 5). In de OnAfterRenderAsync laden we vervolgens ons JavaScript-bestand in als module. Volgens het principe van ‘JavaScript isolation’ isoleert Blazor jouw JavaScript naar kleine, aparte bestanden die je kunt laden als modules. 

Je kunt alleen JavaScript aanroepen als je component of pagina geladen is. OnAfterRenderAsync is dus de eerste mogelijkheid om JavaScript-calls te doen in Blazor Server. 

function showPrompt ( message ) {

    return prompt( message ‘Type anything here’);
}

De inhoud van scripts.js 

Zoals je in regel 14 van de code kunt zien, tekent de HTML een knop. Druk je daarop, dan verschijnt de (JavaScript) alertbox die om wat tekst vraagt (regel 38 – 40 en 33 – 36). 

Hieronder zie je het resultaat zodra je op de knop drukt en een berichtje invoert.

<De technologie van de toekomst >

Zoals je kunt zien biedt Blazor heel veel mogelijkheden voor je webprojecten. En ik verwacht dat het framework functioneel nog veel verder zal groeien. Dankzij Blazor kunnen we nu hele projecten in C# bouwen, zonder een JavaScript-framework en packages te moeten kiezen. Heb je JavaScript-functionaliteit die je echt niet kunt missen? Dan kun je hem dus via JavaScript Interop alsnog gebruiken en combineren met .NET-packages.

Iets waar je bij Blazor goed op moet letten zijn de nuances van het hostingmodel. De SignalR-verbinding kan een probleem zijn als je app veel bandbreedte gebruikt. Je hebt daardoor extra capaciteit nodig om het dataverkeer te regelen. Niet onoplosbaar, maar het kost wel extra moeite.

Ook over de keuze tussen Blazor Server en Blazor WebAssembly moet je goed nadenken. Ontwikkel je ook voor oudere browsers? Dan is Blazor Server waarschijnlijk de beste optie. Verwacht je veel gebruikers en wil je het dataverkeer beperken? Dan is WebAssembly een goede keus.

Maar de grote kracht van Blazor is dat software engineers in .NET frontend én backend kunnen ontwikkelen, zonder kennis van allerlei verschillende talen en frameworks. Met een klein scala aan techniek en kennis komt een team daardoor al snel tot een werkend product. En ook een beginnende .NET-ontwikkelaar kan gemakkelijk in Blazor ontwikkelen.

Blazor is nog jong, maar ik ben enorm enthousiast en ik hoop van harte dat dit de technologie van de toekomst wordt.