Bagikan melalui


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

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

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 komponen OnNotify . InvokeAsync digunakan untuk beralih ke konteks yang benar dan mengantrekan render. 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 lebih lanjut, lihat siklus hidup komponen Razor ASP.NET Core.
  • NotifierService memanggil metode OnNotify komponen di luar konteks sinkronisasi Blazor. InvokeAsync digunakan untuk beralih ke konteks yang benar dan mengantrekan render. 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 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 TaskSendReportdalam , 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 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>

Di Blazor Web Apps dengan 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 komponen lainnya 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:

Metode StartTimerNotifications 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