Articoli

Creare una SPA: pagine e navigazione

da

Se vogliamo realizzare una Single Page Application è fondamentale capire come viene gestita la navigazione tra le varie pagine dell’applicazione, pur avendone fisicamente solo una, la index.html nel caso di Blazor WebAssembly o il file _Host.cshtml in Blazor Server.

Nello sviluppo Desktop, ad esempio con Windows Forms o WPF, eravamo abituati a ragionare in termini di finestre o form: quando l’utente cliccava su una voce di menu, veniva aperta la finestra corrispondente. Nel modello web invece ragioniamo in termini di pagine, quindi alla selezione di una voce di menu dell’applicazione navighiamo verso una specifica pagina. Il concetto di pagina è quindi strettamente legato a quello di navigazione, tanto da poter dire che la nostra applicazione è un insieme di pagine collegate tra loro.

In ogni pagina ci sono sezioni specifiche e sezioni che invece restano invariate, come ad esempio il menu principale, il footer o una barra laterale, quindi viene utilizzata spesso una qualche strategia per evitare di ripetere le parti comuni. In ASP.NET WebForms utilizzavamo la famosa Master Page, in ASP.NET MVC il corrispondente Layout di Razor. In ogni caso una navigazione richiedeva una richiesta al server, che ci restituiva la nuova pagina, costruita dinamicamente.

In una Single Page Application la navigazione non avviene lato server (abbiamo una sola pagina, quindi continuerebbe a restituirci la stessa), ma lato client, dinamicamente nel browser. Questo è possibile grazie a uno speciale componente della nostra applicazione che si occupa di gestire il meccanismo di routing.

Ma quindi che cos’è una Pagina?

In Blazor, così come nella maggior parte dei framework di front-end per Single Page Application, una pagina è un componente destinatario di una navigazione. Questo significa che da qualche parte c’è una configurazione che mette in relazione due elementi principali: il path a cui vogliamo arrivare e il componente che vogliamo visualizzare. In framework come Angular questa configurazione va esplicitata nel modulo di routing, in Blazor è molto più semplice: basta aggiungere nel componente desiderato una direttiva @page "<path>" per spiegare al gestore della navigazione che, quando l’utente vuole navigare verso quel percorso, deve visualizzare il componente che contiene la direttiva corrispondente.

Per distinguere i componenti generici dalle Blazor Pages, viene utilizzata una cartella Pages. Nel nostro progetto al momento ne abbiamo una sola, la Index.razor, che risponde al percorso “/”:

@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>

Per completezza, nel template di Blazor Server avete una ulteriore pagina, Error.razor, utilizzata nel caso di errori non gestiti lato server, come configurato nel metodo Configure() della classe Startup (app.UseExceptionHandler("/Error");). Questa pagina non c’è in Blazor WebAssembly perchè essendo l’elaborazione client-side, non ci possono essere errori non gestiti lato server, dato che non c’è un lato server nell’elaborazione dell’interfaccia.

Proviamo a fare la nostra prima pagina, creandone una specifica per la CRUD degli eventi. Nella cartella Pages andiamo a creare un file Eventi.razor, aggiungiamo la direttiva page in modo che questo componente risponda al path “/eventi” e utilizziamo qui la nostra lista eventi invece che nella index:

@page "/eventi"

<ListaEventi ListaElementi="ListaEventi" OnElimina="EliminaEvento" />
@code {
    List<ElementoListaEventi> ListaEventi { get; set; } 
        = new List<ElementoListaEventi>
            {
                new ElementoListaEventi() { Id = 1, Nome="DevDay Benevento - Blazor", Localita="Benvento", Data = new DateTime(2020, 2,8)},
                new ElementoListaEventi() { Id = 2, Nome="DotNetSide Bari - Blazor", Localita="Bari", Data = new DateTime(2020, 2, 21)}
            };

    public void EliminaEvento(ElementoListaEventi evento) 
    {
        this.ListaEventi.Remove(evento);
    }
}

La parte di Blazor che gestisce la navigazione adesso sa che se l’utente vuole navigare verso il percorso “/eventi” dovrà visualizzare questo componente, la domanda quindi diventa come fa a saperlo e in che punto della nostra applicazione verrà visualizzato. Per poter rispondere andiamo ad analizzare il funzionamento base del componente <Router>.

Il motore di routing di Blazor

Come abbiamo detto nei precedenti articoli, il nostro albero di elementi parte dal componente App, radice dell’alberatura. E’ proprio questo elemento a contenere il componente Router, che si occupa della gestione della navigazione. Ha senso che sia qui, in modo che possa intercettare tutti gli eventi di navigazione e impedire che venga fatta una richiesta HTTP al server, gestendo localmente la richiesta dell’utente.

<Router AppAssembly="@typeof(Program).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>

L’attributo AppAssembly indica al motore di routing in quale assembly .NET debba cercare le pagine, in modo che possa rilevare le direttive @page e registrare le coppie path/componente. In questo caso le pagine si trovano nello stesso Assembly della nostra classe Program, quindi l’Assembly corrente.

A questo punto, quando l’utente cercherà di navigare verso un certo percorso, possono succedere due cose: il path ricercato viene trovato oppure no. Nel caso venga trovato la sezione <Found> del router naviga verso il componente corrispondente passandogli i dati di navigazione (RouteData="@routeData") e indicando un Layout (DefaultLayout="@typeof(MainLayout)"). Nel caso invece in cui non ci sia nessun componente in corrispondenza di quel percorso, la sezione <NotFound> visualizzerà un messaggio di cortesia, anche in questo caso con uno specifico Layout.

Ma che cos’è un Layout? Se avete avuto esperienze con ASP.NET WebForms, stiamo parlando dello stesso concetto di MasterPage, se invece venite da ASP.NET MVC è il corrispondente Layout di Razor che ben conoscete. Tecnicamente si tratta di un componente che troviamo nella cartella Shared, nel file, MainLayout.razor:

@inherits LayoutComponentBase
<div class="sidebar">
    <NavMenu />
</div>
<div class="main">
    <div class="top-row px-4">
        <a href="http://blazor.net" target="_blank" class="ml-md-auto">About</a>
    </div>

    <div class="content px-4">
        @Body
    </div>
</div>

Un file di layout estende una classe astratta base LayoutComponentBase e definisce il markup della struttura della nostra applicazione che non cambia durante la navigazione, indicando con il segnaposto @Body il punto esatto in cui vogliamo che venga renderizzato il componente destinatario della navigazione, o il messaggio di cortesia del componente NotFound in caso nessun componente risponda al percorso richiesto.

Come potete vedere nella sezione sidebar (<div class="sidebar"><NavMenu /></div>) viene utilizzato un componente separato che rappresenta il nostro menu di navigazione, che trovate nel file NavMenu.razor della cartella Shared. Andiamo ad aggiungere al nostro menu la voce Eventi:

...

<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>
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="eventi" Match="NavLinkMatch.All">
                <span class="oi oi-map" aria-hidden="true"></span> Eventi
            </NavLink>
        </li>
    </ul>
</div>

...

Qui potremmo utilizzare direttamente un elemento HTML di tipo anchor (<a href="/eventi" />), ma il componente <NavLink></NavLink> del framework gestisce per noi l’applicazione della classe active sull’elemento nel caso in cui il percorso di navigazione concida esattamente (Match="NavLinkMatch.All") con il valore di href. Ecco il risultato:

Il motore di routing si preoccuperà anche di aggiornare la barra degli indirizzi del browser e la history, in modo che se l’utente dovesse cliccare sul pulsante Indietro del browser, ritornerà alla navigazione precedente.

Non fatevi tentare

Anche Blazor, come tutti gli strumenti che cercano di lasciarvi massima libertà, può essere usato male. La tentazione può essere quella di utilizzare le Blazor Pages per contenere tutta l’interfaccia di una singola pagina, rischiando di dare più responsabilità del dovuto al componente e rendere il codice difficilmente manutenibile.

Lasciate invece a questi elementi il compito di utilizzare i componenti dell’applicazione per comporre la singola pagina, facendo da collettore per le elaborazioni e le invocazioni al back-end, gestendo con i parametri la comunicazione con i componenti figli.

Come al solito trovate il codice descritto qui, nella branch 04-pagine-navigazione.

Conclusioni

Anche oggi abbiamo fatto un ulteriore passo nella realizzazione della nostra Single Page Application. Un passo importante perchè, come abbiamo visto, la navigazione tra le pagine è di fondamentale importanza nella gestione dell’applicazione. Il routing ha comunque molte altre funzionalità che tratteremo in seguito, come la gestione dei parametri e la navigazione da codice, molto utili nella gestione di una applicazione reale. Nel prossimo articolo proseguiremo con la realizzazione della nostra CRUD, concentrandoci sul dettaglio con la gestione delle form e tante buone notizie per chi ha già utilizzato ASP.NET MVC.