Поделиться через


контекст синхронизации ASP.NET Core Blazor

Примечание.

Это не последняя версия этой статьи. В текущем релизе см. версию этой статьи .NET 9.

Предупреждение

Эта версия ASP.NET Core больше не поддерживается. Дополнительные сведения см. в политике поддержки .NET и .NET Core. В текущем выпуске см. версию 9 .NET этой статьи.

Внимание

Эта информация относится к предварительному выпуску продукта, который может быть существенно изменен до его коммерческого выпуска. Майкрософт не предоставляет никаких гарантий, явных или подразумеваемых, относительно приведенных здесь сведений.

В текущем выпуске см версию .NET 9 этой статьи.

Blazor использует контекст синхронизации (SynchronizationContext) для принудительного использования одного логического потока выполнения. Методы жизненного цикла компонента и обратные вызовы событий, инициированные Blazor, выполняются в контексте синхронизации.

BlazorКонтекст синхронизации на стороне сервера пытается эмулировать однопоточную среду, чтобы она тесно соответствовала модели WebAssembly в браузере, которая является однопоточной. Эта эмуляция ограничена только отдельным каналом, что означает, что два разных канала могут выполняться параллельно. В любой момент времени в схеме работа выполняется ровно на одном потоке, что создает впечатление одного логического потока. Две операции одновременно не выполняются в одном канале.

Один логический поток выполнения не подразумевает один асинхронный поток управления. Компонент повторно входит в любой момент, когда ожидается неполный Task. методы жизненного цикла или методы удаления компонентов могут вызываться до возобновления асинхронного потока управления после ожидания завершения Task. Поэтому компонент должен убедиться, что он находится в допустимом состоянии, прежде чем ожидать потенциально неполный Task. В частности, компонент должен убедиться, что он находится в допустимом состоянии для рендеринга, когда OnInitializedAsync или OnParametersSetAsync возвращаются. Если любой из этих методов возвращает неполный Task, они должны убедиться, что часть метода, завершающаяся синхронно, оставляет компонент в допустимом состоянии для отрисовки.

Еще одно влияние на компоненты повторного входа заключается в том, что метод не может отложить Task до тех пор, пока метод не возвращается, передав его в ComponentBase.InvokeAsync. Вызов ComponentBase.InvokeAsync может отложить выполнение Task только до достижения следующего оператораawait.

Компоненты могут реализовать IDisposable или IAsyncDisposable для вызова асинхронных методов с помощью CancellationToken из CancellationTokenSource, отменяемой при удалении компонента. Однако это действительно зависит от сценария. Это зависит от автора компонента, чтобы определить, является ли это правильным поведением. Например, при реализации компонента SaveButton, который сохраняет некоторые локальные данные в базе данных при нажатии кнопки сохранения, автор компонента может намеренно предусмотреть возможность отмены изменений, если пользователь нажимает кнопку и быстро переходит на другую страницу. Это может привести к удалению компонента до завершения асинхронного сохранения.

Одноразовый компонент может проверить удаление после ожидания любой Task, которая не получает CancellationTokenот этого компонента. Неполные Tasks также могут предотвратить сборку мусора удаленного компонента.

ComponentBase игнорирует исключения, вызванные отменой Task (точнее, игнорирует все исключения, если отменяются ожидаемые Task), вследствие чего методы компонентов не обязаны обрабатывать TaskCanceledException и OperationCanceledException.

ComponentBase не может следовать приведенным выше рекомендациям, так как он не описывает, что представляет собой допустимое состояние для производного компонента, и он не реализует IDisposable или IAsyncDisposable. Если OnInitializedAsync возвращает неполный Task, который не использует CancellationToken, и компонент удаляется до завершения Task, ComponentBase всё равно вызывает OnParametersSet и ожидает OnParametersSetAsync. Если одноразовый компонент не использует CancellationToken, OnParametersSet и OnParametersSetAsync должны проверить, утилизирован ли компонент.

Избегайте вызывающих блокировку потока вызовов

Как правило, не следует вызывать следующие методы в компонентах. Следующие методы блокируют поток выполнения и, таким образом, блокируют возобновление работы приложения до тех пор, пока не завершится базовый Task:

Примечание.

Примеры документации Blazor, в которых используются методы блокировки потоков, упомянутые в этом разделе, используют эти методы исключительно в целях демонстрации, а не в качестве рекомендаций по написанию кода. Например, несколько демонстраций кода компонента моделируют продолжительный процесс путем вызова метода Thread.Sleep.

Внешний вызов методов компонента для изменения состояния

Если компонент нужно изменить на основе внешнего события, такого как таймер или другое уведомление, используйте метод InvokeAsync, который выполняет отправку выполнения кода в контекст синхронизации Blazor. Например, рассмотрим следующую службу уведомителя, которая может уведомлять любой компонент, ожидающий передачи данных, об измененном состоянии. Метод Update можно вызывать из любого места в приложении.

TimerService.cs:

namespace BlazorSample;

public class TimerService(NotifierService notifier, 
    ILogger<TimerService> logger) : IDisposable
{
    private int elapsedCount;
    private readonly static 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);
    }
}
namespace BlazorSample;

public class TimerService(NotifierService notifier, 
    ILogger<TimerService> logger) : IDisposable
{
    private int elapsedCount;
    private readonly static 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 readonly static 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 readonly static 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;
}

Зарегистрируйте службы:

  • Для разработки на стороне клиента зарегистрируйте службы как одиночки в клиентском Program файле.

    builder.Services.AddSingleton<NotifierService>();
    builder.Services.AddSingleton<TimerService>();
    
  • Для разработки на стороне сервера зарегистрируйте службы в файле сервера Program :

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

Используйте NotifierService для обновления компонента.

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

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

В предыдущем примере:

  • Таймер инициируется вне Blazorконтекста синхронизации с _ = Task.Run(Timer.Start).
  • NotifierService вызывает метод компонента OnNotify . InvokeAsync используется для переключения на правильный контекст и постановки в очередь повторного рендеринга. Дополнительные сведения см. в статье Отрисовка компонентов Razor ASP.NET Core.
  • Компонент реализует IDisposable. Для делегата OnNotify отменяется подписка в методе Dispose, который вызывается платформой при удалении компонента. Дополнительные сведения см. в разделе ASP.NET Core Razor утилизации компонентов.
  • NotifierService вызывает метод OnNotify компонента вне контекста синхронизации Blazor. InvokeAsync используется для переключения в правильный контекст и перечисления rerender. Дополнительные сведения см. в статье Отрисовка компонентов Razor ASP.NET Core.
  • Компонент реализует IDisposable. Отписка делегата OnNotify происходит в методе Dispose, который вызывается фреймворком при уничтожении компонента. Дополнительные сведения см. в разделе ASP.NET Core Razor утилизации компонентов.

Внимание

Если компонент определяет событие, активируемое из фонового потока, возможно, потребуется для него захватить и восстановить контекст выполнения (ExecutionContext) в момент регистрации обработчика. Дополнительные сведения см. в разделе «Вызов InvokeAsync(StateHasChanged) приводит к тому, что страница возвращается к культуре по умолчанию (dotnet/aspnetcore #28521)».

Чтобы передать пойманные исключения из фоновой TimerService задачи в компонент, чтобы обрабатывать исключения как обычные исключения жизненного цикла, обратитесь к разделу "Обработка пойманных исключений за пределами жизненного цикла компонента Razor".

Обрабатывать пойманные исключения за пределами жизненного цикла компонента Razor

Используйте ComponentBase.DispatchExceptionAsync в компоненте Razor для обработки исключений, возникающих за пределами стека вызовов жизненного цикла компонента. Это позволяет коду компонента обрабатывать исключения, как если бы они были исключениями метода жизненного цикла. После этого Blazorмеханизмы обработки ошибок, такие как границы ошибок, могут обрабатывать исключения.

Примечание.

ComponentBase.DispatchExceptionAsync используется в Razor файлах компонентов (.razor), наследующих от ComponentBase. При создании компонентов, которые implement IComponent directly, используйте RenderHandle.DispatchExceptionAsync.

Чтобы обрабатывать пойманные исключения за пределами цикла жизненного компонента Razor, передайте исключение DispatchExceptionAsync и дождитесь результата.

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

Распространенный сценарий для предыдущего подхода заключается в том, что компонент запускает асинхронную операцию, но не ожидает Task, часто называемую паттерном запустить и забыть, потому что метод запускается (начинается) и результат метода забывается (отбрасывается). Если операция завершается ошибкой, может потребоваться, чтобы компонент обрабатывал сбой как исключение жизненного цикла компонентов для любой из следующих целей:

  • Поместите компонент в состояние сбоя, например, чтобы активировать границу обработки ошибок.
  • Прекратите схему, если граница ошибки отсутствует.
  • Запустите тот же процесс логирования, который применяется для исключений жизненного цикла.

В следующем примере пользователь выбирает кнопку "Отправить отчет ", чтобы активировать фоновый метод, ReportSender.SendAsyncкоторый отправляет отчет. В большинстве случаев компонент ожидает Task асинхронного вызова и обновляет пользовательский интерфейс, чтобы указать, что операция завершена. В следующем примере метод SendReport не ожидает Task и не сообщает результат пользователю. Поскольку компонент намеренно отвергает Task в SendReport, любые асинхронные сбои происходят вне обычного стека вызовов жизненного цикла и, следовательно, остаются незамеченными для Blazor.

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

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

Для обработки сбоев, таких как исключения метода жизненного цикла, нужно явным образом отправить исключения обратно в компонент с помощью DispatchExceptionAsync, как показано в следующем примере:

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

Альтернативный подход использует Task.Run:

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

Для рабочей демонстрации реализуйте пример уведомления таймера в вызывании методов компонента извне для обновления состояния. Blazor В приложении добавьте следующие файлы из примера уведомления таймера и зарегистрируйте службы в файле, как описано в Program разделе:

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

В примере используется таймер вне Razor жизненного цикла компонента, где необработанное исключение обычно не обрабатывается Blazorмеханизмами обработки ошибок, такими как граница ошибки.

Во-первых, измените код, TimerService.cs чтобы создать искусственное исключение за пределами жизненного цикла компонента. В цикле whileTimerService.csвызывается исключение, когда elapsedCount достигается значение двух:

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

Поместите границу ошибки в основной макет приложения. Замените разметку <article>...</article> следующей разметкой.

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

В Blazor Web Appс границей ошибки, применяемой только к статическому MainLayout компоненту, граница активна только во время фазы статической серверной отрисовки (static SSR). Граница не активируется только потому, что компонент дальше вниз иерархии компонентов является интерактивным. Чтобы обеспечить интерактивное взаимодействие для компонента MainLayout и остальных компонентов, расположенных ниже в иерархии компонентов, включите интерактивную отрисовку для экземпляров компонентов HeadOutlet и Routes в компоненте App (Components/App.razor). В следующем примере используется режим отрисовки интерактивного сервера (InteractiveServer).

<HeadOutlet @rendermode="InteractiveServer" />

...

<Routes @rendermode="InteractiveServer" />

Если вы запускаете приложение на этом этапе, исключение будет выброшено, когда счётчик времени достигнет значения 2. Однако пользовательский интерфейс не изменяется. Граница ошибки не отображает содержимое ошибки.

Чтобы отправить исключения из службы таймера обратно в компонент Notifications, в него вносятся следующие изменения:

  • Запустите таймер в инструкцииtry-catch. В предложении catch блока try-catch исключения отправляются обратно в компонент, передавая Exception в DispatchExceptionAsync и ожидая результат.
  • В методе StartTimer запустите службу асинхронного таймера в Action делегате Task.Run и намеренно отмените возвращенную Task.

StartTimer Метод Notifications компонента (Notifications.razor):

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

Когда служба таймера запускается и достигает значения два, исключение отправляется в компонент Razor, что, в свою очередь, активирует границу ошибки для отображения содержимого ошибки <ErrorBoundary> в компоненте MainLayout.

Ну, дорогой! Вот это да! - Джордж Кейи