ASP.NET Core Blazor 同步上下文
注意
此版本不是本文的最新版本。 有关当前版本,请参阅本文的 .NET 9 版本。
警告
此版本的 ASP.NET Core 不再受支持。 有关详细信息,请参阅 .NET 和 .NET Core 支持策略。 对于当前版本,请参阅此文的 .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);
}
}
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;
}
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;
}
}
在上面的示例中:
- 计时器是使用
_ = Task.Run(Timer.Start)
在 Blazor 的外部启动的。 NotifierService
会调用该组件的OnNotify
方法。InvokeAsync
用于切换到正确的上下文,并将重新呈现排入队列。 有关详细信息,请参阅 ASP.NET Core Razor 组件呈现。- 组件会实现 IDisposable。
OnNotify
委托在Dispose
方法中取消订阅,在释放组件时,框架会调用此方法。 有关详细信息,请参阅 ASP.NET Core Razor 组件生命周期。
NotifierService
在 Blazor 的同步上下文之外调用组件的OnNotify
方法。InvokeAsync
用于切换到正确的上下文,并将重新呈现排入队列。 有关详细信息,请参阅 ASP.NET Core Razor 组件呈现。- 组件会实现 IDisposable。
OnNotify
委托在Dispose
方法中取消订阅,在释放组件时,框架会调用此方法。 有关详细信息,请参阅 ASP.NET Core Razor 组件生命周期。
重要
如果组件 Razor 定义从后台线程触发的事件,则可能需要该组件在注册处理程序时捕获和还原执行上下文 (ExecutionContext)。 有关更多信息,请参阅调用 InvokeAsync(StateHasChanged)
会导致页面回退到默认区域性 (dotnet/aspnetcore #28521)。
要将捕获的异常从后台 TimerService
调度到组件,以将异常视为正常的生命周期事件异常,请参阅在 Razor 组件生命周期以外处理捕获的异常部分。
在 Razor 组件的生命周期外处理捕获的异常
在 Razor 组件中使用 ComponentBase.DispatchExceptionAsync 来处理在组件的生命周期调用堆栈外部引发的异常。 这允许组件的代码将异常视为生命周期方法异常。 此后,Blazor 的错误处理机制(如错误边界)可以处理异常。
注意
ComponentBase.DispatchExceptionAsync 用于继承自 ComponentBase 的 Razor 组件文件 (.razor
)。 创建实现 implement IComponent directly 的组件时,请使用 RenderHandle.DispatchExceptionAsync。
若要处理在 Razor 组件生命周期之外捕获的异常,请将异常传递给 DispatchExceptionAsync,然后等待结果:
try
{
...
}
catch (Exception ex)
{
await DispatchExceptionAsync(ex);
}
上述方法的一个常见场景是某个组件启动了一个异步操作但不等待 Task,通常称为“发后即忘”模式,因为方法被触发(启动),而方法的结果被遗忘(丢弃)。 如果操作失败,你可能希望组件将失败视为组件生命周期异常,出于以下任何目标:
- 例如,将组件置于出错状态,以触发错误边界。
- 如果没有错误边界,则终止线路。
- 触发针对生命周期异常发生的相同日志记录。
在以下示例中,用户选择“发送报表”按钮,以触发发送报表的后台方法 ReportSender.SendAsync
。 在大多数情况下,组件会等待异步调用的 Task,并更新 UI 以指示操作已完成。 在以下示例中,SendReport
方法不会等待 Task,也不会向用户报告结果。 由于组件有意放弃 SendReport
中的 Task,因此任何异步故障都发生在正常的生命周期调用堆栈中,因而不会被 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
中的代码,以在组件的生命周期之外创建人工异常。 在 TimerService.cs
的 while
循环中,当 elapsedCount
达到 2 的值时引发异常:
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>
在错误边界仅应用于静态 MainLayout
组件的 Blazor Web App 中,边界仅在静态服务器端呈现(静态 SSR)阶段处于活动状态。 边界不会因为组件层次结构的下一个组件是交互式的而激活。 若要广泛地为 MainLayout
组件和组件层次结构中更下方组件的 rest 启用交互性,请为 App
组件 (Components/App.razor
) 中的 HeadOutlet
和 Routes
组件实例启用交互式呈现。 以下示例采用交互式服务器(InteractiveServer
)呈现模式:
<HeadOutlet @rendermode="InteractiveServer" />
...
<Routes @rendermode="InteractiveServer" />
如果此时运行应用,当经过的计数达到 2 的值时,将引发异常。 但是,UI 不会更改。 错误边界不显示错误内容。
为了将计时器服务中的异常调度回 Notifications
组件,对组件进行了以下更改:
- 在
try-catch
语句中启动计时器。 在try-catch
块的catch
子句中,通过将 Exception 传递到 DispatchExceptionAsync 并等候结果来将异常调度回组件。 - 在
StartTimer
方法中,在 Task.Run 的 Action 委托中启动异步计时器服务,并故意放弃返回的 Task。
Notifications
组件 (Notifications.razor
) 的 StartTimer
方法:
private void StartTimer()
{
_ = Task.Run(async () =>
{
try
{
await Timer.Start();
}
catch (Exception ex)
{
await DispatchExceptionAsync(ex);
}
});
}
当计时器服务执行并达到 2 的计数时,异常将调度到 Razor 组件,然后反过来触发错误边界,以在 MainLayout
组件中显示 <ErrorBoundary>
的错误内容:
Oh, dear! Oh, my! - George Takei