ASP.NET Core Blazor 同步處理內容
注意
這不是這篇文章的最新版本。 如需目前的版本,請參閱 本文的 .NET 9 版本。
警告
不再支援此版本的 ASP.NET Core。 如需詳細資訊,請參閱 .NET 和 .NET Core 支持原則。 如需目前的版本,請參閱 本文的 .NET 9 版本。
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}", 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;
}
}
在前述範例中:
- 計時器是透過
_ = 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
達到二的值時擲出例外狀況:
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" />
如果您此時執行應用程式,當經過的計數達到二的值時,就會擲出例外狀況。 不過,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