Condividi tramite


ASP.NET Core contesto di sincronizzazione 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.

Un singolo thread logico di esecuzione non implica un singolo flusso di controllo asincrono. Un componente è rientrante in qualunque punto in cui attende un Taskincompleto. metodi del ciclo di vita o metodi di eliminazione dei componenti possono essere chiamati prima che il flusso di controllo asincrono venga ripreso dopo aver atteso il completamento di un Task. Pertanto, un componente deve assicurarsi che sia in uno stato valido prima di attendere un Taskpotenzialmente incompleto. In particolare, un componente deve assicurarsi che sia in uno stato valido per il rendering quando viene restituito OnInitializedAsync o OnParametersSetAsync. Se uno di questi metodi restituisce un Taskincompleto, deve assicurarsi che la parte del metodo che completa in modo sincrono lasci il componente in uno stato valido per il rendering.

Un'altra implicazione dei componenti rientranti consiste nel fatto che un metodo non può rinviare un Task fino a quando il metodo non viene restituito passandolo a ComponentBase.InvokeAsync. La chiamata ComponentBase.InvokeAsync può rinviare il Task solo fino a quando non si raggiunge il successivo operatore await.

I componenti possono implementare IDisposable o IAsyncDisposable per chiamare metodi asincroni usando un CancellationToken da un CancellationTokenSource annullato quando il componente viene eliminato. Tuttavia, questo dipende realmente dallo scenario. Spetta all'autore del componente determinare se si tratta del comportamento corretto. Ad esempio, se si implementa un componente SaveButton che rende persistenti alcuni dati locali in un database quando viene selezionato un pulsante di salvataggio, l'autore del componente potrebbe effettivamente voler annullare le modifiche se l'utente seleziona il pulsante e passa rapidamente a un'altra pagina, che potrebbe eliminare il componente prima del completamento del salvataggio asincrono.

Un componente eliminabile può verificare l'eliminazione dopo l'attesa di qualsiasi Task che non riceve il CancellationTokendel componente. I Taskincompleti possono anche impedire la raccolta dei rifiuti di un componente eliminato.

ComponentBase ignora le eccezioni causate dall'annullamento di Task (più precisamente, ignora tutte le eccezioni se le Taskattese vengono annullate), pertanto i metodi del componente non devono gestire TaskCanceledException e OperationCanceledException.

ComponentBase non può seguire le linee guida precedenti perché non concettualizza ciò che costituisce uno stato valido per un componente derivato e non implementa IDisposable o IAsyncDisposable. Se OnInitializedAsync restituisce un Task incompleto che non usa un CancellationToken e il componente viene eliminato prima del completamento del Task, ComponentBase chiama ancora OnParametersSet e attende OnParametersSetAsync. Se un componente eliminabile non usa un CancellationToken, OnParametersSet e OnParametersSetAsync devono verificare se il componente è stato eliminato.

Evitare le chiamate che bloccano i thread

Di norma, non chiamare i metodi seguenti all'interno dei 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}", 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;
}

Registrare i servizi:

  • Per lo sviluppo lato client, registrare i servizi come singleton nel file 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 del OnNotify componente. InvokeAsync viene utilizzato per passare al contesto corretto e accodare un nuovo 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 ulteriori informazioni, vedere eliminazione 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 rerender. 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 ulteriori informazioni, vedere ASP.NET Core per l'eliminazione dei componenti 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 ulteriori informazioni, vedere La chiamata a InvokeAsync(StateHasChanged) causa il fallback alla cultura predefinita (dotnet/aspnetcore #28521).

Per inviare eccezioni rilevate dallo sfondo TimerService al componente per gestire le eccezioni come normali eccezioni del ciclo di vita, consultare la sezione Gestire le eccezioni rilevate al di fuori del ciclo di vita di un componente Razor.

Gestire le eccezioni rilevate al di fuori del ciclo di vita di un Razor componente

Usare ComponentBase.DispatchExceptionAsync in un componente Razor per gestire 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 dei log che avviene 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 il completamento Task di una chiamata asincrona e aggiorna l'interfaccia utente per indicare che l'operazione è stata completata. Nell'esempio seguente il SendReport metodo non attende un Task oggetto e non segnala il risultato all'utente. Poiché il componente scarta intenzionalmente Task in SendReport, eventuali errori asincroni si verificano fuori dallo stack di chiamate del ciclo di vita normale, pertanto non vengono rilevati 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>

Nei Blazor Web App con il confine di errore applicato solo a un componente statico MainLayout, il confine è attivo solo durante la fase di rendering statico lato server (SSR). 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 componente MainLayout e il resto dei componenti più in basso nella gerarchia dei componenti, abilitare il rendering interattivo per le istanze del componente HeadOutlet e Routes nel componente App (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 sollevata quando il conteggio 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.
  • Nel metodo StartTimer, avviare il servizio timer asincrono nel Action delegato di Task.Run e scartare intenzionalmente l'oggetto restituito Task.

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