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

Примечание.

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

Внимание

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

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

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

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

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

Как правило, не следует вызывать следующие методы в компонентах. Следующие методы блокируют поток выполнения и, таким образом, блокируют возобновление работы приложения до тех пор, пока не завершится базовый 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);
    }
}
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}");
                }
            }
        }
    }

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

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

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

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

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, который вызывается платформой при удалении компонента. Дополнительные сведения см. в статье Жизненный цикл компонентов Razor ASP.NET Core.
  • NotifierService вызывает метод OnNotify компонента вне контекста синхронизации Blazor. InvokeAsync используется для переключения на подходящий контекст и постановки отрисовки в очередь. Дополнительные сведения см. в статье Отрисовка компонентов Razor ASP.NET Core.
  • Компонент реализует IDisposable. Для делегата OnNotify отменяется подписка в методе Dispose, который вызывается платформой при удалении компонента. Дополнительные сведения см. в статье Жизненный цикл компонентов Razor 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 результата пользователю. Так как компонент намеренно отключается карта, TaskSendReportвсе асинхронные сбои возникают вне стека вызовов нормального жизненного цикла, поэтому не рассматриваются следующим 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);
        }
    });
}

Для рабочей демонстрации реализуйте пример уведомления таймера во внешних методах компонента Invoke для обновления состояния. 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 веб-приложения с границей ошибки, применяемой только к стаическому MainLayout компоненту, граница активна только во время этапа отрисовки на стороне статического сервера (статический SSR). Граница не активируется только потому, что компонент дальше вниз иерархии компонентов является интерактивным. Чтобы включить интерактивное взаимодействие для MainLayout компонента и остальных компонентов в иерархии компонентов, включите интерактивную отрисовку для HeadOutletRoutes экземпляров компонентов в App компоненте (Components/App.razor). В следующем примере используется режим отрисовки интерактивного сервера(InteractiveServer

<HeadOutlet @rendermode="InteractiveServer" />

...

<Routes @rendermode="InteractiveServer" />

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

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

  • Запустите таймер в инструкцииtry-catch. catch В предложении try-catch блока исключения отправляются обратно в компонент, передав результат ExceptionDispatchExceptionAsync и ожидая его.
  • В методе 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 :

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