Razor-Komponentenrendering in ASP.NET Core

Hinweis

Dies ist nicht die neueste Version dieses Artikels. Informationen zum aktuellen Release finden Sie in der .NET 8-Version dieses Artikels.

Wichtig

Diese Informationen beziehen sich auf ein Vorabversionsprodukt, das vor der kommerziellen Freigabe möglicherweise noch wesentlichen Änderungen unterliegt. Microsoft gibt keine Garantie, weder ausdrücklich noch impliziert, hinsichtlich der hier bereitgestellten Informationen.

Informationen zum aktuellen Release finden Sie in der .NET 8-Version dieses Artikels.

In diesem Artikel wird das Rendering von Razor-Komponenten in ASP.NET Core Blazor-Apps erläutert, z. B. wann StateHasChanged aufgerufen werden muss, um das Rendern einer Komponente manuell auszulösen.

Renderingkonventionen für ComponentBase

Komponenten müssen gerendert werden, wenn sie der Komponentenhierarchie von einer übergeordneten Komponente erstmalig hinzugefügt werden. Dies ist der einzige Zeitpunkt, zu dem eine Komponente gerendert werden muss. Komponenten können zu anderen Zeitpunkten gerendert werden. Dies erfolgt gemäß ihrer eigenen Logik und den entsprechenden Konventionen.

Razor-Komponenten erben standardmäßig von der ComponentBase-Basisklasse. Diese enthält Logik, die das erneute Rendern zu folgenden Zeitpunkten auslöst:

Von ComponentBase geerbte Komponenten überspringen wiederholte Rendervorgänge aufgrund von Parameteraktualisierungen, wenn eine der folgenden Aussagen zutrifft:

  • Alle Parameter stammen aus einer Reihe bekannter Typen† oder einem beliebigen primitiven Typ, der sich seit der Festlegung der vorherigen Gruppe aus Parametern nicht geändert hat.

    †Das Framework von Blazor verwendet integrierte Regeln und Parametertypen für die Änderungserkennung. Diese Regeln und die Typen können jederzeit geändert werden. Weitere Informationen finden Sie unter ChangeDetection-API in der ASP.NET Core Referenzquelle.

    Hinweis

    Dokumentationslinks zur .NET-Referenzquelle laden in der Regel den Standardbranch des Repositorys, der die aktuelle Entwicklung für das nächste Release von .NET darstellt. Um ein Tag für ein bestimmtes Release auszuwählen, wählen Sie diesen mit der Dropdownliste Switch branches or tags (Branches oder Tags wechseln) aus. Weitere Informationen finden Sie unter How to select a version tag of ASP.NET Core source code (dotnet/AspNetCore.Docs #26205) (Auswählen eines Versionstags von ASP.NET Core-Quellcode (dotnet/AspNetCore.Docs #26205)).

  • Die Überschreibung der ShouldRenderMethode der Komponente gibt false zurück (die Standardimplementierung von ComponentBase gibt immer true zurück).

Steuern des Renderingflows

In den meisten Fällen führen ComponentBase-Konventionen zu erneuten Renderingvorgängen für die richtige Teilmenge der Komponenten, nachdem ein Ereignis auftritt. Entwickler müssen in der Regel keine manuelle Logik bereitstellen, damit das Framework die Information erhält, welche Komponenten erneut gerendert werden müssen und wann sie erneut gerendert werden müssen. Der Gesamteffekt der Konventionen des Frameworks besteht darin, dass die Komponente, für die ein Ereignis auftritt, sich selbst erneut rendert. Dies löst rekursiv ein erneutes Rendern der nachfolgenden Komponenten aus, deren Parameterwerte sich möglicherweise geändert haben.

Weitere Informationen zu den Auswirkungen auf die Leistung der Konventionen des Frameworks und dazu, wie Sie die Komponentenhierarchie einer App für das Rendern optimieren können, finden Sie unter Bewährte Methoden für die Leistung von Blazor in ASP.NET Core.

Streamingrendering

Verwenden Sie Streamingrendering mit statischem serverseitigen Rendering (SSR) oder Prerendering, um Inhaltsupdates für den Antwortdatenstrom zu streamen und die Benutzererfahrung für Komponenten zu verbessern, die langwierige asynchrone Aufgaben zum vollständigen Rendern ausführen.

Betrachten Sie beispielsweise eine Komponente, die beim Laden der Seite eine langwierige Datenbankabfrage oder einen Web-API-Aufruf zum Rendern von Daten durchführt. Normalerweise müssen asynchrone Aufgaben, die im Rahmen des Renderns einer serverseitigen Komponente ausgeführt werden, abgeschlossen werden, bevor die gerenderte Antwort gesendet wird, wodurch sich das Laden der Seite verzögern kann. Jede erhebliche Verzögerung beim Rendern der Seite beeinträchtigt die Benutzererfahrung. Um die Benutzererfahrung zu verbessern, wird beim Streamingrendering zunächst die gesamte Seite schnell mit Platzhalterinhalten gerendert, während asynchrone Vorgänge ausgeführt werden. Nach Abschluss der Vorgänge wird der aktualisierte Inhalt über dieselbe Antwortverbindung an den Client gesendet und in das DOM eingefügt.

Für das Streamingrendering darf der Server die Ausgabe nicht puffern. Die Antwortdaten müssen beim Generieren der Daten an den Client fließen. Für Hosts, die eine Pufferung erzwingen, wird das Streamingrendering langsam beeinträchtigt, und die Seite wird ohne Streamingrendering geladen.

Um Inhaltsaktualisierungen bei Verwendung des statischen serverseitigen Renderings (statisches SSR) oder Prerenderings zu streamen, wenden Sie das [StreamRendering(true)]-Attribut auf die Komponente an. Streamingrendering muss explizit aktiviert werden, da gestreamte Updates dazu führen können, dass Inhalte auf der Seite verschoben werden. Komponenten ohne das Attribut übernehmen automatisch Streamingrendering, wenn die übergeordnete Komponente das Feature verwendet. Übergeben Sie false an das Attribut in einer untergeordneten Komponente, um das Feature an dieser Stelle und weiter unten im Teilbaum der Komponente zu deaktivieren. Das Attribut ist funktionsfähig, wenn es auf Komponenten, die von einer Razor Klassenbibliothek bereitgestellt werden, angewendet wird.

Das folgende Beispiel basiert auf der Weather-Komponente in einer App, die aus der Blazor Web-App-Projektvorlage erstellt wurde. Der Aufruf von Task.Delay simuliert das asynchrone Abrufen von Wetterdaten. Die Komponente rendert zunächst Platzhalterinhalte („Loading...“), ohne auf den Abschluss der asynchronen Verzögerung zu warten. Wenn die asynchrone Verzögerung abgeschlossen und der Inhalt der Wetterdaten generiert ist, wird der Inhalt zur Antwort gestreamt und in die Wettervorhersagetabelle eingefügt.

Weather.razor:

@page "/weather"
@attribute [StreamRendering(true)]

...

@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 = ...
    }
}

Unterdrücken der UI-Aktualisierung (ShouldRender)

ShouldRender wird jedes Mal aufgerufen, wenn eine Komponente gerendert wird. Setzen Sie ShouldRender außer Kraft, um die Aktualisierung der Benutzeroberfläche zu verwalten. Wenn die Implementierung true zurückgibt, wird die Benutzeroberfläche aktualisiert.

Selbst wenn ShouldRender außer Kraft gesetzt wird, wird die Komponente immer anfänglich gerendert.

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

Weitere Informationen zu bewährten Methoden für die Leistung in Bezug auf ShouldRenderfinden Sie unter Bewährte Methoden für die Leistung von Blazor in ASP.NET Core.

Gründe für einen Aufruf von StateHasChanged

Wenn Sie StateHasChanged aufrufen, können Sie jederzeit ein Rendering auslösen. Achten Sie jedoch darauf, StateHasChanged nicht unnötigerweise aufzurufen. Dies ist ein häufiger Fehler, durch den unnötige Renderingkosten verursacht werden.

Code sollte unter den folgenden Umständen StateHasChanged nicht aufrufen müssen:

  • Routinemäßige Verarbeitung von Ereignissen (synchron und asynchron), da ComponentBase ein Rendering für die meisten Routingereignishandler auslöst
  • Implementieren typischer Lebenszykluslogik (synchron und asynchron), z. B. OnInitialized oder OnParametersSetAsync, da ComponentBase ein Rendering für typische Lebenszyklusereignisse auslöst

In den Fällen, die in den folgenden Abschnitten dieses Artikels beschrieben werden, kann es jedoch sinnvoll sein, StateHasChanged aufzurufen:

Ein asynchroner Handler beinhaltet mehrere asynchrone Phasen

Aufgrund der Art, wie Aufgaben in .NET definiert werden, kann ein Empfänger von Task nur den endgültigen Abschluss beobachten, keine asynchronen Zwischenzustände. Deshalb kann ComponentBase nur ein erneutes Rendering auslösen, wenn Task zuerst zurückgegeben wird und wenn Task endgültig abgeschlossen ist. Das Framework weiß nicht, dass eine Komponente an anderen Zwischenpunkten erneut gerendert werden soll, zum Beispiel, wenn eine IAsyncEnumerable<T>-Schnittstelle Daten als eine Reihe von dazwischenliegenden Tasks zurückgibt. Wenn Sie an Zwischenpunkten ein erneutes Rendering durchführen möchten, rufen Sie StateHasChanged an diesen Punkten auf.

Betrachten Sie die folgende CounterState1-Komponente, die bei jeder Ausführung der IncrementCount-Methode den Zählerstand viermal aktualisiert:

  • Automatische Rendervorgänge erfolgen nach dem ersten und letzten Inkrement von currentCount.
  • Manuelle Rendervorgänge werden durch Aufrufe von StateHasChanged ausgelöst, wenn das Framework nicht automatisch Rendervorgänge bei Zwischenverarbeitungspunkten auslöst, bei denen currentCount inkrementiert wird.

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"

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

Ein Aufruf von einer externen Quelle an das Blazor-Renderingsystem bzw. -Ereignisverarbeitungssystem wird empfangen

ComponentBase kennt nur die eigenen Lebenszyklusmethoden und von Blazor ausgelösten Ereignisse. ComponentBase hat keine Informationen zu anderen Ereignissen, die möglicherweise im Code auftreten. Alle C#-Ereignisse, die von einem benutzerdefinierten Datenspeicher ausgelöst werden, sind beispielsweise für Blazor nicht bekannt. Damit solche Ereignisse ein erneutes Rendering auslösen, damit aktualisierte Werte auf der Benutzeroberfläche angezeigt werden, rufen Sie StateHasChanged auf.

Berücksichtigen Sie die folgende CounterState2-Komponente, die System.Timers.Timer verwendet, um eine Anzahl in regelmäßigen Abständen zu aktualisieren, und StateHasChanged aufruft, um die Benutzeroberfläche zu aktualisieren:

  • OnTimerCallback wird außerhalb eines von Blazor verwalteten Renderingflows oder einer Ereignisbenachrichtigung ausgeführt. OnTimerCallback muss daher StateHasChanged aufrufen, weil Blazor die Änderungen an currentCount im Rückruf nicht bekannt sind.
  • Die Komponente implementiert IDisposable, wobei Timer verworfen wird, wenn das Framework die Dispose-Methode aufruft. Weitere Informationen finden Sie unter Rendering von Razor-Komponenten in ASP.NET Core.

Da der Rückruf außerhalb des Synchronisierungskontexts von Blazor aufgerufen wird, muss die Komponente die Logik von OnTimerCallback in ComponentBase.InvokeAsync verpacken, um sie in den Synchronisierungskontext des Renderers zu verschieben. Dies ist äquivalent zum Marshallen des Benutzeroberflächenthreads in anderen Benutzeroberflächenframeworks. StateHasChanged kann nur im Synchronisierungskontext des Renderers aufgerufen werden und löst andernfalls eine Ausnahme aus:

System.InvalidOperationException: „Der aktuelle Thread ist nicht dem Dispatcher zugeordnet. Verwenden Sie InvokeAsync() zum Wechseln der Ausführung auf den Dispatcher beim Auslösen des Renderings oder des Komponentenzustands.“

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

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

Eine Komponente soll außerhalb der untergeordneten Struktur gerendert werden, die von einem bestimmten Ereignis erneut gerendert wird

Die Benutzeroberfläche kann Folgendes umfassen:

  1. Senden eines Ereignisses an eine Komponente.
  2. Ändern eines Zustands.
  3. Rendern einer völlig anderen Komponente, die kein Nachkomme der Komponente ist, die das Ereignis empfängt.

Eine Möglichkeit, mit diesem Szenario umzugehen, ist die Bereitstellung einer Zustandsverwaltungsklasse, häufig als DI-Dienst (Dependency Injection, Abhängigkeitsinjektion), der in mehrere Komponenten injiziert wird. Wenn eine Komponente eine Methode für den Status-Manager aufruft, löst der Status-Manager ein C#-Ereignis aus, das dann von einer unabhängigen Komponente empfangen wird.

Ansätze zum Verwalten des Zustands finden Sie in den folgenden Ressourcen:

Für den Zustands-Manager-Ansatz befinden sich C#-Ereignisse außerhalb der Blazor-Renderingpipeline. Rufen Sie StateHasChanged für andere Komponenten auf, die Sie als Reaktion auf die Ereignisse des Zustands-Managers erneut rendern möchten.

Der Zustands-Manager-Ansatz ähnelt dem Fall mit System.Timers.Timer im vorherigen Abschnitt. Da die Ausführungsaufrufliste in der Regel im Synchronisierungskontext des Renderers verbleibt, ist der Aufruf von InvokeAsync normalerweise nicht erforderlich. Der Aufruf von InvokeAsync ist nur erforderlich, wenn sich die Logik außerhalb des Synchronisierungskontexts befindet, z. B. wenn ContinueWith für Task aufgerufen wird oder Task mit ConfigureAwait(false) erwartet wird. Weitere Informationen finden Sie im Abschnitt Ein Aufruf von einer externen Quelle an das Blazor-Renderingsystem bzw. -Ereignisverarbeitungssystem wird empfangen.

WebAssembly-Ladestatusanzeige für Blazor-Web Apps

Eine Ladestatusanzeige ist in einer App, die aus der Blazor-Web App-Projektvorlage erstellt wurde, nicht vorhanden. Für eine zukünftige Version von .NET ist ein neues Feature zur Ladestatusanzeige geplant. In der Zwischenzeit kann eine App benutzerdefinierten Code übernehmen, um eine Ladestatusanzeige zu erstellen. Weitere Informationen finden Sie unter Starten von ASP.NET CoreBlazor.