Dela via


ASP.NET Core Razor komponentåtergivning

Not

Det här är inte den senaste versionen av den här artikeln. För den aktuella utgåvan, se .NET 9-versionen av den här artikeln.

Varning

Den här versionen av ASP.NET Core stöds inte längre. Mer information finns i .NET och .NET Core Support Policy. Den aktuella versionen finns i .NET 9-versionen av den här artikeln .

Viktig

Den här informationen gäller en förhandsversionsprodukt som kan ändras avsevärt innan den släpps kommersiellt. Microsoft lämnar inga garantier, uttryckliga eller underförstådda, med avseende på den information som tillhandahålls här.

För den aktuella versionen, se .NET 9-versionen av den här artikeln.

I den här artikeln beskrivs Razor komponentåtergivning i ASP.NET Core Blazor-appar, inklusive när StateHasChanged anropas för att manuellt utlösa en komponent som ska återges.

Återgivningskonventioner för ComponentBase

Komponenter måste renderas när de först läggs till i komponenthierarkin av en överordnad komponent. Det här är den enda gången som komponenten måste renderas. Komponenter kan renderas vid andra tillfällen enligt deras egen logik och konventioner.

Razor komponenter ärver från den ComponentBase basklassen, som innehåller logik för att utlösa rerendering vid följande tidpunkter:

Komponenter som ärvs från ComponentBase hoppar över omarbetningar på grund av parameteruppdateringar om något av följande är sant:

  • Alla parametrar kommer från en uppsättning kända typer† eller någon primitiv typ som inte har ändrats sedan den tidigare uppsättningen parametrar angavs.

    †Det Blazor ramverket använder en uppsättning inbyggda regler och explicita parametertypkontroller för ändringsidentifiering. Dessa regler och typer kan ändras när som helst. Mer information finns i ChangeDetection-API:et i referenskällan ASP.NET Core.

    Anteckning

    Dokumentationslänkar till .NET-referenskällan läser vanligtvis in lagringsplatsens standardgren, vilket representerar den aktuella utvecklingen för nästa version av .NET. Om du vill välja en tagg för en specifik version använder du listrutan för att växla mellan grenar eller taggar. Mer information finns i Så här väljer du en versionstagg för ASP.NET Core-källkod (dotnet/AspNetCore.Docs #26205).

  • Åsidosättningen av komponentens ShouldRender-metod returnerar false (standardimplementeringen ComponentBase returnerar alltid true).

Kontrollera återgivningsflödet

I de flesta fall resulterar ComponentBase konventioner i rätt delmängd av komponentåtersändare när en händelse inträffar. Utvecklare behöver vanligtvis inte tillhandahålla manuell logik för att tala om för ramverket vilka komponenter som ska återskapas och när de ska återställas. Den övergripande effekten av ramverkets konventioner är att komponenten som tar emot en händelse omrenderar sig själv, vilket rekursivt utlöser omrendering av de underliggande komponenter vars parametervärden kan ha ändrats.

Mer information om prestandakonsekvenserna av ramverkets konventioner och hur du optimerar en apps komponenthierarki för rendering finns i ASP.NET Metodtips för Core Blazor-prestanda.

Återgivning av direktuppspelning

Använd strömningsåtergivning med statisk återgivning på serversidan (statisk SSR) eller prerendering för att strömma innehållsuppdateringar på svarsströmmen och förbättra användarupplevelsen för komponenter som utför långvariga asynkrona uppgifter för fullständig återgivning.

Tänk dig till exempel en komponent som gör en tidskrävande databasfråga eller ett webb-API-anrop för att återge data när sidan läses in. Normalt måste asynkrona uppgifter som körs som en del av återgivningen av en komponent på serversidan slutföras innan det renderade svaret skickas, vilket kan fördröja inläsningen av sidan. Eventuella betydande fördröjningar i återgivningen av sidan skadar användarupplevelsen. För att förbättra användarupplevelsen återger direktuppspelningsåtergivningen först hela sidan snabbt med platshållarinnehåll medan asynkrona åtgärder körs. När åtgärderna har slutförts skickas det uppdaterade innehållet till klienten på samma svarsanslutning och korrigeras till DOM.

Direktuppspelningsrendering kräver att servern undviker att buffrar utdata. Svarsdata måste flöda till klienten när data genereras. För värdar som framtvingar buffring försämras strömningsåtergivningen på ett smidigt sätt och sidan läses in utan återgivning av direktuppspelning.

Om du vill strömma innehållsuppdateringar när du använder statisk återgivning på serversidan (statisk SSR) eller förrendering använder du attributet [StreamRendering] i .NET 9 eller senare (använd [StreamRendering(true)] i .NET 8) på komponenten. Återgivning av direktuppspelning måste uttryckligen aktiveras eftersom strömmade uppdateringar kan leda till att innehållet på sidan flyttas. Komponenter utan attributet implementerar automatiskt direktuppspelningsåtergivning om den överordnade komponenten använder funktionen. Passera false till attributet hos en underkomponent för att inaktivera funktionen på den nivån och längre ner i komponentsubträdet. Attributet fungerar när det tillämpas på komponenter som tillhandahålls av ett Razor klassbibliotek.

Följande exempel baseras på den Weather komponenten i en app som skapats från Blazor Web App-projektmallen. Anropet till Task.Delay simulerar hämtning av väderdata asynkront. Komponenten renderar ursprungligen platshållarinnehåll ("Loading...") utan att vänta på att den asynkrona fördröjningen ska slutföras. När den asynkrona fördröjningen är slutförd och väderdatainnehållet har genererats, strömmas innehållet till svaret och integreras i väderprognostabellen.

Weather.razor:

@page "/weather"
@attribute [StreamRendering]

...

@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        ...
        <tbody>
            @foreach (var forecast in forecasts)
            {
                <tr>
                    <td>@forecast.Date.ToShortDateString()</td>
                    <td>@forecast.TemperatureC</td>
                    <td>@forecast.TemperatureF</td>
                    <td>@forecast.Summary</td>
                </tr>
            }
        </tbody>
    </table>
}

@code {
    ...

    private WeatherForecast[]? forecasts;

    protected override async Task OnInitializedAsync()
    {
        await Task.Delay(500);

        ...

        forecasts = ...
    }
}

Ignorera uppdatering av användargränssnitt (ShouldRender)

ShouldRender anropas varje gång en komponent återges. Åsidosätt ShouldRender för att hantera uppdatering av användargränssnittet. Om implementeringen returnerar trueuppdateras användargränssnittet.

Även om ShouldRender åsidosättas återges komponenten alltid från början.

ControlRender.razor:

@page "/control-render"

<PageTitle>Control Render</PageTitle>

<h1>Control Render Example</h1>

<label>
    <input type="checkbox" @bind="shouldRender" />
    Should Render?
</label>

<p>Current count: @currentCount</p>

<p>
    <button @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;
    private bool shouldRender = true;

    protected override bool ShouldRender() => shouldRender;

    private void IncrementCount() => currentCount++;
}
@page "/control-render"

<PageTitle>Control Render</PageTitle>

<h1>Control Render Example</h1>

<label>
    <input type="checkbox" @bind="shouldRender" />
    Should Render?
</label>

<p>Current count: @currentCount</p>

<p>
    <button @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;
    private bool shouldRender = true;

    protected override bool ShouldRender() => shouldRender;

    private void IncrementCount() => currentCount++;
}
@page "/control-render"

<label>
    <input type="checkbox" @bind="shouldRender" />
    Should Render?
</label>

<p>Current count: @currentCount</p>

<p>
    <button @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;
    private bool shouldRender = true;

    protected override bool ShouldRender()
    {
        return shouldRender;
    }

    private void IncrementCount()
    {
        currentCount++;
    }
}
@page "/control-render"

<label>
    <input type="checkbox" @bind="shouldRender" />
    Should Render?
</label>

<p>Current count: @currentCount</p>

<p>
    <button @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;
    private bool shouldRender = true;

    protected override bool ShouldRender()
    {
        return shouldRender;
    }

    private void IncrementCount()
    {
        currentCount++;
    }
}
@page "/control-render"

<label>
    <input type="checkbox" @bind="shouldRender" />
    Should Render?
</label>

<p>Current count: @currentCount</p>

<p>
    <button @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;
    private bool shouldRender = true;

    protected override bool ShouldRender()
    {
        return shouldRender;
    }

    private void IncrementCount()
    {
        currentCount++;
    }
}
@page "/control-render"

<label>
    <input type="checkbox" @bind="shouldRender" />
    Should Render?
</label>

<p>Current count: @currentCount</p>

<p>
    <button @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;
    private bool shouldRender = true;

    protected override bool ShouldRender()
    {
        return shouldRender;
    }

    private void IncrementCount()
    {
        currentCount++;
    }
}

För mer information om bästa praxis för prestanda för ShouldRender, se ASP.NET Core Blazor prestanda bästa praxis.

StateHasChanged

När du anropar StateHasChanged, schemaläggs en ny rendering för att köras när appens huvudtråd är ledig.

Komponenterna köas för återgivning, och de köas inte igen om det redan finns en väntande omrendering. Om en komponent anropar StateHasChanged fem gånger i rad inom en loop kommer komponenten bara att renderas en gång. Det här beteendet kodas i ComponentBase, som först kontrollerar om den har placerat en rerender i kö innan ytterligare en sparas.

En komponent kan renderas flera gånger under samma cykel, vilket ofta inträffar när en komponent har barn som interagerar med varandra.

  • En överordnad komponent renderar flera barnkomponenter.
  • Underordnade komponenter renderar och utlöser en uppdatering på det överordnade objektet.
  • En överordnad komponent renderas om med nytt tillstånd.

Med den här designen kan StateHasChanged anropas när det behövs utan risk för onödig rendering. Du kan alltid ta kontroll över det här beteendet i enskilda komponenter genom att implementera IComponent direkt och manuellt styra när komponenten renderas.

Tänk på följande IncrementCount metod som ökar antalet, anropar StateHasChangedoch ökar antalet igen:

private void IncrementCount()
{
    currentCount++;
    StateHasChanged();
    currentCount++;
}

När du går igenom koden i felsökningsprogrammet kanske du tror att antalet uppdateras i användargränssnittet för den första körningen av currentCount++ omedelbart efter att StateHasChanged anropas. Användargränssnittet visar dock inte ett uppdaterat antal vid den tidpunkten på grund av den synkrona bearbetning som äger rum för den här metodens körning. Det finns ingen möjlighet för återgivningsverktyget att återge komponenten förrän händelsehanteraren är klar. Användargränssnittet visar ökningar för båda currentCount++ körningar i en enda återgivning.

Om du väntar på något mellan de currentCount++ raderna ger det väntande anropet återgivningsverktyget en chans att rendera. Detta har lett till att vissa utvecklare anropar Delay med en fördröjning på en millisekunder i sina komponenter för att tillåta att en rendering sker, men vi rekommenderar inte att godtyckligt sakta ned en app för att skicka en rendering.

Den bästa metoden är att invänta Task.Yield, vilket tvingar komponenten att bearbeta kod asynkront och rendera under den aktuella batchen med en andra renderingen i en separat batch efter att den utdelade uppgiften har kört fortsättningen.

Överväg följande ändrade IncrementCount-metod, som uppdaterar användargränssnittet två gånger eftersom renderingen som köas av StateHasChanged utförs när uppgiften avbryts med anropet till Task.Yield:

private async Task IncrementCount()
{
    currentCount++;
    StateHasChanged();
    await Task.Yield();
    currentCount++;
}

Var noga med att inte anropa StateHasChanged i onödan, vilket är ett vanligt misstag som medför onödiga renderingskostnader. Koden ska inte behöva anropa StateHasChanged när:

  • Hantera händelser rutinmässigt, synkront eller asynkront, eftersom ComponentBase utlöser en återgivning för de flesta rutinmässiga händelsehanterare.
  • Implementera typisk livscykellogik, till exempel OnInitialized eller OnParametersSetAsync, oavsett om det är synkront eller asynkront, eftersom ComponentBase utlöser en rendering för vanliga livscykelhändelser.

Det kan dock vara klokt att anropa StateHasChanged i de fall som beskrivs i följande avsnitt i den här artikeln:

En asynkron hanterare omfattar flera asynkrona faser

På grund av hur uppgifter definieras i .NET kan en mottagare av en Task bara observera dess slutliga slutförande, inte mellanliggande asynkrona tillstånd. Därför kan ComponentBase bara utlösa rerendering när Task först returneras och när Task slutligen slutförs. Ramverket kan inte veta att rendera om en komponent vid andra mellanliggande punkter, till exempel när en IAsyncEnumerable<T>returnerar data i en följd av intermediära Task. Om du vill återskapa vid mellanliggande punkter anropar du StateHasChanged på dessa punkter.

Överväg följande CounterState1 komponent, som uppdaterar antalet fyra gånger varje gång IncrementCount-metoden körs:

  • Automatiska renderingar sker efter de första och sista stegen av currentCount.
  • Manuella återgivningar utlöses av anrop till StateHasChanged när ramverket inte automatiskt utlöser omritningar vid mellanliggande bearbetningspunkter där currentCount ökar.

CounterState1.razor:

@page "/counter-state-1"

<PageTitle>Counter State 1</PageTitle>

<h1>Counter State Example 1</h1>

<p>
    Current count: @currentCount
</p>

<p>
    <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;

    private async Task IncrementCount()
    {
        currentCount++;
        // Renders here automatically

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        // Renders here automatically
    }
}
@page "/counter-state-1"

<PageTitle>Counter State 1</PageTitle>

<h1>Counter State Example 1</h1>

<p>
    Current count: @currentCount
</p>

<p>
    <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;

    private async Task IncrementCount()
    {
        currentCount++;
        // Renders here automatically

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        // Renders here automatically
    }
}
@page "/counter-state-1"

<p>
    Current count: @currentCount
</p>

<p>
    <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;

    private async Task IncrementCount()
    {
        currentCount++;
        // Renders here automatically

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        // Renders here automatically
    }
}
@page "/counter-state-1"

<p>
    Current count: @currentCount
</p>

<p>
    <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;

    private async Task IncrementCount()
    {
        currentCount++;
        // Renders here automatically

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        // Renders here automatically
    }
}
@page "/counter-state-1"

<p>
    Current count: @currentCount
</p>

<p>
    <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;

    private async Task IncrementCount()
    {
        currentCount++;
        // Renders here automatically

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        // Renders here automatically
    }
}
@page "/counter-state-1"

<p>
    Current count: @currentCount
</p>

<p>
    <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;

    private async Task IncrementCount()
    {
        currentCount++;
        // Renders here automatically

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        // Renders here automatically
    }
}

Ta emot ett anrop från något externt till Blazor renderings- och händelsehanteringssystemet

ComponentBase känner bara till sina egna livscykelmetoder och Blazor-utlösta händelser. ComponentBase känner inte till andra händelser som kan inträffa i kod. Till exempel är alla C#-händelser som genereras av ett anpassat datalager okända för Blazor. För att sådana händelser ska utlösa rerendering för att visa uppdaterade värden i användargränssnittet anropar du StateHasChanged.

Överväg följande CounterState2 komponent som använder System.Timers.Timer för att uppdatera ett antal med jämna mellanrum och anropar StateHasChanged för att uppdatera användargränssnittet:

  • OnTimerCallback körs utanför ett Blazor-hanterat återgivningsflöde eller händelsemeddelande. Därför måste OnTimerCallback anropa StateHasChanged eftersom Blazor inte känner till ändringarna i currentCount i återanropet.
  • Komponenten implementerar IDisposable, där Timer tas bort när ramverket anropar metoden Dispose. Mer information finns i ASP.NET Core Razor-komponentens livscykel.

Eftersom återanropet utförs utanför Blazor:s synkroniseringskontext, måste komponenten omsluta logiken för OnTimerCallback i ComponentBase.InvokeAsync för att överföra den till renderarens synkroniseringskontext. Detta motsvarar marshalling till användargränssnittstråden i andra gränssnittsramverk. StateHasChanged kan bara anropas från renderarens synkroniseringskontext och utlöser ett undantag annars:

System.InvalidOperationException: "Den aktuella tråden är inte associerad med Dispatcher. Använd InvokeAsync() för att växla körning till Dispatcher när återgivnings- eller komponenttillstånd utlöses."

CounterState2.razor:

@page "/counter-state-2"
@using System.Timers
@implements IDisposable

<PageTitle>Counter State 2</PageTitle>

<h1>Counter State Example 2</h1>

<p>
    This counter demonstrates <code>Timer</code> disposal.
</p>

<p>
    Current count: @currentCount
</p>

@code {
    private int currentCount = 0;
    private Timer timer = new(1000);

    protected override void OnInitialized()
    {
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            currentCount++;
            StateHasChanged();
        });
    }

    public void Dispose() => timer.Dispose();
}
@page "/counter-state-2"
@using System.Timers
@implements IDisposable

<PageTitle>Counter State 2</PageTitle>

<h1>Counter State Example 2</h1>

<p>
    This counter demonstrates <code>Timer</code> disposal.
</p>

<p>
    Current count: @currentCount
</p>

@code {
    private int currentCount = 0;
    private Timer timer = new(1000);

    protected override void OnInitialized()
    {
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            currentCount++;
            StateHasChanged();
        });
    }

    public void Dispose() => timer.Dispose();
}
@page "/counter-state-2"
@using System.Timers
@implements IDisposable

<h1>Counter with <code>Timer</code> disposal</h1>

<p>
    Current count: @currentCount
</p>

@code {
    private int currentCount = 0;
    private Timer timer = new(1000);

    protected override void OnInitialized()
    {
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            currentCount++;
            StateHasChanged();
        });
    }

    public void Dispose() => timer.Dispose();
}
@page "/counter-state-2"
@using System.Timers
@implements IDisposable

<h1>Counter with <code>Timer</code> disposal</h1>

<p>
    Current count: @currentCount
</p>

@code {
    private int currentCount = 0;
    private Timer timer = new(1000);

    protected override void OnInitialized()
    {
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            currentCount++;
            StateHasChanged();
        });
    }

    public void Dispose() => timer.Dispose();
}
@page "/counter-state-2"
@using System.Timers
@implements IDisposable

<h1>Counter with <code>Timer</code> disposal</h1>

<p>
    Current count: @currentCount
</p>

@code {
    private int currentCount = 0;
    private Timer timer = new(1000);

    protected override void OnInitialized()
    {
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            currentCount++;
            StateHasChanged();
        });
    }

    public void Dispose() => timer.Dispose();
}
@page "/counter-state-2"
@using System.Timers
@implements IDisposable

<h1>Counter with <code>Timer</code> disposal</h1>

<p>
    Current count: @currentCount
</p>

@code {
    private int currentCount = 0;
    private Timer timer = new Timer(1000);

    protected override void OnInitialized()
    {
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            currentCount++;
            StateHasChanged();
        });
    }

    public void Dispose() => timer.Dispose();
}

Så här renderar du en komponent utanför underträdet som återskapas av en viss händelse

Användargränssnittet kan omfatta:

  1. Skickar en händelse till en komponent.
  2. Ändra ett visst tillstånd.
  3. Återskapa en helt annan komponent som inte är en underordnad till komponenten som tar emot händelsen.

Ett sätt att hantera det här scenariot är att tillhandahålla en tillståndshanterings-klass, ofta som en tjänst för beroendeinjektion (DI), som installeras i flera komponenter. När en komponent anropar en metod på tillståndshanteraren genererar tillståndshanteraren en C#-händelse som sedan tas emot av en oberoende komponent.

Metoder för att hantera tillstånd finns i följande resurser:

För state manager-metoden ligger C#-händelser utanför Blazor återgivningspipeline. Kör StateHasChanged på andra komponenter som du vill återskapa som svar på händelser från tillståndshanteraren.

State Manager-metoden liknar det tidigare fallet med System.Timers.Timer i föregående avsnitt. Eftersom anropsstacken vanligtvis finns kvar i återgivarens synkroniseringskontext, krävs normalt inte anrop till InvokeAsync. Att anropa InvokeAsync krävs endast om logiken undflyr synkroniseringskontexten, till exempel anropar ContinueWith på en Task eller väntar på en Task med ConfigureAwait(false). Mer information finns i avsnittet Ta emot ett anrop från något externt till Blazor renderings- och händelsehanteringssystem.

WebAssembly läser in förloppsindikator för Blazor Web Apps

En indikator för laddningsförlopp finns inte i en app som skapats från Blazor Web App-projektmallen. En ny funktion för lastindikator planeras för en framtida version av .NET. Under tiden kan en app använda anpassad kod för att skapa en indikator för inläsningsförlopp. Mer information finns i ASP.NET Core Blazor startprogram.