Share via


ASP.NET Core Blazor 同步處理內容

注意

這不是這篇文章的最新版本。 如需目前版本,請參閱本文的 .NET 8 版本

重要

這些發行前產品的相關資訊在產品正式發行前可能會有大幅修改。 Microsoft 對此處提供的資訊,不做任何明確或隱含的瑕疵擔保。

如需目前版本,請參閱本文的 .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);
    }
}
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;
}

註冊服務:

  • 針對用戶端開發,請在用戶端 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;
}

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 元件轉譯
  • 元件會實作 IDisposableOnNotify 委派會在 Dispose 方法中取消訂閱,在處置元件時,架構會呼叫此方法。 如需詳細資訊,請參閱 ASP.NET Core Razor 元件生命週期
  • NotifierService 會在 Blazor 的同步處理內容之外叫用元件的 OnNotify 方法。 InvokeAsync 可用來切換至正確內容,並將轉譯排入佇列。 如需詳細資訊,請參閱 ASP.NET Core Razor 元件轉譯
  • 元件會實作 IDisposableOnNotify 委派會在 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.cswhile 迴圈中,當 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 Apps 中,界限只會在靜態伺服器端轉譯 (靜態 SSR) 階段期間起作用。 界限不會啟動,只是因為元件階層更下層的元件是互動式的。 若要為 MainLayout 元件以及元件階層下的其餘元件廣泛啟用互動功能,請為 App 元件 (Components/App.razor) 中的 HeadOutletRoutes 元件階層啟用互動式轉譯。 下列範例採用互動式伺服器 (InteractiveServer) 轉譯模式:

<HeadOutlet @rendermode="InteractiveServer" />

...

<Routes @rendermode="InteractiveServer" />

如果您此時執行應用程式,當經過的計數達到二的值時,就會擲出例外狀況。 不過,UI 不會變更。 錯誤界限不會顯示錯誤內容。

若要將例外狀況從計時器服務分派回到 Notifications 元件,請對該元件進行下列變更:

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