Cominciamo a mettere le mani sul codice, creando la struttura della nostra Single Page Application. L’idea è quella di guidarvi nella realizzazione di una applicazione enterprise con Blazor, partendo da una classica CRUD (Create, Read, Update e Delete) di una entità e rifattorizzando man mano il codice, fino a renderlo riutilizzabile e condivisibile tra diversi progetti.

Come avete potuto leggere negli articoli precedenti, c’è una differenza di fondo tra Blazor Server e Blazor WebAssembly, quindi dove necessario faremo le poche ma dovute distinzioni tra i due. Assicuratevi di aver installato i template di Blazor, come spiegato nel primo articolo: si parte!

La struttura del progetto con Blazor Server

Creiamo un nuovo progetto specificando il nome della cartella da creare e in cui vogliamo generare il progetto:

dotnet new blazorserver -o event-manager-server

Se non preferite la riga di comando potete usare il classico wizard di creazione di un nuovo progetto in Visual Studio. In entrambi i casi sarà creato un progetto ASP.NET Core preconfigurato per l’uso di Blazor Server. Se andiamo a guardare il file Program.cs possiamo notare una classica configurazione di una applicazione ASP.NET Core:

public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
}

La configurazione di Blazor Server viene fatta nella classe Startup, dove nel metodo ConfigureServices viene aggiunto il supporto alle pagine Razor (services.AddRazorPages()) e ai componenti Blazor (services.AddServerSideBlazor()). Nel metodo Configure, vengono poi impostati gli endpoints di Blazor: l’Hub SignalR (endpoints.MapBlazorHub()) e l’indirizzo verso il quale navigare in caso di pagina non trovata (endpoints.MapFallbackToPage("/_Host")).

public class Startup
{
    [...]

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddRazorPages();
        services.AddServerSideBlazor();
        [...]
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        [...]

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapBlazorHub();
            endpoints.MapFallbackToPage("/_Host");
        });
    }
}

Una volta configurato il tutto, andiamo a definire la nostra interfaccia utente, sfruttando un concetto molto utilizzato nei framework di sviluppo front-end moderni: il Componente. L’idea è quella di prendere la nostra interfaccia e suddividerla in elementi più piccoli che chiamiamo componenti. In Blazor si parla di Blazor Components: tecnicamente sono file con estensione .razor nei quali andiamo a inserire il nostro markup, le direttive Razor e il codice C# associato al componente.

Approfondiremo in un apposito articolo questo concetto, per il momento accontentiamoci di sapere che in Blazor Server la definizione dell’interfaccia parte da una singola pagina (da cui il nome Single Page Application), chiamata solitamente _Host.html, che contiene l’HTML di base e un primo componente.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>event-manager-server</title>
    <base href="~/" />
    <link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
    <link href="css/site.css" rel="stylesheet" />
</head>
<body>
    <app>
        <component type="typeof(App)" render-mode="ServerPrerendered" />
    </app>

    <div id="blazor-error-ui">
        <environment include="Staging,Production">
            An error has occurred. This application may no longer respond until reloaded.
        </environment>
        <environment include="Development">
            An unhandled exception has occurred. See browser dev tools for details.
        </environment>
        <a href="" class="reload">Reload</a>
        <a class="dismiss">🗙</a>
    </div>

    <script src="_framework/blazor.server.js"></script>
</body>
</html>

Come potete vedere, niente di diverso da una classica pagina HTML, fatta eccezione per alcuni elementi. Subito sotto il body potete vedere l’utilizzo di un componente Blazor, denominato appe corrispondente al file App.razor presente nel progetto.

<app>
    <component type="typeof(App)" render-mode="ServerPrerendered" />
</app>

Questo componente sarà elaborato lato server (render-mode="ServerPrerendered") e sarà la radice dell’albero di componenti che costituirà la nostra interfaccia.

Sotto questo componente c’è un blocco opzionale che in caso di errore mostra un messaggio che, in fase di staging o produzione, invita a ricaricare la pagina, nel caso invece siate in fase di sviluppo, indica di verificare gli errori nella console del browser.

<div id="blazor-error-ui">
    <environment include="Staging,Production">
        An error has occurred. This application may no longer respond until reloaded.
    </environment>
    <environment include="Development">
        An unhandled exception has occurred. See browser dev tools for details.
    </environment>
    <a href="" class="reload">Reload</a>
    <a class="dismiss">🗙</a>
</div>

Infine la parte più importante:

<script src="_framework/blazor.server.js"></script>

Questo link scarica nel browser la componente client di Blazor Server, responsibile della comunicazione con il server mediante SignalR.

La struttura del progetto con Blazor WebAssembly

Per Blazor WebAssembly dobbiamo solo modificare il nome del template e il nome del progetto:

dotnet new blazorwasm -o event-manager-wasm

Anche in questo caso potete utilizzare il wizard di Visual Studio se non siete amanti della riga di comando. La struttura base del progetto è molto diversa dalla precedente, proprio perchè in questo caso non c’è elaborazione server, e il risultato sarà servito come un insieme di file statici.

Notiamo subito l’assenza del file Startup.cs, che è stato rimosso nell’ultimo rilascio dato che non aveva molto senso in questo contesto: possiamo fare tutto il necessario per avviare il webserver che ci fornisce staticamente i file compilati direttamente dal Program.cs.

public class Program
{
    public static async Task Main(string[] args)
    {
        var builder = WebAssemblyHostBuilder.CreateDefault(args);
        builder.RootComponents.Add<App>("app");
        await builder.Build().RunAsync();
    }
}

Nel metodo Maindella classe Program andiamo a instanziare il builder della configurazione predefinita di hosting. L’oggetto che ci viene restituito è di tipo WebAssemblyHostBuilder, che ci espone la collection RootComponents: un insieme di oggetti composti dalle coppie Compontente/selettore. Nel nostro caso aggiungiamo un singolo componente radice (sì, è possibile averne più di uno, ma ne parleremo nei prossimi articoli), il componente App, che identificheremo nel markup con il selettore <app></app>. Infine invochiamo il metodo Build(), di tipo WebAssemblyHost, che ci mette a disposizione la chiamata asincrona RunAsync(), che avvia il nostro server e lo mette in attesa delle richieste.

La nostra Single Page, in questo caso denominata index.html, si trova nella cartella wwwroot perchè, come già detto, in questa versione di Blazor andiamo a servire risorse statiche, dato che non avviene elaborazione server-side, come invece accade con il file _Host.cshtml di Blazor Server. La struttura è molto simile alla precedente:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />
    <title>event-manager-wasm</title>
    <base href="/" />
    <link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
    <link href="css/site.css" rel="stylesheet" />
</head>
<body>
    <app>Loading...</app>
    <div id="blazor-error-ui">
        An unhandled error has occurred.
        <a href="" class="reload">Reload</a>
        <a class="dismiss">🗙</a>
    </div>
    <script src="_framework/blazor.webassembly.js"></script>
</body>
</html>

In questo caso il rendering del primo componente, che conterrà la nostra interfaccia, avviene semplicemente con il selettore specificato in fase di configurazione. Lo script che carichiamo prima di chiudere il <body> (<script src="_framework/blazor.webassembly.js"></script>) è quello di avvio e configurazione della versione WebAssembly di Mono (dotnet.wasm), che caricherà le nostre DLL e avvierà il rendering client-side.

Facciamo un po’ di pulizia

Adesso che abbiamo la struttura di base, facciamo qualche piccola modifica e un po’ di pulizia. Cominciamo con il cambiare il <title> della nostra Single Page (_Host.cshtml in Blazor Server e index.html in Blazor WebAssembly) in “Event Manager” ed eliminiamo dalla cartella Pages i file Counter.razor e FetchData.razor.

Nella cartella Shared troviamo il file NavMenu.razor, il componente che rappresenta il menu dell’applicazione. Apriamolo ed eliminiamo le voci di menu Counter e FetchData, che puntano ai file che abbiamo eliminato. Sostituiamo anche il titolo del menu in “Event Manager”. Il codice risultante è il seguente per entrambi i progetti:

<div class="top-row pl-4 navbar navbar-dark">
    <a class="navbar-brand" href="">Event Manager</a>
    <button class="navbar-toggler" @onclick="ToggleNavMenu">
        <span class="navbar-toggler-icon"></span>
    </button>
</div>
<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
    <ul class="nav flex-column">
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
                <span class="oi oi-home" aria-hidden="true"></span> Home
            </NavLink>
        </li>
    </ul>
</div>

@code {
    ...
}

Dalla cartella Pages aprimo il file Index.razor e sostituiamo il contenuto con un messaggio di benvenuto:

@page "/"
<h1>Event Manager</h1>
<p>Benvenuti nella Single Page Application scritta in Blazor per la gestione degli eventi.</p>
<p>Selezionare dal menu laterale l'opzione desiderata.</p>

Infine, dal progetto Blazor Server eliminiamo la cartella Data che contiene un servizio con dei dati fake, utilizzato dalla pagina FetchData per mostrare l’utilizzo di dati dal back-end. Nel file Startup.cs, andiamo di conseguenza ad eliminare la registrazione della classe WeatherForecastService. Stesso lavoro di pulizia va fatto anche nel progetto WebAssembly, ma in questo caso ci basta eliminare la cartella wwwroot > sample-data.

Avviamo il progetto (dotnet run da riga di comando o F5 da Visual Studio) e verifichiamo il risultato del nostro lavoro:

Struttura base progetto SPA con Blazor

La tipica bellezza estetica di cui è capace uno sviluppatore… ma funziona! Trovate il codice sorgente, organizzato in branch per ogni articolo della serie, qui.

Conclusioni

Per oggi ci fermiamo qui, nel prossimo articolo vedremo nel dettaglio che cos’è un componente, sia dal punto di vista logico che dal punto di vista tecnico, e come questo concetto sia il mattoncino con cui costruire la user interface della nostra Single Page Application.