konteks sinkronisasi ASP.NET Core Blazor
Catatan
Ini bukan versi terbaru dari artikel ini. Untuk rilis saat ini, lihat versi .NET 8 dari artikel ini.
Peringatan
Versi ASP.NET Core ini tidak lagi didukung. Untuk informasi selengkapnya, lihat Kebijakan Dukungan .NET dan .NET Core. Untuk rilis saat ini, lihat versi .NET 8 dari artikel ini.
Penting
Informasi ini berkaitan dengan produk pra-rilis yang mungkin dimodifikasi secara substansial sebelum dirilis secara komersial. Microsoft tidak memberikan jaminan, tersirat maupun tersurat, sehubungan dengan informasi yang diberikan di sini.
Untuk rilis saat ini, lihat versi .NET 8 dari artikel ini.
Blazor menggunakan konteks sinkronisasi (SynchronizationContext) untuk memberlakukan utas eksekusi logis tunggal. Metode siklus hidup komponen dan panggilan balik peristiwa yang dimunculkan oleh Blazor dijalankan pada konteks sinkronisasi.
BlazorKonteks sinkronisasi sisi server mencoba meniru lingkungan utas tunggal sehingga sangat cocok dengan model WebAssembly di browser, yang merupakan utas tunggal. Emulasi ini hanya dilingkup ke sirkuit individual, yang berarti dua sirkuit yang berbeda dapat berjalan secara paralel. Pada titik waktu tertentu dalam sirkuit, pekerjaan dilakukan pada tepat satu utas, yang menghasilkan kesan dari satu utas logis. Tidak ada dua operasi yang dijalankan secara bersamaan dalam sirkuit yang sama.
Hindari panggilan yang memblokir utas
Secara umum, jangan panggil metode berikut dalam komponen. Metode berikut memblokir utas eksekusi dan dengan demikian memblokir aplikasi agar tidak melanjutkan pekerjaan hingga Task yang mendasarinya selesai:
Catatan
Contoh dokumentasi Blazor yang menggunakan metode pemblokiran utas yang disebutkan di bagian ini hanya menggunakan metode untuk tujuan demonstrasi, bukan sebagai panduan pengodean yang disarankan. Misalnya, beberapa demonstrasi kode komponen mensimulasikan proses yang berjalan lama dengan memanggil Thread.Sleep.
Memanggil metode komponen secara eksternal untuk memperbarui status
Jika komponen harus diperbarui berdasarkan peristiwa eksternal, seperti timer atau pemberitahuan lainnya, gunakan metode InvokeAsync
, yang mengirimkan eksekusi kode ke konteks sinkronisasi Blazor. Misalnya, pertimbangkan layanan pemberitahuan berikut yang dapat memberi tahu komponen apa pun yang mendengarkan tentang status yang diperbarui. Metode Update
dapat dipanggil dari mana saja di aplikasi.
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;
}
Daftarkan layanan:
Untuk pengembangan sisi klien, daftarkan layanan sebagai singleton dalam file sisi
Program
klien:builder.Services.AddSingleton<NotifierService>(); builder.Services.AddSingleton<TimerService>();
Untuk pengembangan sisi server, daftarkan layanan sebagaimana dilingkup dalam file server
Program
:builder.Services.AddScoped<NotifierService>(); builder.Services.AddScoped<TimerService>();
Gunakan NotifierService
untuk memperbarui komponen.
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;
}
}
Dalam contoh sebelumnya:
- Timer dimulai di luar Blazorkonteks sinkronisasi dengan
_ = Task.Run(Timer.Start)
. NotifierService
memanggil metode komponenOnNotify
.InvokeAsync
digunakan untuk beralih ke konteks yang benar dan mengantrekan rerender. Untuk informasi lebih lanjut, lihat perenderan komponen Razor ASP.NET Core.- Komponen mengimplementasikan IDisposable. Delegasi
OnNotify
berhenti berlangganan dalam metodeDispose
, yang dipanggil oleh kerangka kerja saat komponen dibuang. Untuk informasi lebih lanjut, lihat siklus hidup komponen Razor ASP.NET Core.
NotifierService
memanggil metodeOnNotify
komponen di luar konteks sinkronisasi Blazor.InvokeAsync
digunakan untuk beralih ke konteks yang benar dan mengantrekan rerender. Untuk informasi lebih lanjut, lihat perenderan komponen Razor ASP.NET Core.- Komponen mengimplementasikan IDisposable. Delegasi
OnNotify
berhenti berlangganan dalam metodeDispose
, yang dipanggil oleh kerangka kerja saat komponen dibuang. Untuk informasi lebih lanjut, lihat siklus hidup komponen Razor ASP.NET Core.
Penting
Razor Jika komponen mendefinisikan peristiwa yang dipicu dari utas latar belakang, komponen mungkin diperlukan untuk menangkap dan memulihkan konteks eksekusi (ExecutionContext) pada saat handler terdaftar. Untuk informasi selengkapnya, lihat Halaman penyebab panggilan InvokeAsync(StateHasChanged)
mundur ke budaya default (dotnet/aspnetcore #28521).
Untuk mengirimkan pengecualian yang tertangkap dari latar belakang TimerService
ke komponen untuk memperlakukan pengecualian seperti pengecualian peristiwa siklus hidup normal, lihat bagian Menangani pengecualian yang tertangkap di luar Razor siklus hidup komponen.
Menangani pengecualian yang tertangkap di luar Razor siklus hidup komponen
Gunakan ComponentBase.DispatchExceptionAsync dalam komponen untuk memproses pengecualian yang Razor dilemparkan di luar tumpukan panggilan siklus hidup komponen. Ini memungkinkan kode komponen untuk memperlakukan pengecualian seolah-olah mereka adalah pengecualian metode siklus hidup. Setelah itu, Blazormekanisme penanganan kesalahan, seperti batas kesalahan, dapat memproses pengecualian.
Catatan
ComponentBase.DispatchExceptionAsync digunakan dalam Razor file komponen (.razor
) yang mewarisi dari ComponentBase. Saat membuat komponen yang implement IComponent directly, gunakan RenderHandle.DispatchExceptionAsync.
Untuk menangani pengecualian yang tertangkap di luar Razor siklus hidup komponen, berikan pengecualian ke DispatchExceptionAsync dan tunggu hasilnya:
try
{
...
}
catch (Exception ex)
{
await DispatchExceptionAsync(ex);
}
Skenario umum untuk pendekatan sebelumnya adalah ketika komponen memulai operasi asinkron tetapi tidak menunggu Task, sering disebut pola kebakaran dan lupa karena metode diaktifkan (dimulai) dan hasil metode dilupakan (dibuang). Jika operasi gagal, Anda mungkin ingin komponen memperlakukan kegagalan sebagai pengecualian siklus hidup komponen untuk salah satu tujuan berikut:
- Masukkan komponen ke dalam status rusak, misalnya, untuk memicu batas kesalahan.
- Hentikan sirkuit jika tidak ada batas kesalahan.
- Picu pengelogan yang sama yang terjadi untuk pengecualian siklus hidup.
Dalam contoh berikut, pengguna memilih tombol Kirim laporan untuk memicu metode latar belakang, ReportSender.SendAsync
, yang mengirim laporan. Dalam kebanyakan kasus, komponen menunggu Task panggilan asinkron dan memperbarui UI untuk menunjukkan operasi selesai. Dalam contoh berikut, SendReport
metode tidak menunggu Task dan tidak melaporkan hasilnya kepada pengguna. Karena komponen sengaja membuang Task SendReport
dalam , kegagalan asinkron apa pun terjadi dari tumpukan panggilan siklus hidup normal, oleh karena itu tidak terlihat oleh Blazor:
<button @onclick="SendReport">Send report</button>
@code {
private void SendReport()
{
_ = ReportSender.SendAsync();
}
}
Untuk memperlakukan kegagalan seperti pengecualian metode siklus hidup, secara eksplisit mengirimkan kembali pengecualian ke komponen dengan DispatchExceptionAsync, seperti yang ditunjukkan contoh berikut:
<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);
}
}
}
Pendekatan alternatif memanfaatkan Task.Run:
private void SendReport()
{
_ = Task.Run(async () =>
{
try
{
await ReportSender.SendAsync();
}
catch (Exception ex)
{
await DispatchExceptionAsync(ex);
}
});
}
Untuk demonstrasi yang berfungsi, terapkan contoh pemberitahuan timer di Memanggil metode komponen secara eksternal untuk memperbarui status. Di aplikasi Blazor , tambahkan file berikut dari contoh pemberitahuan timer dan daftarkan layanan dalam Program
file seperti yang dijelaskan bagian:
TimerService.cs
NotifierService.cs
Notifications.razor
Contoh menggunakan timer di luar Razor siklus hidup komponen, di mana pengecualian yang tidak tertangani biasanya tidak diproses oleh Blazormekanisme penanganan kesalahan , seperti batas kesalahan.
Pertama, ubah kode untuk TimerService.cs
membuat pengecualian buatan di luar siklus hidup komponen. Dalam perulangan while
TimerService.cs
, berikan pengecualian ketika elapsedCount
mencapai nilai dua:
if (elapsedCount == 2)
{
throw new Exception("I threw an exception! Somebody help me!");
}
Tempatkan batas kesalahan di tata letak utama aplikasi. <article>...</article>
Ganti markup dengan markup berikut.
Di 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>
Dalam Blazor Web Apphal batas kesalahan hanya diterapkan ke komponen statis MainLayout
, batas hanya aktif selama fase penyajian sisi server statis (SSR statis). Batas tidak diaktifkan hanya karena komponen lebih jauh ke hierarki komponen bersifat interaktif. Untuk mengaktifkan interaktivitas secara luas untuk MainLayout
komponen dan rest komponen lebih jauh ke hierarki komponen, aktifkan penyajian interaktif untuk HeadOutlet
instans komponen dan Routes
dalam App
komponen (Components/App.razor
). Contoh berikut mengadopsi mode render Server Interaktif (InteractiveServer
):
<HeadOutlet @rendermode="InteractiveServer" />
...
<Routes @rendermode="InteractiveServer" />
Jika Anda menjalankan aplikasi pada saat ini, pengecualian akan dilemparkan saat jumlah yang berlalu mencapai nilai dua. Namun, UI tidak berubah. Batas kesalahan tidak menampilkan konten kesalahan.
Untuk mengirimkan pengecualian dari layanan timer kembali ke Notifications
komponen, perubahan berikut dilakukan pada komponen:
- Mulai timer dalam
try-catch
pernyataan.catch
Dalam klausultry-catch
blok, pengecualian dikirim kembali ke komponen dengan meneruskan Exception ke DispatchExceptionAsync dan menunggu hasilnya. - Dalam metode ,
StartTimer
mulai layanan timer asinkron di Action delegasi Task.Run dan sengaja membuang yang dikembalikan Task.
Metode StartTimer
Notifications
komponen (Notifications.razor
):
private void StartTimer()
{
_ = Task.Run(async () =>
{
try
{
await Timer.Start();
}
catch (Exception ex)
{
await DispatchExceptionAsync(ex);
}
});
}
Ketika layanan timer dijalankan dan mencapai hitungan dua, pengecualian dikirim ke Razor komponen, yang pada gilirannya memicu batas kesalahan untuk menampilkan konten <ErrorBoundary>
kesalahan dalam MainLayout
komponen:
Oh, sayang! Oh, astaga! - George Takei
ASP.NET Core