Riprendiamo a parlare di aspetti avanzati di Blazor, affrontando uno degli strumenti più interessanti che abbiamo a disposizione per il riutilizzo del codice che scriviamo. Abbiamo già visto negli articoli precedenti che possiamo creare un libreria di classi per condividere oggetti di scambio tra back-end e front-end, in questo articolo vedremo invece come poter condividere i componenti che realizziamo per la nostra interfaccia, in modo da poterli riutilizzare in applicazioni diverse.

Creazione della libreria

Partiamo dalla creazione di una cartella LibreriaComponenti, all’interno della quale, da riga di comando, creaiamo una solution vuota, aggiungiamo un progetto Blazor WASM per testare la nostra libreria e la aggiungiamo alla solution:

mkdir LibreriaComponenti
cd LibreriaComponenti
dotnet new sln
dotnet new blazorwasm -o LibreriaComponenti.Host.BlazorWASM
dotnet sln add ./LibreriaComponenti.Host.BlazorWASM 

Se lanciate il comando dotnet new, la CLI vi mostrerà i template disponibili, tra i quali ne troverete uno di cui probabilmente non vi siete mai accorti: razorclasslib. Si tratta di una libreria di classi pensata per Razor Pages, su cui come sappiamo si basa il framework Blazor. Questo template è proprio quello di cui abbiamo bisogno per i nostri scopi, non ci resta quindi che creare la libreria, aggiungerla alla solution e creare un riferimento ad essa dal progetto Blazor WASM:

dotnet new razorclasslib -o LibreriaComponenti
dotnet sln add ./LibreriaComponenti
cd LibreriaComponenti.Host.BlazorWASM 
dotnet add reference ../LibreriaComponenti

Ovviamente potremmo fare tutto questo da Visual Studio con i classici wizard, ma la riga di comando, oltre ad avere sempre il suo fascino, è cross-platform! Apriamo il progetto con Visual Studio Code (o Visual Studio) e diamo una occhiata alla struttura generata:

Struttura progetto libreria componenti Blazor

Il template crea per noi un componente di esempio denominato Component1 e aggiunge anche la cartella wwwroot per contenere gli eventuali elementi statici a corredo dei nostri componenti. Nel file ExampleJsInterop.cs possiamo anche notare un metodo statico che richiama una funzione JavaScript presente nel file exampleJSInterop.js, come esempio di interoperabilità tra .NET e JavaScript (potete approfondire qui e qui).

Usiamo la libreria

Possiamo quindi aggiungere al file _Imports.razor del progetto Host la using alla libreria (@using LibreriaComponenti) e utilizzare il componente di esempio nella pagina Index.razor:

@page "/"
<Component1></Component1>

ed ecco il risultato:

Uso del componente nel progetto Host

Proviamo a modificare il componente aggiungendo un pulsante che invoca la funzione JavaScript. Aggiungiamo al file _Imports.razor della libreria il riferimento alla libreria JSInterop:

@using Microsoft.AspNetCore.Components.Web
@using Microsoft.JSInterop

In questo modo possiamo iniettare il JSRuntime nel componente e usarlo sul pulsante:

@inject IJSRuntime JSRuntime;
<div class="my-component">
    This Blazor component is defined in the <strong>LibreriaComponenti</strong> package.

    <button @onclick="@(() => ExampleJsInterop.Prompt(JSRuntime, "Come ti chiami?"))">
        Chiama funzione JavaScript
    </button>
</div>

Eseguendo il progetto vedremo il pulsante sull’interfaccia, ma cliccandoci non funzionerà! Se poi guardiamo meglio la definizione del componente, possiamo anche notare l’uso di una classe CSS my-component, definita nel file styles.css della cartella wwwroot, che dovrebbe mostrare un sfondo a righe trasversali sul componente.

Questo malfunzionamento è dovuto al fatto che sia il file CSS che lo script exampleJsInterop.js, non sono stati inclusi nella index.html del progetto host, quindi non sono stati caricati. Per poterlo fare abbiamo bisogno di conoscere il percorso di questi file una volta che la DLL è stata compilata. I file statici vengono esposti dalle librerie Razor secondo la convenzione _content/{nome libreria}/{nome file}, quindi nel nostro caso la index.html diventa:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
    <title>LibreriaComponenti.Host.BlazorWASM</title>
    <base href="/" />
    <link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
    <link href="css/app.css" rel="stylesheet" />

    <link href="_content/LibreriaComponenti/styles.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>

    <script src="_content/LibreriaComponenti/exampleJsInterop.js"></script>
</body>
</html>

Attenzione al nome della libreria: è case sensitive, quindi maiuscole e minuscole nel nome fanno differenza. Ecco il risultato:

Integrazione dei file statici della libreria

Usare una libreria con Blazor WASM e Blazor WebAssembly

Un uso interessante delle librerie di componenti in questo periodo di attesa del rilascio di Blazor WASM, è l’uso di un progetto condiviso tra un host WebAssembly e un host Blazor Server. Uno scenario del genere, oltre a permetterci di debuggare comodamente in Blazor Server ed eseguire gli stessi componenti in Blazor WASM, dove il debug è ancora ostico, ci permette anche di evidenziare i punti differenzianti tra i due modelli di hosting.

Copiamo le cartelle Pages e Shared dal progetto WASM al progetto della libreria, aggiungendo alle dipendenze del progetto la libreria System.Net.Http.Json. Il file LibreriaComponenti.csproj diventa il seguente:

<Project Sdk="Microsoft.NET.Sdk.Razor">

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
    <RazorLangVersion>3.0</RazorLangVersion>
  </PropertyGroup>


  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Components" Version="3.1.2" />
    <PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="3.1.2" />
    <PackageReference Include="System.Net.Http.Json" Version="3.2.0-rc1.20217.1" />
  </ItemGroup>

</Project>

Modifichiamo anche il file _Imports.razor della libreria aggiungendo i namespace necessari:

@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.JSInterop
@using System.Net.Http
@using System.Net.Http.Json
@using LibreriaComponenti.Shared

Spostiamo anche il componente App.razor nella libreria, modificando la configurazione del Router per caricare le pagine dall’assembly della libreria, modificando il valore dell’attributo AppAssembly da "@typeof(Program).Assembly" a "@typeof(MainLayout).Assembly".

<Router AppAssembly="@typeof(MainLayout).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>
        </LayoutView>
    </NotFound>
</Router>

Rilanciando il progetto vedrete che tutto continua a funzionare correttamente. Creiamo adesso un progetto Blazor Server, aggiungiamolo alla solution e aggiungiamo la reference alla libreria condivisa:

dotnet new blazorserver -o LibreriaComponenti.Host.BlazorServer
dotnet sln add ./LibreriaComponenti.Host.BlazorServer 
cd LibreriaComponenti.Host.BlazorServer
dotnet add reference ../LibreriaComponenti

Eliminiamo la cartella Shared e i file nella cartella Pages a meno del file Host.cshtml, che dobbiamo modificare per aggiungere i riferimenti ai file statici:

@page "/"
@namespace LibreriaComponenti.Host.BlazorServer.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@{
    Layout = null;
}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>LibreriaComponenti.Host.BlazorServer</title>
    <base href="~/" />
    <link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
    <link href="css/site.css" rel="stylesheet" />

    <link href="_content/LibreriaComponenti/styles.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>

    <script src="_content/LibreriaComponenti/exampleJsInterop.js"></script>
</body>
</html>

Ricordiamoci di aggiungere al file _Imports.razor il namespace della libreria (@using LibreriaComponenti) e facciamo una piccola fix al Program.cs dove il metodo CreateDefaultBuilder non riesce a trovare l’oggetto Host perchè il namespace del nostro progetto contiene proprio la parola Host:

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

Come possiamo vedere ci basta esplicitare il corretto namespace della libreria Microsoft. Avviando il progetto server vedrete che apparentemente tutto funziona… Trovate il codice illustrato sull’account GitHub della community.

Conclusioni

Abbiamo visto come integrare una libreria di componenti in progetti Blazor Server e Blazor WebAssembly, utilizzando i template messi a disposizione da Microsoft. Abbiamo anche visto come sfruttare una libreria per condividere il codice tra i due modelli di hosting messi a disposizione dal framework, ma non tutto sta funzionando correttamente come potrebbe sembrare. Infatti se provate ad accedere alla pagina FetchData, questa non funzionerà correttamente in Blazor Server a causa dell’uso del client HTTP. Nel prossimo articolo vedremo come risolvere questo problema sfruttando la dependency injection di .NET Core.