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

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

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 del OnNotify componente. InvokeAsync viene usato per passare al contesto corretto e accodare un rendering. Per altre informazioni, vedere Rendering dei componenti di ASP.NET CoreRazor.
  • Il componente implementa IDisposable. L'iscrizione del delegato OnNotify viene annullata nel metodo Dispose, 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 metodo OnNotify del componente al di fuori del contesto di sincronizzazione di Blazor. InvokeAsync viene usato per passare al contesto corretto e accodare un rendering. Per altre informazioni, vedere Rendering dei componenti di ASP.NET CoreRazor.
  • Il componente implementa IDisposable. L'iscrizione del delegato OnNotify viene annullata nel metodo Dispose, 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 TaskSendReport, 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 App Web con il limite di errore applicato solo a un componente staticoMainLayout, 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 il resto dei componenti più in basso nella gerarchia dei componenti, abilitare il rendering interattivo per le istanze dei HeadOutlet componenti e Routes 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 del try-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