Articoli

Gestire Layout multipli in Blazor

da

In questo articolo vederemo come gestire il layout della nostra applicazione partendo dal MainLayout che abbiamo di default nel nostro template Blazor.

MainLayout.razor

Il MainLayout è una pagina particolare che viene utilizzata come base comune per tutte le nostre pagine. In WebForm avevamo la MasterPage, in ASP.NET MVC avevamo il Layout. Il concetto non è fondamentalmente cambiato: abbiamo un “vassoio” dove comporre la nostra interfaccia ed andare ad inserire tutte le tessere del nostro mosaico.

Vediamo come è fatto:

@inherits LayoutComponentBase
<div class="page">
    <div class="sidebar">
        <NavMenu />
    </div>
    <main>
        <div class="top-row px-4">
            <a href="https://docs.microsoft.com/aspnet/" target="_blank">About</a>
        </div>
        <article class="content px-4">
            @Body
        </article>
    </main>
</div>

Notiamo sostanzialmente due cose:

  • eredita da una classe LayoutComponentBase
  • vediamo un richiamo ad un @Body

Queste due parti sono essenziali per poter avere un layout comune a tutte le pagine. In pratica, come abbiamo visto sull’articolo di Michele sulla navigazione nelle SPA, il layout rimane uguale e il motore di routing si occuperà di sostituire proprio quel @Body che abbiamo appena visto. SPOILER: @Body è una proprierà RenderFragment del LayoutComponentBase.

Aggiungiamo un layout

Fino a qui tutto bene.

Ma se volessimo avere diversi layout a seconda delle pagine visualizzate? Nulla di eclatante: creiamo la nostra nuova pagina di layout partendo per semplicità dalla pagina MainLayout di base togliendo solo il menù laterale.

Quindi mettiamo questa linea subito dopo la direttiva per la navigazione @page nella pagina dove utilizzarla. Nel nostro caso la pagina Index.razor

@Layout MyPersonalLayout

Et volià!

In questo modo avremo una pagina con un layout diverso da tutti gli altri. Ma cosa succede se abbiamo un 404 Page Not Found?

Come potere vedere si ritorna al layout di base.

Caricare un “MainLayout” diverso

Il nostro MainLayout, quindi, è il layout di base dell’applicazione, il layout che Blazor carica di default per cui è il layout su cui “ricade” quando non viene specificato un layout. Quindi da qualche parte Blazor prenderà questo riferimento. Ed in effetti è proprio così.

Apriamo il file App.razor nel progetto Client e sarà evidente il riferimento:

<Router AppAssembly="@typeof(App).Assembly">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
        <FocusOnNavigate RouteData="@routeData" Selector="h1" />
    </Found>
    <NotFound>
        <PageTitle>Not found</PageTitle>
        <LayoutView Layout="@typeof(MainLayout)">
            <p role="alert">Sorry, there's nothing at this address.</p>
        </LayoutView>
    </NotFound>
</Router>

Proprio alla terza e all’ottava riga vedrete il DefaultLayout.

Cambiamo il DefaultLayout

Procediamo per gradi. Cambiamo la riga tre in questo modo:

<RouteView RouteData="@routeData" DefaultLayout="@typeof(CustomLayout)" />

e la riga otto in questo modo:

<LayoutView Layout="@typeof(CustomLayout)">

Come si può vedere, ho semplicemente cambiato il layout definito da MainLayout a CustomLayout che avevamo creato precedentemente. Vediamo cosa succede a fronte di un 404

Ed ecco il nostro bellissimo layout custom caricato.

Caricamento dinamico di un layout

Abbiamo correttamente cambiato il layout di default della nostra applicazione Blazor, ma come possiamo cambiarlo facilmente senza dover ricompilare? Questa funzionalità potrebbe esserci utile quando il nostro sito lo “rivendiamo” a più clienti cambiando semplicemente il layout, oppure quando facciamo dei restyling ma vogliamo mantenere la possibilità di scegliere il vecchio layout.

Per iniziare aggiungiamo una sezione @code alla nostra App.razor e aggiungiamo questo codice

@code
{
    public Type LayoutType { get; set; } = typeof(MainLayout);

    public void SetLayout(Type layout)
    {
        LayoutType = layout;
        StateHasChanged();
    }
}

e il codice razor avrà questo aspetto

<Router AppAssembly="@typeof(App).Assembly">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" DefaultLayout="@LayoutType" />
        <FocusOnNavigate RouteData="@routeData" Selector="h1" />
    </Found>
    <NotFound>
        <PageTitle>Not found</PageTitle>
        <LayoutView Layout="@LayoutType">
            <p role="alert">Sorry, there's nothing at this address.</p>
        </LayoutView>
    </NotFound>
</Router>

In questo modo il nostro layout potrà essere sostituito alla partenza dell’app senza dover ricompilare.

Proviamo…

Il layout potrà essere passato alla funzione SetLayout in molti modi come un parametro in appconfig o su database. Per il nostro concept ci basta una proprietà per validare il concetto.

Sempre in App.razor aggiungiamo nella sezione @code una proprietà e l’override dell’OnInizializedAsync in questo modo

    public string LayoutName { get; set; } = "CustomLayout";
    protected override Task OnInitializedAsync()
    {
        switch (LayoutName)
        {
            case "MainLayout":
                SetLayout(typeof(MainLayout));
                break;
            case "CustomLayout":
                SetLayout(typeof(CustomLayout));
                break;
            default:
                SetLayout(typeof(MainLayout));
                break;
        }
        return base.OnInitializedAsync();
    }

Ed ecco qua il risultato come ci aspettavamo

Usiamo la Dependency Injection

La soluzione vista sin’ora, ci obbliga però a ricompilare ogni volta l’applicazione. Ma se volessimo farlo “al volo” senza ricompilare? Il modo migliore è sfruttare la Dependency Injection.

Aggiungiamo un file appsettings.json lato Client che dovrà essere posizionato in wwwroot. Assicuriamoci anche che sia sempre copiato.

{
  "StartupLayout": {
    "Layout": "BlazorCustomLayoutPage.Client.Shared.MainLayout"
  }
}

Innanzitutto creiamo una classe che definisca come saranno le impostazioni provenienti dall’appsettings.json e la chiameremo StartupLayout. Sarà fatta in questo modo:

public class StartupLayout : IStartupLayout
{
        public string Layout { get; set; } 
}

Ora ci serve un’interfaccia da dare in pasto alla dependency injection che conterrà il nostro layout che chiameremo IUserLayout e sarà fatta in questo modo:

public interface IUserLayout
{
    Type GetLayoutType();
}

Il metodo dichiarato ci permetterà di recuperare il layout da App.razor… ma ci arriviamo, con calma. Per ora implementiamo in una classe reale questa interfaccia. La classe la chiameremo, con un eccesso di fantasia, UserLayout e sarà fatta in questo modo:

    public class UserLayout : IUserLayout
    {
        private StartupLayout option;
        public UserLayout(IOptions<StartupLayout> option)
        {
            this.option = option.Value;
        }

        public Type GetLayoutType()
        {
            return Type.GetType(option.Layout);
        }
    }

Noterete che dalla dependency injection recupereremo la configurazione dall’appsettings.json tramite IOptions. Ora vediamo come modificare il Program.cs del progetto Client per usare correttamente quando abbiamo scritto in questa parte.

builder.Services.Configure<StartupLayout>(options => builder.Configuration.GetSection("StartupLayout").Bind(options));
builder.Services.AddScoped<IUserLayout, UserLayout>();

Nella pria riga recuperiamo dal file appsettings.json il valore che ci interessa, mentre la seconda riga creerà l’istanza del nostro setting per essere utilizzata in App.razor come layout.

Vi ricordate com’era fatto quel file? Ora lo modificheremo come segue per poter essere compatibile con il nostro nuovo modo di gestire i layout. Sarà così:

@using BlazorCustomLayoutPage.Client.Models
@inject HttpClient Http
@inject IUserLayout userLayout

@*<Router AppAssembly="@typeof(App).Assembly">*@
<Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true">
    <Found Context="routeData">
        @*<RouteView RouteData="@routeData" DefaultLayout="@LayoutType" />*@
        <RouteView RouteData="@routeData" DefaultLayout="@userLayout.GetLayoutType()" />
        <FocusOnNavigate RouteData="@routeData" Selector="h1" />
    </Found>
    <NotFound>
        <PageTitle>Not found</PageTitle>
        @*<LayoutView Layout="@LayoutType">*@
         <LayoutView Layout="@userLayout.GetLayoutType()">
            <p role="alert">Sorry, there's nothing at this address.</p>
        </LayoutView>
    </NotFound>
</Router>

Ho lasciato commentate le tre parti create precedentemente e che ora non servono più. Anche il codice legato ad App.razor non ci serve più, perciò possiamo eliminarlo (nel codice di esempio lo lascerò commentato).

Aggiungiamo un layout per la parte “Page not found”

Come abbiamo visto nel file App.razor, esiste una sezione per il “Page not found” con la possibilità di specificare un nostro layout custom. Quindi aggiungiamo un layout per il page not found, che chiameremo PageNotFoundLayout.razor, nella cartella Shared del progetto Client. La pagina di layout che faremo sarà molto semplice. La faremo così:

@inherits LayoutComponentBase

<div class="page">
    <main>
        <article class="content px-4">
            <div class='c'>
                <div class='_404'>404</div>
                <div class='_1'>This is not the page</div>
                <div class='_2'>you are looking for</div>
                <a class='btn' href='#'>Home</a>
            </div>
        </article>
    </main>
</div>

In questo modo daremo immediatamente un messaggio all’utente e daremo anche un bottone per tornare alla home page. Aggiungiamo anche una manciata di CSS per dare un’aria un po’ più accattivante alla nostra pagina et… voilà!

Ovviamente potrete comporvi la pagina come meglio preferite, l’importante è sostituire la corrispondente riga nel file App.razor

<LayoutView Layout="@typeof(PageNotFoundLayout)">

e volendo potremo gestire anche questa pagina dinamicamente esattamente come abbiamo fatto per il layout principale. Basta replicare quello che abbiamo appena fatto per il layout principale dell’applicazione.

Questo è solo un test, ma spero di avervi suscitato la curiosità e la voglia di sperimentare. Troverete tutto il codice su GitHub.

Alla prossima!