Bagikan melalui


konteks sinkronisasi ASP.NET Core Blazor

Catatan

Ini bukan versi terbaru dari artikel ini. Untuk rilis saat ini, lihat versi .NET 9 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 9 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 9 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 mensimulasikan lingkungan utas tunggal agar lebih sesuai dengan model WebAssembly di browser, yang menggunakan utas tunggal. Emulasi ini hanya dilingkup ke sirkuit individual, yang berarti dua sirkuit yang berbeda dapat berjalan secara paralel. Pada setiap titik waktu dalam suatu sirkuit, tugas dilakukan pada tepat satu utas, sehingga memberikan kesan adanya satu utas logis. Tidak ada dua operasi yang dijalankan secara bersamaan dalam sirkuit yang sama.

Satu alur eksekusi logis tidak menyiratkan satu alur kontrol asinkron. Komponen bersifat re-entrant pada setiap titik di mana ia menunggu Taskyang belum lengkap. Metode siklus hidup atau metode pembuangan komponen dapat dipanggil sebelum alur kontrol asinkron dilanjutkan setelah menunggu Task selesai. Oleh karena itu, komponen harus memastikan bahwa dirinya dalam keadaan valid sebelum menanti status Taskyang berpotensi tidak lengkap. Secara khusus, komponen harus memastikan bahwa komponen berada dalam status valid untuk penyajian saat OnInitializedAsync atau OnParametersSetAsync kembali. Jika salah satu metode ini mengembalikan Taskyang tidak lengkap , mereka harus memastikan bahwa bagian dari metode yang selesai secara sinkron meninggalkan komponen dalam keadaan valid untuk penyajian.

Implikasi tambahan dari komponen re-entrant adalah bahwa metode tidak dapat menangguhkan Task sampai setelah metode mengembalikan dengan mengoper ke ComponentBase.InvokeAsync. Memanggil ComponentBase.InvokeAsync hanya dapat menunda Task hingga operator await berikutnya tercapai.

Komponen mungkin menerapkan IDisposable atau IAsyncDisposable untuk memanggil metode asinkron menggunakan CancellationToken dari CancellationTokenSource yang dibatalkan saat komponen dibuang. Namun, ini benar-benar tergantung pada skenarionya. Terserah pembuat komponen untuk menentukan apakah itu perilaku yang benar. Misalnya, jika menerapkan komponen SaveButton yang mempertahankan beberapa data lokal ke database ketika tombol simpan dipilih, pembuat komponen mungkin memang berniat untuk membuang perubahan jika pengguna memilih tombol dan dengan cepat menavigasi ke halaman lain, yang dapat membuang komponen sebelum penyimpanan asinkron selesai.

Komponen sekali pakai dapat memeriksa pembuangan setelah menunggu setiap Task yang tidak menerima CancellationTokendari komponen tersebut. Taskyang tidak lengkap juga dapat mencegah pengumpulan sampah komponen yang dibuang.

ComponentBase mengabaikan pengecualian yang disebabkan oleh pembatalan Task (lebih tepatnya, ini mengabaikan semua pengecualian jika Taskyang ditunggu dibatalkan), sehingga metode komponen tidak perlu menangani TaskCanceledException dan OperationCanceledException.

ComponentBase tidak dapat mengikuti panduan sebelumnya karena tidak mengonsepkan apa yang merupakan status yang valid untuk komponen turunan dan itu sendiri tidak menerapkan IDisposable atau IAsyncDisposable. Jika OnInitializedAsync mengembalikan Task yang tidak lengkap yang tidak menggunakan CancellationToken dan komponen dibuang sebelum Task selesai, ComponentBase masih memanggil OnParametersSet dan menunggu OnParametersSetAsync. Jika komponen sekali pakai tidak menggunakan CancellationToken, maka OnParametersSet dan OnParametersSetAsync harus memeriksa apakah komponen tersebut sudah dibuang.

Hindari panggilan yang membuat utas terblokir

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

Daftarkan layanan:

  • Untuk pengembangan sisi klien, daftarkan layanan sebagai singleton di file 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 konteks sinkronisasi Blazor dengan _ = Task.Run(Timer.Start).
  • NotifierService memanggil metode komponen OnNotify . InvokeAsync digunakan untuk beralih ke konteks yang benar dan mengantrikan pengulangan render. Untuk informasi lebih lanjut, lihat perenderan komponen Razor ASP.NET Core.
  • Komponen mengimplementasikan IDisposable. Delegasi OnNotify tidak berlangganan lagi pada metode Dispose, yang dipanggil oleh framework saat komponen dibuang. Untuk informasi selengkapnya, lihat RazorASP.NET Core .
  • NotifierService memanggil metode OnNotify 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 metode Dispose, yang dipanggil oleh kerangka kerja saat komponen dibuang. Untuk informasi selengkapnya, lihat RazorASP.NET Core .

Penting

Razor Jika komponen mendefinisikan peristiwa yang dipicu dari utas latar belakang, komponen mungkin perlu menangkap dan memulihkan konteks eksekusi (ExecutionContext) pada saat pendaftaran handler. Untuk informasi selengkapnya, lihat Pemanggilan InvokeAsync(StateHasChanged) menyebabkan halaman kembali 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 Razor untuk memproses pengecualian yang 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 menggunakan 'await' Task, sering disebut pola 'fire and forget' karena metode tersebut 'ditembakkan' (dimulai) dan hasil metode tersebut diabaikan (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.
  • Memicu pencatatan yang sama seperti 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 dari 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 di dalam SendReport, kegagalan asinkron apa pun terjadi di luar tumpukan panggilan siklus hidup normal, sehingga 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 siklus hidup komponen , di mana pengecualian yang tidak tertangani biasanya tidak diproses oleh mekanisme penanganan kesalahan , seperti penyelesaian batasan kesalahan .

Pertama, ubah kode untuk TimerService.cs membuat pengecualian buatan di luar siklus hidup komponen. Dalam perulangan whileTimerService.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 App ketika batas kesalahan hanya diterapkan ke komponen MainLayout yang statis, batas tersebut hanya aktif selama fase rendering sisi server statis (SSR statis). Batas tidak akan aktif hanya karena ada komponen yang lebih dalam dalam hierarki komponen yang bersifat interaktif. Untuk mengaktifkan interaktivitas secara luas untuk komponen MainLayout dan komponen lainnya lebih jauh ke hierarki komponen, aktifkan penyajian interaktif untuk instans komponen HeadOutlet dan Routes di komponen App (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 terjadi 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:

Metode StartTimer dari komponen Notifications (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 komponen Razor, yang pada gilirannya memicu batas kesalahan untuk menampilkan konten kesalahan <ErrorBoundary> dalam komponen MainLayout.

Oh, sayang! Oh, astaga! - George Takei