Hur du delar tillstånd mellan komponenter med tjänster, CascadingValue och en app-state-pattern. Plus en stadig introduktion till DI i Blazor.
Blazor använder samma DI-system som ASP.NET Core. Du registrerar tjänster i Program.cs och får ut dem i komponenter via @inject.
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
// Egna tjänster
builder.Services.AddSingleton<IConfigService, ConfigService>();
builder.Services.AddScoped<CartService>();
builder.Services.AddTransient<EmailSender>();
builder.Services.AddHttpClient();
var app = builder.Build();
| Livstid | Blazor Server | Blazor WASM |
|---|---|---|
| Singleton | En instans för hela servern (delas mellan användare!) | En instans per app-laddning |
| Scoped | En instans per användares SignalR-session | Samma som Singleton (en användare = en app) |
| Transient | Ny instans varje gång den begärs | Ny instans varje gång |
AddSingleton i Blazor Server delas mellan alla användare. Lägg aldrig användarspecifik data i en singleton. För användarspecifik state använd AddScoped.
// Services/CartService.cs
public class CartService
{
private readonly List<Product> _items = new();
public IReadOnlyList<Product> Items => _items;
public int Count => _items.Count;
public event Action? OnChange;
public void Add(Product p)
{
_items.Add(p);
OnChange?.Invoke();
}
public void Remove(Product p)
{
_items.Remove(p);
OnChange?.Invoke();
}
}
@page "/cart"
@inject CartService Cart
@implements IDisposable
<h1>Kundvagn (@Cart.Count)</h1>
<ul>
@foreach (var p in Cart.Items)
{
<li>@p.Name <button @onclick="() => Cart.Remove(p)">Ta bort</button></li>
}
</ul>
@code {
protected override void OnInitialized()
=> Cart.OnChange += StateHasChanged;
public void Dispose()
=> Cart.OnChange -= StateHasChanged;
}
event Action i tjänsten, som komponenter prenumererar på i OnInitialized och avregistrerar i Dispose.
Ibland vill man att alla nestade komponenter ska kunna nå ett värde, utan att behöva skicka det som parameter på varje nivå.
@* App.razor eller en layout *@
<CascadingValue Value="currentUser" Name="User">
<CascadingValue Value="theme">
@Body
</CascadingValue>
</CascadingValue>
@code {
private User currentUser = new("Anna");
private Theme theme = new("dark");
}
@* Vilken nestad komponent som helst *@
@code {
[CascadingParameter(Name = "User")] public User CurrentUser { get; set; } = default!;
[CascadingParameter] public Theme CurrentTheme { get; set; } = default!;
}
För större appar samla relaterad state i en tjänst som registreras som Scoped. Det är Blazors enklaste och mest robusta pattern för delad state.
public class AppState
{
public UserSession? CurrentUser { get; private set; }
public string Theme { get; private set; } = "light";
public event Action? OnChange;
public void SetUser(UserSession user)
{
CurrentUser = user;
NotifyChanged();
}
public void ToggleTheme()
{
Theme = Theme == "light" ? "dark" : "light";
NotifyChanged();
}
private void NotifyChanged() => OnChange?.Invoke();
}
// Program.cs
builder.Services.AddScoped<AppState>();
AppState) serialiseras till disk och rekonstrueras när de återkommer, i stället för att circuit:en dumpas och allt tillstånd försvinner. Det gör service+event-mönstret du just lärt dig ännu mer robust. Som komplement finns också [PersistentState]-attributet (introduceras i L8) som hanterar tillstånd över prerender.
// appsettings.json
{
"Api": { "BaseUrl": "https://localhost:5001/api" }
}
// Program.cs
builder.Services.Configure<ApiOptions>(builder.Configuration.GetSection("Api"));
// Komponent
@inject IOptions<ApiOptions> ApiOpts
<p>API: @ApiOpts.Value.BaseUrl</p>
Om en komponent själv ska skapa en ny scope för sina tjänster (typiskt för DbContext):
@inherits OwningComponentBase<AppDbContext>
@code {
protected override async Task OnInitializedAsync()
{
var products = await Service.Products.ToListAsync();
}
}
Lös övningarna självständigt. Det finns inget facit — lärandet sker i processen.
CartService som hanterar en kundvagn. Två komponenter på samma sida ska visa antalet i vagnen och uppdateras synkront när produkter läggs till.
CascadingValue så att alla nestade komponenter kan reagera på temaändringen.
ILogStore-tjänst (Scoped) som samlar log-meddelanden. Bygg en sida som visar alla meddelanden och en knapp i ett helt annat hörn av appen som lägger till nya.
CartService som registreras Scoped. Antal i vagnen ska uppdateras i headern oavsett var i appen användaren befinner sig.
AppState-tjänst som hanterar tema (light/dark), språk (sv/en) och inloggad användare. Lägg in CascadingValue i MainLayout och visa hur alla undersidor anpassar sig till valen.