Del 6 av 8

State & Dependency Injection

Hur du delar tillstånd mellan komponenter med tjänster, CascadingValue och en app-state-pattern. Plus en stadig introduktion till DI i Blazor.

Lektion 6 — V6. Vi går från "state i en komponent" till "state i hela appen" — utan att bygga spaghetti.

Dependency Injection 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();

Livstider — viktigt att förstå

LivstidBlazor ServerBlazor WASM
SingletonEn instans för hela servern (delas mellan användare!)En instans per app-laddning
ScopedEn instans per användares SignalR-sessionSamma som Singleton (en användare = en app)
TransientNy instans varje gång den begärsNy instans varje gång
Säkerhetsvarning för Server-render 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.

Använd en tjänst i en komponent

// 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;
}
Pattern: prenumerera + rendera om När en tjänsts data ändras vet komponenten inte om det automatiskt. Lös det med ett event Action i tjänsten, som komponenter prenumererar på i OnInitialized och avregistrerar i Dispose.

CascadingValue — skicka data ner i trädet

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!;
}

App-state-pattern

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>();
.NET 10: Circuit state persistence Blazor Server kan nu pausa och återuppta en circuit — om en användare tappar uppkopplingen ett tag kan deras Scoped-tjänster (som 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.

Konfiguration som tjänst

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

OwningComponentBase — egen scope för en komponent

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();
    }
}

Referenser

Elektroniska resurser

Böcker

Övningar

Lös övningarna självständigt. Det finns inget facit — lärandet sker i processen.

  1. CartService Bygg en CartService som hanterar en kundvagn. Två komponenter på samma sida ska visa antalet i vagnen och uppdateras synkront när produkter läggs till.
  2. Tema-växlare med CascadingValue Lägg till en knapp i headern som växlar mellan ljust och mörkt tema. Använd CascadingValue så att alla nestade komponenter kan reagera på temaändringen.
  3. Logger-tjänst Skapa en 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.
  4. Livstidsexperiment Registrera samma tjänst som Singleton, Scoped och Transient (med olika interface). Skapa en räknare i tjänsten och visa hur antalet förändras i flera komponenter, samt jämför mellan två webbläsarflikar.

Soloprojektor

Projekt 1 — Webshop med state Bygg en mini-webshop med produktlista, produktdetaljer och en kundvagn som syns i headern. All vagnsstate hanteras i en CartService som registreras Scoped. Antal i vagnen ska uppdateras i headern oavsett var i appen användaren befinner sig.
Projekt 2 — App-skal med inställningar (fördjupning) Skapa en 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.

← Föregående: Formulär Nästa: API & Auth →