Del 3 av 8

Data binding & händelser

Hur Blazor håller UI:t synkat med C#-tillståndet, hur två-vägs-binding fungerar, och hur komponenter kommunicerar med EventCallback.

Lektion 3 — V3. Vi tittar djupare på den reaktiva delen: @bind, klickhändelser, EventCallback och vad StateHasChanged egentligen gör.

En-vägs vs två-vägs binding

En-vägs binding är "läs från C# till UI" — vilket @variabel redan ger oss. Två-vägs binding går åt båda hållen och skrivs med @bind.

@page "/binding-demo"

<h1>Hej, @namn!</h1>

<input @bind="namn" />                          @* uppdaterar vid blur *@
<input @bind="namn" @bind:event="oninput" />    @* uppdaterar vid varje tangent *@

@code {
    private string namn = "Världen";
}
Vad @bind egentligen gör Det är syntactic sugar för två saker: en value="@namn"-attribut och en @onchange-handler som sätter namn till det nya värdet. Du kan göra det manuellt om du behöver mer kontroll.

Formatering vid binding

<input type="date" @bind="birthDate" @bind:format="yyyy-MM-dd" />

@code {
    private DateTime birthDate = DateTime.Today;
}

Händelser — @onclick och vänner

<button @onclick="Increment">+1</button>
<button @onclick="@(() => count -= 1)">-1</button>
<button @onclick="Reset">Återställ</button>

<p>Antal: @count</p>

@code {
    private int count = 0;

    private void Increment() => count++;
    private void Reset()     => count  = 0;
}
DirektivAnvändning
@onclickKlick
@oninputVid varje tangenttryckning i input
@onchangeVid värdeändring — triggas när elementet tappar fokus (blur) med ett nytt värde
@onsubmitFormulärinlämning
@onkeydown / @onkeyupTangenttryckning
@onmouseover / @onmouseoutMusrörelser
Vad är fokus och blur?

Ett HTML-element har fokus när det är aktivt — d.v.s. när du klickat i det eller tabbar till det med tangentbordet. Ett textfält med fokus tar emot tangenttryckningar.

Blur är motsatsen: elementet tappar fokus — till exempel när du klickar någon annanstans på sidan eller tabbar vidare till nästa fält.

Det är vid blur som @onchange triggas för textinput — alltså inte för varje bokstav du skriver, utan en gång när du "lämnar" fältet. Det innebär:

@* @onchange — uppdateras vid blur (fokustapp) *@
<input @onchange="e => namn = e.Value?.ToString() ?? """ value="@namn" />

@* @oninput — uppdateras direkt vid varje tangenttryckning *@
<input @oninput="e => namn = e.Value?.ToString() ?? """ value="@namn" />

@* @bind är default onchange; @bind:event="oninput" ändrar till oninput *@
<input @bind="namn" />
<input @bind="namn" @bind:event="oninput" />

<p>Namn: @namn</p>

@code { private string namn = ""; }

Event-argument

<input @onkeydown="HandleKey" />
<div @onclick="HandleClick">Klicka var som helst</div>

@code {
    private void HandleKey(KeyboardEventArgs e)
    {
        if (e.Key == "Enter") Console.WriteLine("Enter trycktes");
    }

    private void HandleClick(MouseEventArgs e)
    {
        Console.WriteLine($"Klick på ({e.ClientX}, {e.ClientY})");
    }
}

Asynkrona händelsehanterare

<button @onclick="LoadData" disabled="@isLoading">
    @(isLoading ? "Laddar..." : "Hämta data")
</button>

@code {
    private bool isLoading;
    private List<Product> products = new();

    private async Task LoadData()
    {
        isLoading = true;
        products = await Http.GetFromJsonAsync<List<Product>>("api/products") ?? new();
        isLoading = false;
    }
}
Tänk på — trådar och StateHasChanged

Blazor anropar StateHasChanged() automatiskt efter en händelsehanterare (t.ex. @onclick). Men om du ändrar tillstånd från en bakgrundstråd eller timer måste du anropa InvokeAsync(StateHasChanged) själv.

Vad är en bakgrundstråd? — Ett program kan köra flera saker parallellt. Varje sådan parallell körning kallas en tråd (thread). Blazors UI-uppdateringar sker alltid på en specifik tråd — "UI-tråden". En bakgrundstråd är en annan tråd som körs parallellt, t.ex. när du startar en System.Threading.Timer eller ett långt Task.Run(...). Koden i timerns callback körs på en bakgrundstråd, inte på UI-tråden.

Problemet: om bakgrundstråden ändrar en C#-variabel och du sedan anropar StateHasChanged() direkt, vet inte Blazor att anropet kom från "fel" tråd och kan krascha eller ignorera uppdateringen. InvokeAsync skickar anropet tillbaka till rätt tråd.

Tumregel

EventCallback — barn till förälder

När en barnkomponent vill berätta något för sin förälder används EventCallback:

@* ProductCard.razor *@
<div class="card">
    <h3>@Product.Name</h3>
    <p>@Product.Price.ToString("C")</p>
    <button @onclick="HandleBuy">Lägg i kundvagn</button>
</div>

@code {
    [Parameter, EditorRequired] public Product Product { get; set; } = default!;
    [Parameter] public EventCallback<Product> OnBuy { get; set; }

    private Task HandleBuy() => OnBuy.InvokeAsync(Product);
}
@* Förälder: Shop.razor *@
@foreach (var p in products)
{
    <ProductCard Product="p" OnBuy="AddToCart" />
}

<p>I kundvagn: @cart.Count</p>

@code {
    private List<Product> products = new();
    private List<Product> cart     = new();

    private void AddToCart(Product p) => cart.Add(p);
}

Två-vägs binding mellan komponenter

Ibland vill man @bind mot ett värde i en barnkomponent. Då behöver barnet både en Value-parameter och en ValueChanged EventCallback:

@* SearchBox.razor *@
<input value="@Value" @oninput="OnInput" placeholder="Sök..." />

@code {
    [Parameter] public string Value { get; set; } = "";
    [Parameter] public EventCallback<string> ValueChanged { get; set; }

    private Task OnInput(ChangeEventArgs e)
        => ValueChanged.InvokeAsync(e.Value?.ToString() ?? "");
}
@* Förälder *@
<SearchBox @bind-Value="searchTerm" />
<p>Du söker efter: @searchTerm</p>

@code { private string searchTerm = ""; }

StateHasChanged — det reaktiva hjärtat

Blazor renderar om en komponent när:

private System.Threading.Timer? _timer;

protected override void OnInitialized()
{
    _timer = new System.Threading.Timer(_ =>
    {
        sekunder++;
        InvokeAsync(StateHasChanged);  // viktig — vi är på en annan tråd
    }, null, 0, 1000);
}

Referenser

Elektroniska resurser

Böcker

Övningar

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

  1. Live-sökning Bygg en sida med en <input> som filtrerar en lista på 20 hårdkodade produkter live medan du skriver. Använd @bind:event="oninput".
  2. Räknare med async Skapa en knapp som vid klick "hämtar data" (simulera med await Task.Delay(1500)). Visa en laddnings­indikator under tiden och inaktivera knappen.
  3. EventCallback i praktiken Bygg en RatingStars-komponent (1–5 stjärnor) som rapporterar ändrade värden till föräldern via en EventCallback<int>. Föräldern visar det valda värdet.
  4. Tvåvägs-binding mellan komponenter Bygg en NumberSpinner-komponent med +/- knappar som föräldern kan @bind-Value mot. Visa två spinners på samma sida och summera deras värden.

Soloprojektor

Projekt 1 — Att-göra-lista Bygg en klassisk to-do-app: input + lägg till-knapp, lista med checkboxar och en räknare för "antal kvar". All state hålls i komponenten. Stöd ENTER för att lägga till.
Projekt 2 — Kalkylator (fördjupning) Bygg en kalkylator med en CalculatorButton-komponent som rapporterar klick till en föräldrakomponent via EventCallback. Föräldern håller display-state och beräknar resultat.

← Föregående: Komponenter Nästa: Routing & layout →