Quando i progetti cominciano a crescere è importante tenere il codice ordinato e ben suddiviso, magari suddividendo la solution in progetti che rispecchino i confini, o meglio i Bounded Context, della nostra applicazione. Già, ma come possiamo spostare componenti e pagine all’esterno di un progetto Blazor? Come facciamo a gestire le route di queste pagine? Prendiamo, ad esempio, l’applicazione generata dal template di Visual Studio e proviamo a pensare di spostare ogni pagina in un apposito progetto dedicato.
Suddividiamo la solution
Quando creiamo un nuovo progetto Blazor in Visual Studio viene generata questa applicazione
e se andiamo a guardare il codice generato dall’IDE troviamo qualcosa di simile a questo
Come possiamo vedere tutte le pagine che compongono la nostra applicazione sono contenute nella cartella Pages del progetto Client della solution, mentre i componenti nella cartella Shared. Fin che il progetto resta di piccole dimensioni questo approccio può funzionare benissimo, ma cosa succede quando le features da implementare aumentano ed i nostri componenti iniziano a crescere a dismisura?
Certamente possiamo suddividere le cartelle Pages e Shared in altrettante sottocartelle, ognuna per ogni feature da implementare. Può essere un’idea, ma vediamo come possiamo invece suddividere ogni feature in un nuovo progetto ed ottenere una solution simile a questa
In questa nuova versione nel progetto Client di Blazor è rimasta soltanto la nostra Index.razor, tutto il resto è stato spostato nei rispettivi progetti. Ma questa, come vedremo, non è la sola differenza. Nella prima versione tutti i componenti della nostra soluzione vengono caricati all’avvio dell’applicazione, rendendola sempre più lenta al crescere di quest’ultimi. Nella seconda versione i componenti vengono caricati solo quando invocati la prima volta. Ma andiamo a vedere come possiamo ottenere questo risultato.
Refactor
Partiamo dal capire che tipo di progetto dobbiamo utilizzare per ospitare i nostri componenti Razor. Affinché una class library possa contenere, e renderizzare, componenti Razor deve essere di tipo Razor Class Library, quindi creiamo un progetto di questo tipo per ogni modulo della nostra applicazione e spostiamo i rispettivi componenti al loro interno.
A questo punto, se proviamo a lanciare la nostra applicazione avremo una serie di errori che ci informano che la nostra app non è più in grado di individuare i componenti. Come mai? Proviamo ad analizzare il contenuto del file App.razor
<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>
In questo file l’oggetto Router, incaricato appunto di risolvere le rotte di destinazione delle nostre chiamate, cerca i riferimenti nell’Assembly che contiene l’oggetto App, ossia nel progetto Client della nostra applicazione. Quindi, fino a quando i nostri componenti si trovano in questo progetto, tutto funziona alla meraviglia, ma non appena li spostiamo, come appunto abbiamo fatto noi, il Router non è più in grado di trovarli.
Vediamo come modificare il file App.razor per ottenere il risultato desiderato. Innanzitutto, per un codice più pulito, suddividiamo la parte razor dalla parte in C# creando una file App.razor.cs
using System.Reflection;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Routing;
using Microsoft.AspNetCore.Components.WebAssembly.Services;
namespace MultiProjectsRoute.Client;
public class AppBase : ComponentBase, IDisposable
{
[Inject] private LazyAssemblyLoader AssemblyLoader { get; set; }
[Inject] private ILogger<App> Logger { get; set; }
protected readonly List<Assembly> LazyLoadedAssemblies = new();
protected async Task OnNavigateAsync(NavigationContext args)
{
try
{
switch (args.Path)
{
case "fetchdata":
{
var assemblies = await AssemblyLoader.LoadAssembliesAsync(new List<string>
{ "MultiProjectsRoute.Modules.WeatherForecast.dll" });
LazyLoadedAssemblies.AddRange(assemblies);
break;
}
case "counter":
{
var assemblies = await AssemblyLoader.LoadAssembliesAsync(new List<string>
{ "MultiProjectsRoute.Modules.Counter.dll" });
LazyLoadedAssemblies.AddRange(assemblies);
break;
} // We need to load MultiProjects.Modules.Dashboard.dll at startup
default:
{
var assemblies = await AssemblyLoader.LoadAssembliesAsync(new List<string> { "MultiProjectsRoute.Modules.Dashboard.dll" });
LazyLoadedAssemblies.AddRange(assemblies);
break;
}
}
} catch (Exception ex)
{
Logger.LogError($"Error Loading spares page: {ex}");
}
}
#region Dispose
public void Dispose(bool disposing)
{
if (disposing) { }
}
public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
~AppBase() { this.Dispose(false); }
#endregion
}
Per poter creare una classe da cui far ereditare le proprietà al nostro componente razor dobbiamo implementare la classe astratta ComponentBase e l’interfaccia IDisposable. A questo punto possiamo indicare alla nostra classe App.razor che può ereditare da AppBase in questo modo
@inherits AppBase <Router AppAssembly="@typeof(Program).Assembly" AdditionalAssemblies="@LazyLoadedAssemblies" OnNavigateAsync="@OnNavigateAsync"> <Found Context="routeData"> <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"/> </Found> <NotFound> <LayoutView Layout="@typeof(MainLayout)"> <p>Sorry, there's nothing at this address.</p> <p>Loaded assemblies are:</p> @foreach (var assembly in LazyLoadedAssemblies) { <h5>@assembly.FullName</h5> } </LayoutView> </NotFound> </Router>
Cosa è successo alla nostra App.razor? Innanzitutto eredita da AppBase, la classe che abbiamo scritto prima e che contiene le proprietà ed i comportamenti necessari. Ma questi sono dettagli di codice, non sono la vera novità. La vera novità è l’aggiunto della proprietà AdditionalAssemblies, di cui abbiamo parlato poco sopra, che indica al Router dove cercare i componenti. Ma non solo, con il metodo OnNavigateAsync andiamo ad indicare anche come, e quando, questi componenti verranno caricati.
Notiamo che l’assembly MultiProjects.Modules.Desktop.dll viene caricato di default. Questo ci serve subito, all’avvio della nostra app.
Gli altri invece solo quando l’utente ne avrà bisogno, ossia al click sulla sideBar di navigazione.
Affinchè il tutto possa funzionare senza errori abbiamo bisogno di un altro piccolo accorgimento. Dobbiamo segnalare al progetto Client che i nostri moduli ora verranno caricati in modalità Lazy Load, come possiamo farlo? Apriamo il file csproj e andiamo ad aggiungere queste righe dopo avere referenziato, nelle dependencies del progetto, i relativi progetti.
<ItemGroup> <Folder Include="Configuration\" /> <BlazorWebAssemblyLazyLoad Include="MultiProjectsRoute.Modules.Dashboard.dll" /> <BlazorWebAssemblyLazyLoad Include="MultiProjectsRoute.Modules.Counter.dll" /> <BlazorWebAssemblyLazyLoad Include="MultiProjectsRoute.Modules.WeatherForecast.dll" /> </ItemGroup>
A questo punto il progetto è pronto per essere eseguito, ma completamente re-fattorizzato e pronto a crescere secondo le esigenze del nostro cliente, senza trasformare le nostre sessione di codice in momenti da incubo.
Trovate il codice qui: https://github.com/Ace68/BlazorMultiProjects
Happy coding a tutti!