ASP.NET contesto di sincronizzazione core Blazor
Nota
Questa non è la versione più recente di questo articolo. Per la versione corrente, vedere la versione .NET 9 di questo articolo.
Avviso
Questa versione di ASP.NET Core non è più supportata. Per altre informazioni, vedere i criteri di supporto di .NET e .NET Core. Per la versione corrente, vedere la versione .NET 9 di questo articolo.
Importante
Queste informazioni si riferiscono a un prodotto non definitive che può essere modificato in modo sostanziale prima che venga rilasciato commercialmente. Microsoft non riconosce alcuna garanzia, espressa o implicita, in merito alle informazioni qui fornite.
Per la versione corrente, vedere la versione .NET 9 di questo articolo.
Blazor usa un contesto di sincronizzazione (SynchronizationContext) per imporre un singolo thread logico di esecuzione. I metodi del ciclo di vita di un componente e i callback degli eventi generati da Blazor vengono eseguiti nel contesto di sincronizzazione.
BlazorIl contesto di sincronizzazione lato server tenta di emulare un ambiente a thread singolo in modo che corrisponda strettamente al modello WebAssembly nel browser, che è a thread singolo. Questo emulazione ha come ambito solo un singolo circuito, ovvero due circuiti diversi possono essere eseguiti in parallelo. In un determinato momento all'interno di un circuito, il lavoro viene eseguito su un solo thread, che produce l'impressione di un singolo thread logico. Nessuna operazione viene eseguita simultaneamente all'interno dello stesso circuito.
Evitare le chiamate di blocco dei thread
Di norma, non chiamare i metodi seguenti nei componenti. I metodi seguenti bloccano il thread di esecuzione e quindi impediscono all'app di riprendere il lavoro fino al completamento della classe Task sottostante:
Nota
Gli esempi della documentazione di Blazor che usano i metodi di blocco dei thread citati in questa sezione usano tali metodi solo per scopi dimostrativi, non come linea guida consigliata per la creazione di codice. Ad esempio, alcune dimostrazioni di codice del componente simulano un processo a esecuzione prolungata chiamando Thread.Sleep.
Richiamare i metodi dei componenti esternamente per aggiornare lo stato
Nel caso in cui un componente debba essere aggiornato in base a un evento esterno, ad esempio un timer o un'altra notifica, usare il metodo InvokeAsync
, che invia l'esecuzione del codice al contesto di sincronizzazione di Blazor. Si consideri ad esempio il servizio di notifica seguente che può notificare a qualsiasi componente in ascolto lo stato aggiornato. Il metodo Update
può essere chiamato da qualsiasi punto dell'app.
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;
}
Registrare i servizi:
Per lo sviluppo sul lato client, registrare i servizi come singleton nel file lato
Program
client:builder.Services.AddSingleton<NotifierService>(); builder.Services.AddSingleton<TimerService>();
Per lo sviluppo sul lato server, registrare i servizi come inclusi nel file del server
Program
:builder.Services.AddScoped<NotifierService>(); builder.Services.AddScoped<TimerService>();
Usare NotifierService
per aggiornare un componente.
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;
}
}
Nell'esempio precedente:
- Il timer viene avviato all'esterno del contesto di Blazorsincronizzazione con
_ = Task.Run(Timer.Start)
. NotifierService
richiama il metodo delOnNotify
componente.InvokeAsync
viene usato per passare al contesto corretto e accodare un rerender. Per altre informazioni, vedere Rendering dei componenti di ASP.NET CoreRazor.- Il componente implementa IDisposable. L'iscrizione del delegato
OnNotify
viene annullata nel metodoDispose
, che viene chiamato dal framework quando il componente viene eliminato. Per altre informazioni, vedere Ciclo di vita dei componenti di ASP.NET Core Razor.
NotifierService
richiama il metodoOnNotify
del componente al di fuori del contesto di sincronizzazione di Blazor.InvokeAsync
viene usato per passare al contesto corretto e accodare un rerender. Per altre informazioni, vedere Rendering dei componenti di ASP.NET CoreRazor.- Il componente implementa IDisposable. L'iscrizione del delegato
OnNotify
viene annullata nel metodoDispose
, che viene chiamato dal framework quando il componente viene eliminato. Per altre informazioni, vedere Ciclo di vita dei componenti di ASP.NET Core Razor.
Importante
Se un Razor componente definisce un evento attivato da un thread in background, il componente potrebbe essere necessario per acquisire e ripristinare il contesto di esecuzione (ExecutionContext) al momento della registrazione del gestore. Per altre informazioni, vedere La pagina Chiamate InvokeAsync(StateHasChanged)
causa il fallback alle impostazioni cultura predefinite (dotnet/aspnetcore #28521).
Per inviare eccezioni rilevate dallo sfondo TimerService
al componente per gestire le eccezioni come le normali eccezioni dell'evento del ciclo di vita, vedere la sezione Gestire le eccezioni rilevate all'esterno del ciclo di vita di un Razor componente.
Gestire le eccezioni rilevate al di fuori del ciclo di vita di un Razor componente
Usare ComponentBase.DispatchExceptionAsync in un Razor componente per elaborare le eccezioni generate all'esterno dello stack di chiamate del ciclo di vita del componente. In questo modo il codice del componente può trattare le eccezioni come se fossero eccezioni del metodo del ciclo di vita. Successivamente, Blazori meccanismi di gestione degli errori, ad esempio i limiti degli errori, possono elaborare le eccezioni.
Nota
ComponentBase.DispatchExceptionAsync viene usato nei Razor file di componente (.razor
) che ereditano da ComponentBase. Quando si creano componenti che implement IComponent directly, usare RenderHandle.DispatchExceptionAsync.
Per gestire le eccezioni rilevate al di fuori del ciclo di vita di un Razor componente, passare l'eccezione a DispatchExceptionAsync e attendere il risultato:
try
{
...
}
catch (Exception ex)
{
await DispatchExceptionAsync(ex);
}
Uno scenario comune per l'approccio precedente è quando un componente avvia un'operazione asincrona, ma non attende un Task, spesso denominato modello fire e forget perché il metodo viene attivato (avviato) e il risultato del metodo viene dimenticato (eliminato). Se l'operazione non riesce, è possibile che il componente consideri l'errore come un'eccezione del ciclo di vita del componente per uno degli obiettivi seguenti:
- Inserire il componente in uno stato di errore, ad esempio, per attivare un limite di errore.
- Terminare il circuito se non è presente alcun limite di errore.
- Attivare la stessa registrazione che si verifica per le eccezioni del ciclo di vita.
Nell'esempio seguente l'utente seleziona il pulsante Invia report per attivare un metodo in background, ReportSender.SendAsync
, che invia un report. Nella maggior parte dei casi, un componente attende l'oggetto Task di una chiamata asincrona e aggiorna l'interfaccia utente per indicare il completamento dell'operazione. Nell'esempio seguente il SendReport
metodo non attende un Task oggetto e non segnala il risultato all'utente. Poiché il componente elimina intenzionalmente in Task SendReport
, eventuali errori asincroni si verificano fuori dallo stack di chiamate del ciclo di vita normale, pertanto non vengono visualizzati da Blazor:
<button @onclick="SendReport">Send report</button>
@code {
private void SendReport()
{
_ = ReportSender.SendAsync();
}
}
Per gestire gli errori come le eccezioni del metodo del ciclo di vita, inviare in modo esplicito le eccezioni al componente con DispatchExceptionAsync, come illustrato nell'esempio seguente:
<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);
}
}
}
Un approccio alternativo sfrutta Task.Run:
private void SendReport()
{
_ = Task.Run(async () =>
{
try
{
await ReportSender.SendAsync();
}
catch (Exception ex)
{
await DispatchExceptionAsync(ex);
}
});
}
Per una dimostrazione funzionante, implementare l'esempio di notifica timer in Richiamare i metodi del componente esternamente per aggiornare lo stato. In un'app Blazor aggiungere i file seguenti dall'esempio di notifica timer e registrare i servizi nel Program
file come illustrato nella sezione:
TimerService.cs
NotifierService.cs
Notifications.razor
L'esempio usa un timer all'esterno del ciclo di vita di un Razor componente, in cui un'eccezione non gestita normalmente non viene elaborata dai Blazormeccanismi di gestione degli errori, ad esempio un limite di errore.
Prima di tutto, modificare il codice in TimerService.cs
per creare un'eccezione artificiale al di fuori del ciclo di vita del componente. while
Nel ciclo di TimerService.cs
, generare un'eccezione quando raggiunge elapsedCount
un valore di due:
if (elapsedCount == 2)
{
throw new Exception("I threw an exception! Somebody help me!");
}
Posizionare un limite di errore nel layout principale dell'app. Sostituire il <article>...</article>
markup con il markup seguente.
In 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>
In Blazor Web Apps con il limite di errore applicato solo a un componente statico MainLayout
, il limite è attivo solo durante la fase di rendering statico lato server (SSR statico). Il limite non viene attivato solo perché un componente più in basso nella gerarchia dei componenti è interattivo. Per abilitare l'interattività su larga scala per il MainLayout
componente e per i rest componenti più in basso nella gerarchia dei componenti, abilitare il rendering interattivo per le istanze e HeadOutlet
Routes
dei componenti nel App
componente (Components/App.razor
). L'esempio seguente adotta la modalità di rendering Interactive Server (InteractiveServer
):
<HeadOutlet @rendermode="InteractiveServer" />
...
<Routes @rendermode="InteractiveServer" />
Se si esegue l'app a questo punto, l'eccezione viene generata quando il conteggio trascorso raggiunge un valore pari a due. Tuttavia, l'interfaccia utente non cambia. Il limite di errore non mostra il contenuto dell'errore.
Per inviare le eccezioni dal servizio timer al Notifications
componente, vengono apportate le modifiche seguenti al componente:
- Avviare il timer in un'istruzione
try-catch
.catch
Nella clausola deltry-catch
blocco le eccezioni vengono inviate di nuovo al componente passando Exception a DispatchExceptionAsync e attendendo il risultato. StartTimer
Nel metodo avviare il servizio timer asincrono nel Action delegato di Task.Run e rimuovere intenzionalmente l'oggetto restituitoTask.
Metodo StartTimer
del Notifications
componente (Notifications.razor
):
private void StartTimer()
{
_ = Task.Run(async () =>
{
try
{
await Timer.Start();
}
catch (Exception ex)
{
await DispatchExceptionAsync(ex);
}
});
}
Quando il servizio timer viene eseguito e raggiunge un conteggio di due, l'eccezione viene inviata al Razor componente, che a sua volta attiva il limite di errore per visualizzare il contenuto degli errori di <ErrorBoundary>
nel MainLayout
componente:
Oh, caro! Oh mio Dio! - George Takei