renderowanie składników ASP.NET Core Razor

Uwaga

Nie jest to najnowsza wersja tego artykułu. Aby zapoznać się z bieżącą wersją, zapoznaj się z wersją tego artykułu platformy .NET 8.

Ważne

Te informacje odnoszą się do produktu w wersji wstępnej, który może zostać znacząco zmodyfikowany, zanim zostanie wydany komercyjnie. Firma Microsoft nie udziela żadnych gwarancji, jawnych lub domniemanych, w odniesieniu do informacji podanych w tym miejscu.

Aby zapoznać się z bieżącą wersją, zapoznaj się z wersją tego artykułu platformy .NET 8.

W tym artykule opisano Razor renderowanie składników w aplikacjach ASP.NET Core Blazor , w tym czas wywołania StateHasChanged w celu ręcznego wyzwolenia składnika do renderowania.

Konwencje renderowania dla ComponentBase

Składniki muszą być renderowane po pierwszym dodaniu ich do hierarchii składników przez składnik nadrzędny. Jest to jedyny czas renderowania składnika. Składniki mogą być renderowane w innych momentach zgodnie z własną logiką i konwencjami.

Domyślnie Razor składniki dziedziczą z klasy bazowej ComponentBase , która zawiera logikę wyzwalającą rerendering w następujących godzinach:

Składniki dziedziczone po ComponentBase pomiń rerenders z powodu aktualizacji parametrów, jeśli którakolwiek z następujących wartości jest prawdziwa:

Sterowanie przepływem renderowania

W większości przypadków ComponentBase konwencje powodują prawidłowy podzestaw składników rerenders po wystąpieniu zdarzenia. Deweloperzy nie są zwykle zobowiązani do zapewnienia logiki ręcznej, aby poinformować platformę, które składniki rerender i kiedy je rerender. Ogólny wpływ konwencji struktury polega na tym, że składnik odbierający sam rerenders zdarzenia, który rekursywnie wyzwala rerendering składników potomnych, których wartości parametrów mogły ulec zmianie.

Aby uzyskać więcej informacji na temat wpływu na wydajność konwencji platformy i sposobu optymalizowania hierarchii składników aplikacji na potrzeby renderowania, zobacz ASP.NET Core performance best practices (Najlepsze rozwiązania dotyczące wydajności podstawowejBlazor).

Renderowanie przesyłania strumieniowego

Renderowanie strumieniowe przy użyciu renderowania statycznego po stronie serwera (statyczne SSR) lub wstępne przesyłanie strumieniowe aktualizacji zawartości w strumieniu odpowiedzi i ulepszanie środowiska użytkownika dla składników, które wykonują długotrwałe zadania asynchroniczne w celu pełnego renderowania.

Rozważmy na przykład składnik, który tworzy długotrwałe zapytanie bazy danych lub wywołanie internetowego interfejsu API w celu renderowania danych podczas ładowania strony. Zwykle zadania asynchroniczne wykonywane w ramach renderowania składnika po stronie serwera muszą zostać wykonane przed wysłaniem renderowanej odpowiedzi, co może opóźnić ładowanie strony. Wszelkie znaczne opóźnienia w renderowaniu strony szkodzą środowisku użytkownika. Aby poprawić środowisko użytkownika, renderowanie strumieniowe początkowo renderuje całą stronę szybko z zawartością zastępczą podczas wykonywania operacji asynchronicznych. Po zakończeniu operacji zaktualizowana zawartość jest wysyłana do klienta na tym samym połączeniu odpowiedzi i poprawiana do modelu DOM.

Renderowanie strumieniowe wymaga, aby serwer unikał buforowania danych wyjściowych. Dane odpowiedzi muszą przepływać do klienta w miarę generowania danych. W przypadku hostów, które wymuszają buforowanie, renderowanie przesyłania strumieniowego działa bezpiecznie, a strona jest ładowana bez renderowania strumieniowego.

Aby przesyłać strumieniowo aktualizacje zawartości podczas korzystania ze statycznego renderowania po stronie serwera (statycznego SSR) lub prerenderingu, zastosuj [StreamRendering(true)] atrybut do składnika. Renderowanie przesyłania strumieniowego musi być jawnie włączone, ponieważ przesyłane strumieniowo aktualizacje mogą spowodować zmianę zawartości na stronie. Składniki bez atrybutu automatycznie przyjmują renderowanie przesyłania strumieniowego, jeśli składnik nadrzędny używa funkcji. Przekaż false do atrybutu w składniku podrzędnym, aby wyłączyć tę funkcję w tym momencie i dalej w dół poddrzewa składnika. Atrybut jest funkcjonalny po zastosowaniu do składników dostarczanych przez bibliotekę Razorklas.

Poniższy przykład jest oparty na składniku Weather w aplikacji utworzonej Blazor na podstawie szablonu projektu aplikacji internetowej. Wywołanie w celu Task.Delay symulowania asynchronicznego pobierania danych pogodowych. Składnik początkowo renderuje zawartość symboli zastępczych ("Loading...") bez oczekiwania na zakończenie opóźnienia asynchronicznego. Po zakończeniu opóźnienia asynchronicznego i wygenerowaniu zawartości danych pogodowych zawartość jest przesyłana strumieniowo do odpowiedzi i poprawiana w tabeli prognozy pogody.

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

Pomijanie odświeżania interfejsu użytkownika (ShouldRender)

ShouldRender jest wywoływany za każdym razem, gdy składnik jest renderowany. Zastąpić ShouldRender zarządzanie odświeżaniem interfejsu użytkownika. Jeśli implementacja zwróci truewartość , interfejs użytkownika zostanie odświeżony.

Nawet jeśli ShouldRender jest zastępowany, składnik jest zawsze renderowany.

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

Aby uzyskać więcej informacji na temat najlepszych rozwiązań dotyczących wydajności odnoszących się do ShouldRenderprogramu , zobacz ASP.NET Core Blazor performance best practices (Najlepsze rozwiązania dotyczące wydajności rdzenia).

Kiedy należy zadzwonić StateHasChanged

Wywołanie StateHasChanged umożliwia wyzwalanie renderowania w dowolnym momencie. Należy jednak uważać, aby StateHasChanged nie wywoływać niepotrzebnie, co jest typowym błędem, który nakłada niepotrzebne koszty renderowania.

Kod nie powinien być wywoływany StateHasChanged , gdy:

  • Rutynowo obsługa zdarzeń, zarówno synchronicznie, jak i asynchronicznie, ponieważ ComponentBase wyzwala renderowanie dla większości rutynowych procedur obsługi zdarzeń.
  • Implementowanie typowej logiki cyklu życia, takiej jak OnInitialized lub OnParametersSetAsync, zarówno synchronicznie, jak i asynchronicznie, ponieważ ComponentBase wyzwala renderowanie dla typowych zdarzeń cyklu życia.

Jednak wywołanie StateHasChanged w przypadkach opisanych w poniższych sekcjach tego artykułu może mieć sens:

Asynchroniczna procedura obsługi obejmuje wiele faz asynchronicznych

Ze względu na sposób, w jaki zadania są zdefiniowane na platformie .NET, odbiornik obiektu Task może obserwować jego końcowe zakończenie, a nie pośrednie stany asynchroniczne. W związku z tym może wyzwalać rerendering tylko wtedy, ComponentBase gdy Task element jest zwracany po raz pierwszy i po zakończeniu Task . Platforma nie może wiedzieć, aby rerender składnika w innych punktach pośrednich, takich jak gdy IAsyncEnumerable<T>dane są zwracane w serii s pośrednichTask. Jeśli chcesz rerender w punktach pośrednich, wywołaj je StateHasChanged w tych punktach.

Rozważmy następujący CounterState1 składnik, który aktualizuje liczbę cztery razy za każdym razem, gdy metoda jest wykonywana IncrementCount :

  • Automatyczne renderowanie występuje po pierwszych i ostatnich przyrostach elementu currentCount.
  • Renderowanie ręczne jest wyzwalane przez wywołania, StateHasChanged gdy platforma nie wyzwala automatycznie rerenders w punktach przetwarzania pośredniego, w których currentCount jest zwiększana.

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

Odbieranie wywołania z elementu zewnętrznego Blazor do systemu renderowania i obsługi zdarzeń

ComponentBase wie tylko o własnych metodach cyklu życia i Blazorzdarzeniach wyzwalanych. ComponentBase nie wie o innych zdarzeniach, które mogą wystąpić w kodzie. Na przykład wszystkie zdarzenia języka C# zgłaszane przez niestandardowy magazyn danych są nieznane.Blazor Aby takie zdarzenia wyzwalały rerendering w celu wyświetlenia zaktualizowanych wartości w interfejsie użytkownika, wywołaj metodę StateHasChanged.

Należy wziąć pod uwagę następujący CounterState2 składnik, który używa System.Timers.Timer do aktualizowania liczby w regularnych odstępach czasu i wywołań StateHasChanged w celu zaktualizowania interfejsu użytkownika:

  • OnTimerCallback działa poza dowolnym Blazorprzepływem renderowania zarządzanego lub powiadomieniem o zdarzeniach. W związku z tym należy wywołać metodę StateHasChanged , OnTimerCallback ponieważ Blazor nie jest świadoma zmian currentCount w wywołaniu zwrotnym.
  • Składnik implementuje IDisposableelement , gdzie Timer element jest usuwany, gdy struktura wywołuje metodę Dispose . Aby uzyskać więcej informacji, zobacz Cykl życia składników platformy ASP.NET Core Razor.

Ponieważ wywołanie zwrotne jest wywoływane poza Blazorkontekstem synchronizacji, składnik musi opakowować logikę OnTimerCallback elementu , ComponentBase.InvokeAsync aby przenieść go do kontekstu synchronizacji modułu renderowania. Jest to odpowiednik marshalingu do wątku interfejsu użytkownika w innych strukturach interfejsu użytkownika. StateHasChanged Może być wywoływany tylko z kontekstu synchronizacji modułu renderowania i zgłasza wyjątek w przeciwnym razie:

System.InvalidOperationException: "Bieżący wątek nie jest skojarzony z dyspozytorem. Użyj metody InvokeAsync(), aby przełączyć wykonywanie na dyspozytor podczas wyzwalania renderowania lub stanu składnika.

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

Aby renderować składnik poza poddrzewem, który jest rerendered przez określone zdarzenie

Interfejs użytkownika może obejmować:

  1. Wysyłanie zdarzenia do jednego składnika.
  2. Zmiana stanu.
  3. Rerendering zupełnie inny składnik, który nie jest elementem potomnym składnika odbierającego zdarzenie.

Jednym ze sposobów radzenia sobie z tym scenariuszem jest zapewnienie klasy zarządzania stanem, często jako usługa wstrzykiwania zależności (DI), wstrzykiwana do wielu składników. Gdy jeden składnik wywołuje metodę w menedżerze stanu, menedżer stanu zgłasza zdarzenie języka C#, które jest następnie odbierane przez niezależny składnik.

Aby zapoznać się z metodami zarządzania stanem, zobacz następujące zasoby:

W przypadku podejścia menedżera stanu zdarzenia języka C# znajdują się poza potokiem renderowania Blazor . Wywołaj StateHasChanged inne składniki, które mają być rerender w odpowiedzi na zdarzenia menedżera stanu.

Podejście menedżera stanu jest podobne do wcześniejszego przypadku w System.Timers.Timer poprzedniej sekcji. Ponieważ stos wywołań wykonywania zwykle pozostaje w kontekście synchronizacji modułu renderowania, wywołanie InvokeAsync nie jest zwykle wymagane. Wywołanie InvokeAsync jest wymagane tylko wtedy, gdy logika uniknie kontekstu synchronizacji, na przykład wywołanie ContinueWith elementu lub oczekiwanie na Task element Task za pomocą ConfigureAwait(false)polecenia . Aby uzyskać więcej informacji, zobacz sekcję Odbieranie wywołania z elementu zewnętrznego Blazor do systemu renderowania i obsługi zdarzeń.

Wskaźnik postępu ładowania zestawu WebAssembly dla Blazor usługi Web Apps

Wskaźnik postępu ładowania nie jest obecny w aplikacji utworzonej Blazor na podstawie szablonu projektu aplikacji internetowej. Planowana jest nowa funkcja wskaźnika postępu ładowania dla przyszłej wersji platformy .NET. W międzyczasie aplikacja może przyjąć niestandardowy kod, aby utworzyć wskaźnik postępu ładowania. Aby uzyskać więcej informacji, zobacz uruchamianie ASP.NET CoreBlazor.