Udostępnij za pomocą


kontekst synchronizacji ASP.NET Core Blazor

Uwaga

Nie jest to najnowsza wersja tego artykułu. Aby zapoznać się z aktualną wersją, zobacz artykuł w wersji .NET 10.

Ostrzeżenie

Ta wersja ASP.NET Core nie jest już obsługiwana. Aby uzyskać więcej informacji, zobacz zasady pomocy technicznej platformy .NET i platformy .NET Core. Aby zapoznać się z bieżącą wersją, zobacz wersję tego artykułu dla platformy .NET 9.

Platforma Blazor używa kontekstu synchronizacji (SynchronizationContext), aby wymuszać pojedynczy logiczny wątek wykonywania. Metody cyklu życia składnika i wywołania zwrotne zdarzeń zgłaszane przez Blazor są wykonywane w kontekście synchronizacji.

BlazorKontekst synchronizacji po stronie serwera próbuje emulować jednowątkowe środowisko, aby ściśle pasować do modelu WebAssembly w przeglądarce, który jest jednowątkowy. Ta emulacja jest ograniczona tylko do pojedynczego obwodu, co oznacza, że dwa różne obwody mogą działać równolegle. W dowolnym momencie w obwodzie praca odbywa się na jednym wątku, co sprawia wrażenie istnienia tylko jednego wątku logicznego. Żadne dwie operacje nie wykonują się równocześnie w tym samym obwodzie.

Pojedynczy logiczny wątek wykonywania nie oznacza asynchronicznego przepływu sterowania. Składnik jest reentrantny w każdym miejscu, w którym oczekuje na niekompletne Task. Metody cyklu życia lub metody usuwania komponentów mogą być wywoływane przed wznowieniem przepływu sterowania asynchronicznego po oczekiwaniu na zakończenie Task. W związku z tym składnik musi upewnić się, że jest w prawidłowym stanie przed oczekiwaniem na potencjalnie niekompletne Task. W szczególności składnik musi zapewnić, że jest w prawidłowym stanie do renderowania, gdy OnInitializedAsync lub OnParametersSetAsync powróci. Jeśli którakolwiek z tych metod zwraca niekompletne Task, odpowiedzialni muszą upewnić się, że część metody, która kończy się synchronicznie, pozostawia składnik w prawidłowym stanie do renderowania.

Inną implikacją składników ponownego wejścia jest to, że metoda nie może przełożyć Task na później, do czasu zakończenia metody, przekazując ją do ComponentBase.InvokeAsync. Wywołanie ComponentBase.InvokeAsync może odroczyć Task jedynie do momentu, gdy zostanie osiągnięty następny operator await.

Składniki mogą implementować IDisposable lub IAsyncDisposable, aby wywoływać asynchroniczne metody przy użyciu CancellationToken z CancellationTokenSource, który jest anulowany, gdy składnik zostanie usunięty. Jednak to naprawdę zależy od scenariusza. Autor składnika decyduje o tym, czy jest to prawidłowe zachowanie. Na przykład, jeśli składnik SaveButton jest zaimplementowany w taki sposób, że zapisuje pewne lokalne dane do bazy danych po wybraniu przycisku „Zapisz”, autor może zaplanować odrzucenie zmian, jeśli użytkownik szybko przejdzie do innej strony. Może to spowodować, że składnik zostanie usunięty przed zakończeniem asynchronicznego procesu zapisywania.

Jednorazowy składnik może sprawdzić usunięcie po oczekiwaniu na dowolne Task, które nie otrzymują składnika CancellationToken. Niekompletne elementy oznaczone jako Taskmogą również utrudnić zbieranie odpadów pamięci usuniętego składnika.

ComponentBase ignoruje wyjątki spowodowane anulowaniem Task (dokładniej ignoruje wszystkie wyjątki, jeśli oczekiwane Tasksą anulowane), więc metody składników nie muszą obsługiwać TaskCanceledException i OperationCanceledException.

ComponentBase nie może postępować zgodnie z poprzednimi wytycznymi, ponieważ nie definiuje koncepcji tego, co stanowi prawidłowy stan składnika pochodnego i nie implementuje IDisposable ani IAsyncDisposable. Jeśli OnInitializedAsync zwraca niekompletne Task, które nie używa CancellationToken, i składnik zostanie usunięty zanim Task zostanie ukończony, ComponentBase nadal wywołuje OnParametersSet i oczekuje OnParametersSetAsync. Jeśli składnik jednorazowy nie używa CancellationToken, OnParametersSet i OnParametersSetAsync należy sprawdzić, czy składnik jest usunięty.

Unikaj wywołań blokujących wątki

Ogólnie rzecz biorąc, nie należy wywoływać następujących metod w składnikach. Poniższe metody blokują wątek wykonania, a tym samym uniemożliwiają aplikacji wznowienie pracy, dopóki bazowy komponent Task nie zostanie ukończony.

Uwaga

Przykłady dokumentacji Blazor, które używają wymienionych w tej sekcji metod blokujących wątki, służą wyłącznie celom demonstracyjnym i nie stanowią zaleceń programistycznych. Na przykład kilka przykładów kodu składników symuluje długo działający proces przez wywołanie metody Thread.Sleep.

Wywoływanie metod składników zewnętrznie w celu zaktualizowania stanu

Jeśli składnik musi być aktualizowany na podstawie zdarzenia zewnętrznego, takiego jak czasomierz lub inne powiadomienie, użyj metody InvokeAsync, która kieruje wykonywanie kodu do kontekstu synchronizacji platformy Blazor. Na przykład rozważmy następującą usługę powiadamiania, która może powiadamiać dowolny nasłuchujący składnik o zaktualizowanym stanie. Metodę Update można wywoływać z dowolnego miejsca w aplikacji.

TimerService.cs:

namespace BlazorSample;

public class TimerService(NotifierService notifier, 
    ILogger<TimerService> logger) : IDisposable
{
    private int elapsedCount;
    private static readonly TimeSpan heartbeatTickRate = TimeSpan.FromSeconds(5);
    private readonly ILogger<TimerService> logger = logger;
    private readonly NotifierService notifier = notifier;
    private PeriodicTimer? timer;

    public async Task Start()
    {
        if (timer is null)
        {
            timer = new(heartbeatTickRate);
            logger.LogInformation("Started");

            using (timer)
            {
                while (await timer.WaitForNextTickAsync())
                {
                    elapsedCount += 1;
                    await notifier.Update("elapsedCount", elapsedCount);
                    logger.LogInformation("ElapsedCount {Count}", elapsedCount);
                }
            }
        }
    }

    public void Stop()
    {
        if (timer is not null)
        {
            timer.Dispose();
            timer = null;
            logger.LogInformation("Stopped");
        }
    }

    public void Dispose()
    {
        timer?.Dispose();

        // The following prevents derived types that introduce a
        // finalizer from needing to re-implement IDisposable.
        GC.SuppressFinalize(this);
    }
}
namespace BlazorSample;

public class TimerService(NotifierService notifier, 
    ILogger<TimerService> logger) : IDisposable
{
    private int elapsedCount;
    private static readonly TimeSpan heartbeatTickRate = TimeSpan.FromSeconds(5);
    private readonly ILogger<TimerService> logger = logger;
    private readonly NotifierService notifier = notifier;
    private PeriodicTimer? timer;

    public async Task Start()
    {
        if (timer is null)
        {
            timer = new(heartbeatTickRate);
            logger.LogInformation("Started");

            using (timer)
            {
                while (await timer.WaitForNextTickAsync())
                {
                    elapsedCount += 1;
                    await notifier.Update("elapsedCount", elapsedCount);
                    logger.LogInformation("ElapsedCount {Count}", elapsedCount);
                }
            }
        }
    }

    public void Dispose()
    {
        timer?.Dispose();

        // The following prevents derived types that introduce a
        // finalizer from needing to re-implement IDisposable.
        GC.SuppressFinalize(this);
    }
}
public class TimerService : IDisposable
{
    private int elapsedCount;
    private static readonly TimeSpan heartbeatTickRate = TimeSpan.FromSeconds(5);
    private readonly ILogger<TimerService> logger;
    private readonly NotifierService notifier;
    private PeriodicTimer? timer;

    public TimerService(NotifierService notifier,
        ILogger<TimerService> logger)
    {
        this.notifier = notifier;
        this.logger = logger;
    }

    public async Task Start()
    {
        if (timer is null)
        {
            timer = new(heartbeatTickRate);
            logger.LogInformation("Started");

            using (timer)
            {
                while (await timer.WaitForNextTickAsync())
                {
                    elapsedCount += 1;
                    await notifier.Update("elapsedCount", elapsedCount);
                    logger.LogInformation("elapsedCount: {ElapsedCount}", elapsedCount);
                }
            }
        }
    }

    public void Dispose()
    {
        timer?.Dispose();
    }
}
public class TimerService : IDisposable
{
    private int elapsedCount;
    private static readonly TimeSpan heartbeatTickRate = TimeSpan.FromSeconds(5);
    private readonly ILogger<TimerService> logger;
    private readonly NotifierService notifier;
    private PeriodicTimer? timer;

    public TimerService(NotifierService notifier,
        ILogger<TimerService> logger)
    {
        this.notifier = notifier;
        this.logger = logger;
    }

    public async Task Start()
    {
        if (timer is null)
        {
            timer = new(heartbeatTickRate);
            logger.LogInformation("Started");

            using (timer)
            {
                while (await timer.WaitForNextTickAsync())
                {
                    elapsedCount += 1;
                    await notifier.Update("elapsedCount", elapsedCount);
                    logger.LogInformation("elapsedCount: {ElapsedCount}", elapsedCount);
                }
            }
        }
    }

    public void Dispose()
    {
        timer?.Dispose();
    }
}
using System;
using System.Timers;
using Microsoft.Extensions.Logging;

public class TimerService : IDisposable
{
    private int elapsedCount;
    private readonly ILogger<TimerService> logger;
    private readonly NotifierService notifier;
    private Timer timer;

    public TimerService(NotifierService notifier, ILogger<TimerService> logger)
    {
        this.notifier = notifier;
        this.logger = logger;
    }

    public void Start()
    {
        if (timer is null)
        {
            timer = new();
            timer.AutoReset = true;
            timer.Interval = 10000;
            timer.Elapsed += HandleTimer;
            timer.Enabled = true;
            logger.LogInformation("Started");
        }
    }

    private async void HandleTimer(object source, ElapsedEventArgs e)
    {
        elapsedCount += 1;
        await notifier.Update("elapsedCount", elapsedCount);
        logger.LogInformation("elapsedCount: {ElapsedCount}", elapsedCount);
    }

    public void Dispose()
    {
        timer?.Dispose();
    }
}
using System;
using System.Timers;
using Microsoft.Extensions.Logging;

public class TimerService : IDisposable
{
    private int elapsedCount;
    private readonly ILogger<TimerService> logger;
    private readonly NotifierService notifier;
    private Timer timer;

    public TimerService(NotifierService notifier, ILogger<TimerService> logger)
    {
        this.notifier = notifier;
        this.logger = logger;
    }

    public void Start()
    {
        if (timer is null)
        {
            timer = new Timer();
            timer.AutoReset = true;
            timer.Interval = 10000;
            timer.Elapsed += HandleTimer;
            timer.Enabled = true;
            logger.LogInformation("Started");
        }
    }

    private async void HandleTimer(object source, ElapsedEventArgs e)
    {
        elapsedCount += 1;
        await notifier.Update("elapsedCount", elapsedCount);
        logger.LogInformation("elapsedCount: {ElapsedCount}", elapsedCount);
    }

    public void Dispose()
    {
        timer?.Dispose();
    }
}

NotifierService.cs:

namespace BlazorSample;

public class NotifierService
{
    public async Task Update(string key, int value)
    {
        if (Notify != null)
        {
            await Notify.Invoke(key, value);
        }
    }

    public event Func<string, int, Task>? Notify;
}
namespace BlazorSample;

public class NotifierService
{
    public async Task Update(string key, int value)
    {
        if (Notify != null)
        {
            await Notify.Invoke(key, value);
        }
    }

    public event Func<string, int, Task>? Notify;
}
public class NotifierService
{
    public async Task Update(string key, int value)
    {
        if (Notify != null)
        {
            await Notify.Invoke(key, value);
        }
    }

    public event Func<string, int, Task>? Notify;
}
public class NotifierService
{
    public async Task Update(string key, int value)
    {
        if (Notify != null)
        {
            await Notify.Invoke(key, value);
        }
    }

    public event Func<string, int, Task>? Notify;
}
using System;
using System.Threading.Tasks;

public class NotifierService
{
    public async Task Update(string key, int value)
    {
        if (Notify != null)
        {
            await Notify.Invoke(key, value);
        }
    }

    public event Func<string, int, Task> Notify;
}
using System;
using System.Threading.Tasks;

public class NotifierService
{
    public async Task Update(string key, int value)
    {
        if (Notify != null)
        {
            await Notify.Invoke(key, value);
        }
    }

    public event Func<string, int, Task> Notify;
}

Rejestrowanie usług:

  • W przypadku programowania po stronie klienta zarejestruj usługi jako singletony w pliku po stronie Program klienta:

    builder.Services.AddSingleton<NotifierService>();
    builder.Services.AddSingleton<TimerService>();
    
  • W przypadku programowania po stronie serwera zarejestruj usługi w zakresie w pliku serwera Program :

    builder.Services.AddScoped<NotifierService>();
    builder.Services.AddScoped<TimerService>();
    

Użyj obiektu NotifierService, aby zaktualizować składnik.

Notifications.razor:

@page "/notifications"
@implements IDisposable
@inject NotifierService Notifier
@inject TimerService Timer

<PageTitle>Notifications</PageTitle>

<h1>Notifications Example</h1>

<h2>Timer Service</h2>

<button @onclick="StartTimer">Start Timer</button>

<button @onclick="StopTimer">Stop Timer</button>

<h2>Notifications</h2>

<p>
    Status:
    @if (lastNotification.key is not null)
    {
        <span>@lastNotification.key = @lastNotification.value</span>
    }
    else
    {
        <span>Awaiting notification</span>
    }
</p>

@code {
    private (string key, int value) lastNotification;

    protected override void OnInitialized() => Notifier.Notify += OnNotify;

    public async Task OnNotify(string key, int value)
    {
        await InvokeAsync(() =>
        {
            lastNotification = (key, value);
            StateHasChanged();
        });
    }

    private void StartTimer() => _ = Task.Run(Timer.Start);

    private void StopTimer() => Timer.Stop();

    public void Dispose() => Notifier.Notify -= OnNotify;
}

Notifications.razor:

@page "/notifications"
@implements IDisposable
@inject NotifierService Notifier
@inject TimerService Timer

<PageTitle>Notifications</PageTitle>

<h1>Notifications Example</h1>

<h2>Timer Service</h2>

<button @onclick="StartTimer">Start Timer</button>

<h2>Notifications</h2>

<p>
    Status:
    @if (lastNotification.key is not null)
    {
        <span>@lastNotification.key = @lastNotification.value</span>
    }
    else
    {
        <span>Awaiting notification</span>
    }
</p>

@code {
    private (string key, int value) lastNotification;

    protected override void OnInitialized() => Notifier.Notify += OnNotify;

    public async Task OnNotify(string key, int value)
    {
        await InvokeAsync(() =>
        {
            lastNotification = (key, value);
            StateHasChanged();
        });
    }

    private void StartTimer() => _ = Task.Run(Timer.Start);

    public void Dispose() => Notifier.Notify -= OnNotify;
}

ReceiveNotifications.razor:

@page "/receive-notifications"
@implements IDisposable
@inject NotifierService Notifier
@inject TimerService Timer

<h1>Receive Notifications</h1>

<h2>Timer Service</h2>

<button @onclick="StartTimer">Start Timer</button>

<h2>Notifications</h2>

<p>
    Status:
    @if (lastNotification.key is not null)
    {
        <span>@lastNotification.key = @lastNotification.value</span>
    }
    else
    {
        <span>Awaiting notification</span>
    }
</p>

@code {
    private (string key, int value) lastNotification;

    protected override void OnInitialized()
    {
        Notifier.Notify += OnNotify;
    }

    public async Task OnNotify(string key, int value)
    {
        await InvokeAsync(() =>
        {
            lastNotification = (key, value);
            StateHasChanged();
        });
    }

    private void StartTimer()
    {
        _ = Task.Run(Timer.Start);
    }

    public void Dispose()
    {
        Notifier.Notify -= OnNotify;
    }
}

ReceiveNotifications.razor:

@page "/receive-notifications"
@implements IDisposable
@inject NotifierService Notifier
@inject TimerService Timer

<h1>Receive Notifications</h1>

<h2>Timer Service</h2>

<button @onclick="StartTimer">Start Timer</button>

<h2>Notifications</h2>

<p>
    Status:
    @if (lastNotification.key is not null)
    {
        <span>@lastNotification.key = @lastNotification.value</span>
    }
    else
    {
        <span>Awaiting notification</span>
    }
</p>

@code {
    private (string key, int value) lastNotification;

    protected override void OnInitialized()
    {
        Notifier.Notify += OnNotify;
    }

    public async Task OnNotify(string key, int value)
    {
        await InvokeAsync(() =>
        {
            lastNotification = (key, value);
            StateHasChanged();
        });
    }

    private void StartTimer()
    {
        _ = Task.Run(Timer.Start);
    }

    public void Dispose()
    {
        Notifier.Notify -= OnNotify;
    }
}

ReceiveNotifications.razor:

@page "/receive-notifications"
@implements IDisposable
@inject NotifierService Notifier
@inject TimerService Timer

<h1>Receive Notifications</h1>

<h2>Timer Service</h2>

<button @onclick="StartTimer">Start Timer</button>

<h2>Notifications</h2>

<p>
    Status:
    @if (lastNotification.key is not null)
    {
        <span>@lastNotification.key = @lastNotification.value</span>
    }
    else
    {
        <span>Awaiting notification</span>
    }
</p>

@code {
    private (string key, int value) lastNotification;

    protected override void OnInitialized()
    {
        Notifier.Notify += OnNotify;
    }

    public async Task OnNotify(string key, int value)
    {
        await InvokeAsync(() =>
        {
            lastNotification = (key, value);
            StateHasChanged();
        });
    }

    private void StartTimer()
    {
        Timer.Start();
    }

    public void Dispose()
    {
        Notifier.Notify -= OnNotify;
    }
}

ReceiveNotifications.razor:

@page "/receive-notifications"
@implements IDisposable
@inject NotifierService Notifier
@inject TimerService Timer

<h1>Receive Notifications</h1>

<h2>Timer Service</h2>

<button @onclick="StartTimer">Start Timer</button>

<h2>Notifications</h2>

<p>
    Status:
    @if (lastNotification.key != null)
    {
        <span>@lastNotification.key = @lastNotification.value</span>
    }
    else
    {
        <span>Awaiting notification</span>
    }
</p>

@code {
    private (string key, int value) lastNotification;

    protected override void OnInitialized()
    {
        Notifier.Notify += OnNotify;
    }

    public async Task OnNotify(string key, int value)
    {
        await InvokeAsync(() =>
        {
            lastNotification = (key, value);
            StateHasChanged();
        });
    }

    private void StartTimer()
    {
        Timer.Start();
    }

    public void Dispose()
    {
        Notifier.Notify -= OnNotify;
    }
}

W powyższym przykładzie:

  • Czasomierz jest inicjowany poza kontekstem synchronizacji Blazor przy użyciu _ = Task.Run(Timer.Start).
  • NotifierService wywołuje metodę OnNotify składnika. InvokeAsync służy do przełączania się do poprawnego kontekstu i kolejkowania ponownego renderowania. Aby uzyskać więcej informacji, zobacz Renderowanie składników platformy ASP.NET Core Razor.
  • Komponent implementuje IDisposable. Delegat OnNotify jest odsubskrybowywany w metodzie Dispose, która jest wywoływana przez framework, gdy składnik zostanie usunięty. Aby uzyskać więcej informacji, zobacz usuwanie komponentów w ASP.NET Core Razor.
  • NotifierService wywołuje metodę OnNotify składnika poza jego kontekstem synchronizacji Blazor. InvokeAsync służy do przełączania się do poprawnego kontekstu i kolejkowania ponownego renderowania. Aby uzyskać więcej informacji, zobacz Renderowanie składników platformy ASP.NET Core Razor.
  • Komponent implementuje IDisposable. Delegat OnNotify jest odsubskrybowywany w metodzie Dispose, która jest wywoływana przez framework, gdy składnik zostanie usunięty. Aby uzyskać więcej informacji, zobacz usuwanie komponentów w ASP.NET Core Razor.

Ważne

Razor Jeśli składnik definiuje zdarzenie wyzwalane z wątku w tle, składnik może być wymagany do przechwycenia i przywrócenia kontekstu wykonywania (ExecutionContext) w momencie zarejestrowania programu obsługi. Aby uzyskać więcej informacji, zobacz Wywoływanie InvokeAsync(StateHasChanged) powoduje powrót strony do domyślnej kultury (dotnet/aspnetcore #28521).

Aby wysłać przechwycone wyjątki z tła TimerService do składnika, aby traktować je podobnie jak normalne wyjątki zdarzeń cyklu życia, zobacz sekcję Obsługa przechwyconych wyjątków poza cyklem życia komponentu Razor.

Obsługa przechwyconych wyjątków poza cyklem życia Razor komponentu

Użyj ComponentBase.DispatchExceptionAsync w komponencie Razor do przetwarzania wyjątków zgłoszonych poza stosem wywołań cyklu życia komponentu. Pozwala to kodowi składnika traktować wyjątki tak, jakby były wyjątkami metody cyklu życia. Następnie mechanizmy obsługi błędów, Blazortakie jak granice błędów, mogą przetwarzać wyjątki.

Uwaga

ComponentBase.DispatchExceptionAsync jest używany w plikach komponentów Razor (.razor), które dziedziczą z ComponentBase. Podczas tworzenia składników, które implement IComponent directly, użyj RenderHandle.DispatchExceptionAsync.

Aby obsłużyć przechwycone wyjątki poza cyklem życia składnika Razor, przekaż wyjątek do DispatchExceptionAsync i poczekaj na wynik:

try
{
    ...
}
catch (Exception ex)
{
    await DispatchExceptionAsync(ex);
}

Typowy scenariusz dla powyższego podejścia polega na tym, że komponent uruchamia operację asynchroniczną, ale nie oczekuje jej wyniku, często nazywaną wzorcem Task, ponieważ metoda jest Task (rozpoczęta), a wynik metody jest zapomniany (zignorowany). Jeśli operacja nie powiedzie się, możesz chcieć, aby składnik potraktował błąd jako wyjątek cyklu życia składnika dla dowolnego z następujących celów:

  • Umieść składnik w stanie awarii, na przykład, aby wyzwolić granicę błędu.
  • Zakończ obwód, jeśli nie ma granicy błędu.
  • Wyzwól to samo rejestrowanie, które występuje w przypadku wyjątków cyklu życia.

W poniższym przykładzie użytkownik wybiera przycisk Wyślij raport , aby wyzwolić metodę w tle, ReportSender.SendAsyncktóra wysyła raport. W większości przypadków komponent oczekuje na wywołanie asynchroniczne Task i aktualizuje interfejs użytkownika, aby zasygnalizować zakończenie operacji. W poniższym przykładzie SendReport metoda nie oczekuje na element Task i nie zgłasza wyniku użytkownikowi. Ponieważ składnik celowo odrzuca Task w SendReport, wszystkie błędy asynchroniczne występują poza normalnym stosem wywołań cyklu życia, dlatego nie są widoczne przez Blazor.

<button @onclick="SendReport">Send report</button>

@code {
    private void SendReport()
    {
        _ = ReportSender.SendAsync();
    }
}

Aby traktować błędy, takie jak wyjątki metody cyklu życia, jawnie wysyłaj wyjątki z powrotem do składnika za pomocą polecenia DispatchExceptionAsync, jak pokazano w poniższym przykładzie.

<button @onclick="SendReport">Send report</button>

@code {
    private void SendReport()
    {
        _ = SendReportAsync();
    }

    private async Task SendReportAsync()
    {
        try
        {
            await ReportSender.SendAsync();
        }
        catch (Exception ex)
        {
            await DispatchExceptionAsync(ex);
        }
    }
}

Alternatywne podejście wykorzystuje metodę Task.Run:

private void SendReport()
{
    _ = Task.Run(async () =>
    {
        try
        {
            await ReportSender.SendAsync();
        }
        catch (Exception ex)
        {
            await DispatchExceptionAsync(ex);
        }
    });
}

Aby przeprowadzić działającą demonstrację, zaimplementuj przykład powiadomienia czasomierza w Wywołaj metody składników zewnętrznie, aby zaktualizować stan. Blazor W aplikacji dodaj następujące pliki z przykładu powiadomienia czasomierza i zarejestruj usługi w pliku, jak wyjaśniono w Program sekcji:

  • TimerService.cs
  • NotifierService.cs
  • Notifications.razor

W przykładzie użyto czasomierza poza cyklem życia składnika Razor, gdzie nieobsługiwany wyjątek zwykle nie jest obsługiwany przez mechanizmy obsługi błędów Blazor, takie jak granica obsługi błędów .

Najpierw zmień kod, TimerService.cs aby utworzyć sztuczny wyjątek poza cyklem życia składnika. while W pętli TimerService.cs, wyrzuć wyjątek, gdy elapsedCount osiągnie wartość dwóch:

if (elapsedCount == 2)
{
    throw new Exception("I threw an exception! Somebody help me!");
}

Umieść obszar błędów w głównym układzie aplikacji. Zastąp <article>...</article> znacznik następującym znacznikiem.

W pliku MainLayout.razor:

<article class="content px-4">
    <ErrorBoundary>
        <ChildContent>
            @Body
        </ChildContent>
        <ErrorContent>
            <p class="alert alert-danger" role="alert">
                Oh, dear! Oh, my! - George Takei
            </p>
        </ErrorContent>
    </ErrorBoundary>
</article>

W Blazor Web Apps z granicą błędu stosowaną wyłącznie do statycznego składnika MainLayout granica jest aktywna tylko podczas statycznego renderowania po stronie serwera (statyczne SSR). Granica nie aktywuje się tylko dlatego, że składnik dalej w hierarchii składników jest interaktywny. Aby ogólnie włączyć interakcyjność dla składnika MainLayout i pozostałych składników w dalszej części hierarchii składników, włącz renderowanie interakcyjne dla wystąpień składników HeadOutlet i Routes w składniku App (Components/App.razor). W poniższym przykładzie jest wdrażany tryb renderowania Interactive Server (InteractiveServer):

<HeadOutlet @rendermode="InteractiveServer" />

...

<Routes @rendermode="InteractiveServer" />

Jeśli uruchomisz aplikację w tym momencie, wyjątek zostanie zgłoszony, gdy licznik upłynięcia osiągnie wartość dwóch. Jednak interfejs użytkownika nie zmienia się. Granica błędu nie pokazuje zawartości błędu.

Aby wysłać wyjątki z usługi czasomierza z powrotem do Notifications składnika, do składnika są wprowadzane następujące zmiany:

  • Uruchom czasomierz w instrukcji try-catch. W klauzuli catch bloku try-catch, wyjątki są zwracane do składnika poprzez przekazanie Exception do DispatchExceptionAsync i oczekiwanie na wynik.
  • W metodzie StartTimer uruchom asynchroniczną usługę czasomierza w delegacie ActionTask.Run i celowo odrzuć zwrócony Task element.

StartTimer Metoda Notifications składnika (Notifications.razor):

private void StartTimer()
{
    _ = Task.Run(async () =>
    {
        try
        {
            await Timer.Start();
        }
        catch (Exception ex)
        {
            await DispatchExceptionAsync(ex);
        }
    });
}

Gdy usługa czasomierza jest wykonywana i osiąga liczbę dwóch, wyjątek jest wysyłany do Razor składnika, co z kolei wyzwala granicę błędu, aby wyświetlić zawartość <ErrorBoundary> błędu w składniku MainLayout :

Och, drogi! Och, ja! - George Takei