Partilhar via


Contexto de sincronização do ASP.NET Core Blazor

Observação

Esta não é a versão mais recente deste artigo. Para a versão atual, consulte a versão .NET 10 deste artigo.

Advertência

Esta versão do ASP.NET Core não é mais suportada. Para obter mais informações, consulte a Política de suporte do .NET e .NET Core. Para a versão atual, consulte a versão .NET 9 deste artigo.

Blazor usa um contexto de sincronização (SynchronizationContext) para impor um único thread lógico de execução. Os métodos de ciclo de vida de um componente e os callbacks de eventos acionados por Blazor são executados no contexto de sincronização.

Blazoro contexto de sincronização do lado do servidor tenta emular um ambiente de thread única para que ele corresponda ao modelo WebAssembly no navegador, que é de thread única. Esta emulação tem como escopo apenas um circuito individual, o que significa que dois circuitos diferentes podem ser executados em paralelo. Em qualquer momento dentro de um circuito, o trabalho é realizado em exatamente um fio, o que dá a impressão de um único fio lógico. Não há duas operações executadas simultaneamente dentro do mesmo circuito.

Um único thread lógico de execução não implica um único fluxo de controle assíncrono. Um componente é reentrante em qualquer ponto em que aguarda um Taskincompleto. Métodos de ciclo de vida ou métodos de descarte de componentes podem ser chamados antes que o fluxo de controle assíncrono seja retomado após aguardar a conclusão de uma Task. Portanto, um componente deve garantir que está num estado válido antes de esperar um Taskpotencialmente incompleto. Em particular, um componente deve garantir que esteja em um estado válido para renderização quando OnInitializedAsync ou OnParametersSetAsync retornar. Se qualquer um desses métodos retornar um Taskincompleto, eles devem garantir que a parte do método que é concluída de forma síncrona deixe o componente em um estado válido para renderização.

Outra implicação dos componentes reentrantes é que um método não pode adiar um Task até que o método retorne passando-o para ComponentBase.InvokeAsync. Chamar ComponentBase.InvokeAsync pode apenas adiar o Task até que o próximo operador await seja alcançado.

Os componentes podem implementar IDisposable ou IAsyncDisposable para chamar métodos assíncronos usando um CancellationToken de um CancellationTokenSource que é cancelado quando o componente é descartado. No entanto, isso realmente depende do cenário. Cabe ao autor do componente determinar se esse é o comportamento correto. Por exemplo, se implementar um componente SaveButton que persista alguns dados locais num banco de dados quando um botão de guardar for selecionado, o autor do componente pode de facto querer descartar as alterações se o utilizador selecionar o botão e navegar rapidamente para outra página, o que poderia descartar o componente antes que o salvamento assíncrono seja concluído.

Um componente descartável pode verificar se foi descartado depois de aguardar por qualquer Task que não receba a CancellationTokendo componente. Um Taskincompleto também pode impedir a coleta de lixo de um componente descartado.

ComponentBase ignora as exceções causadas pelo cancelamento Task (mais precisamente, ignora todas as exceções se os Taskaguardados forem cancelados), portanto, os métodos de componentes não precisam lidar com TaskCanceledException e OperationCanceledException.

ComponentBase não pode seguir as diretrizes anteriores porque não conceitua o que constitui um estado válido para um componente derivado e não implementa IDisposable ou IAsyncDisposable. Se OnInitializedAsync retornar um Task incompleto que não usa um CancellationToken e o componente for descartado antes que o Task seja concluído, ComponentBase ainda chamará OnParametersSet e aguardará OnParametersSetAsync. Se um componente descartável não utilizar um CancellationToken, OnParametersSet e OnParametersSetAsync devem verificar se o componente foi descartado.

Evite chamadas de bloqueio de threads

Geralmente, não chame os seguintes métodos em componentes. Os métodos a seguir bloqueiam o thread de execução e, portanto, impedem que o aplicativo retome o trabalho até que o Task subjacente seja concluído:

Observação

Blazor exemplos de documentação que usam os métodos de bloqueio de threads mencionados nesta seção apenas utilizam os métodos para demonstração, não como orientação de codificação recomendada. Por exemplo, algumas demonstrações de código de componente simulam um processo de longa execução chamando Thread.Sleep.

Invoque métodos de componentes externamente para atualizar o estado

Caso um componente precise ser atualizado com base em um evento externo, como um temporizador ou outra notificação, use o método InvokeAsync, que despacha a execução de código para o contexto de sincronização do Blazor. Por exemplo, considere o seguinte serviço notificador que pode notificar qualquer componente ouvinte sobre o estado atualizado. O método Update pode ser chamado de qualquer lugar no aplicativo.

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

Registe os serviços:

  • Para desenvolvimento do lado do cliente, registre os serviços como singletons no arquivo Program do lado do cliente:

    builder.Services.AddSingleton<NotifierService>();
    builder.Services.AddSingleton<TimerService>();
    
  • Para desenvolvimento do lado do servidor, registre os serviços como escopo no arquivo de Program do servidor:

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

Use o NotifierService para atualizar um componente.

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

No exemplo anterior:

  • O temporizador é iniciado fora do contexto de sincronização do Blazorcom _ = Task.Run(Timer.Start).
  • NotifierService invoca o método OnNotify do componente. InvokeAsync é usado para mudar para o contexto correto e enfileirar uma nova renderização. Para obter mais informações, consulte Razordo ASP.NET Core .
  • O componente implementa IDisposable. O delegado OnNotify é desinscrito no método Dispose, que é invocado pela plataforma quando o componente é descartado. Para obter mais informações, consulte Razorde descarte de componentes do Core .
  • NotifierService invoca o método OnNotify do componente fora do contexto de sincronização do Blazor. InvokeAsync é usado para mudar para o contexto correto e enfileirar uma nova renderização. Para obter mais informações, consulte Razordo ASP.NET Core .
  • O componente implementa IDisposable. O delegado OnNotify é desinscrito no método Dispose, que é invocado pela plataforma quando o componente é descartado. Para obter mais informações, consulte Razorde descarte de componentes do Core .

Importante

Se um componente Razor definir um evento que é acionado a partir de um thread em segundo plano, o componente pode ser necessário para capturar e restaurar o contexto de execução (ExecutionContext) no momento em que o manipulador é registrado. Para obter mais informações, consulte Chamar InvokeAsync(StateHasChanged) faz com que a página faça fallback para a cultura padrão (dotnet/aspnetcore #28521).

Para despachar exceções capturadas em segundo plano do TimerService para o componente, de forma a tratar as exceções como se fossem exceções normais do ciclo de vida de eventos, consulte a seção Manipular exceções capturadas fora do ciclo de vida do componente Razor.

Lidar com exceções detetadas fora do ciclo de vida de um componente Razor

Use ComponentBase.DispatchExceptionAsync em um componente Razor para processar exceções lançadas fora da pilha de chamadas do ciclo de vida do componente. Isso permite que o código do componente trate exceções como se fossem exceções do método de ciclo de vida. Depois disso, os mecanismos de tratamento de erros do Blazor, como limites de erro , podem processar as exceções.

Observação

ComponentBase.DispatchExceptionAsync é usado em Razor arquivos de componentes (.razor) que herdam de ComponentBase. Ao criar componentes que implement IComponent directly, use RenderHandle.DispatchExceptionAsync.

Para lidar com exceções detetadas fora do ciclo de vida de um componente Razor, passe a exceção para DispatchExceptionAsync e aguarde o resultado:

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

Um cenário comum para a abordagem anterior é quando um componente inicia uma operação assíncrona, mas não aguarda um Task, muitas vezes chamado de disparar e esquecer padrão porque o método é acionado (iniciado) e o resultado do método é esquecido (jogado fora). Se a operação falhar, convém que o componente trate a falha como uma exceção do ciclo de vida do componente para qualquer uma das seguintes metas:

  • Coloque o componente em um estado com defeito, por exemplo, para acionar um limite de erro .
  • Encerre o circuito se não houver limite de erro.
  • Acione o mesmo registo que é efetuado para exceções do ciclo de vida.

No exemplo a seguir, o usuário seleciona o botão Enviar relatório para acionar um método em segundo plano, ReportSender.SendAsync, que envia um relatório. Na maioria dos casos, um componente aguarda o Task de uma chamada assíncrona e atualiza a interface do usuário para indicar a operação concluída. No exemplo a seguir, o método SendReport não aguarda um Task e não relata o resultado ao usuário. Como o componente descarta intencionalmente o Task em SendReport, quaisquer falhas assíncronas ocorrem fora da pilha de chamadas do ciclo de vida normal, portanto, não são vistas por Blazor:

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

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

Para tratar falhas como exceções do método de ciclo de vida, despache explicitamente as exceções de volta para o componente com DispatchExceptionAsync, como demonstra o exemplo a seguir:

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

Uma abordagem alternativa aproveita Task.Run:

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

Para uma demonstração funcional, execute o exemplo de notificação do temporizador em Invocar métodos de componente externamente para atualizar o estado. Em um aplicativo Blazor, adicione os seguintes arquivos do exemplo de notificação de temporizador e registre os serviços no arquivo Program, conforme explicado na seção:

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

O exemplo usa um temporizador fora do ciclo de vida de um componente Razor, onde uma exceção não tratada normalmente não é processada pelos mecanismos de tratamento de erros do Blazor, como um limite de erro .

Primeiro, altere o código no TimerService.cs para criar uma exceção artificial fora do ciclo de vida do componente. No while loop de TimerService.cs, lance uma exceção quando o elapsedCount atingir um valor de dois:

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

Coloque um limite de erro no layout principal do aplicativo. Substitua a marcação <article>...</article> pela marcação a seguir.

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

Em Blazor Web Apps, quando o limite de erro é aplicado apenas a um componente MainLayout estático, o limite só está ativo durante a fase de renderização estática do lado do servidor (SSR estático). O limite não é ativado apenas porque um componente mais abaixo na hierarquia de componentes é interativo. Para habilitar a interatividade amplamente para o componente MainLayout e o restante dos componentes mais abaixo na hierarquia de componentes, habilite a renderização interativa para as instâncias de componentes HeadOutlet e Routes no componente App (Components/App.razor). O exemplo a seguir adota o modo de renderização do Interactive Server (InteractiveServer):

<HeadOutlet @rendermode="InteractiveServer" />

...

<Routes @rendermode="InteractiveServer" />

Se executar a aplicação neste ponto, a exceção será lançada quando a contagem de tempo decorrido atingir um valor de dois. No entanto, a interface do usuário não muda. O limite de erro não mostra o conteúdo do erro.

Para enviar exceções do serviço de timer de volta para o componente Notifications, as seguintes alterações são feitas no componente:

  • Inicie o temporizador numa instrução try-catch. Na cláusula catch do bloco try-catch, as exceções são enviadas de volta para o componente passando o Exception para DispatchExceptionAsync e aguardando o resultado.
  • No método StartTimer, inicie o serviço de temporizador assíncrono no Action delegado de Task.Run e descarte intencionalmente o Taskretornado.

O método StartTimer do componente Notifications (Notifications.razor):

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

Quando o serviço de timer é executado e atinge uma contagem de dois, a exceção é despachada para o componente Razor, que, por sua vez, aciona o limite de erro para exibir o conteúdo de erro do <ErrorBoundary> no componente MainLayout:

Oh, céus! Oh, meu! - Jorge Takei