Share via


Contexte de synchronisation Blazor ASP.NET Core

Remarque

Ceci n’est pas la dernière version de cet article. Pour la version actuelle, consultez la version .NET 8 de cet article.

Important

Ces informations portent sur la préversion du produit, qui est susceptible d’être en grande partie modifié avant sa commercialisation. Microsoft n’offre aucune garantie, expresse ou implicite, concernant les informations fournies ici.

Pour la version actuelle, consultez la version .NET 8 de cet article.

Blazor utilise un contexte de synchronisation (SynchronizationContext) pour appliquer un seul thread logique d’exécution. Les méthodes de cycle de vie d’un composant et les rappels d’événements déclenchés par Blazor sont exécutés sur le contexte de synchronisation.

Le contexte de synchronisation côté du serveur de Blazor tente d’émuler un environnement monothread afin qu’il corresponde étroitement au modèle WebAssembly dans le navigateur, celui-ci étant monothread. Cette émulation est limitée à un circuit individuel, ce qui signifie que deux circuits différents peuvent fonctionner en parallèle. À tout moment dans un circuit, le travail est effectué sur un seul fil, ce qui donne l’impression d’un seul fil logique. Il n’y a pas deux opérations qui s’exécutent simultanément dans le même circuit.

Éviter les appels de blocage de thread

En règle générale, n’appelez pas les méthodes suivantes dans les composants. Les méthodes suivantes bloquent le thread d’exécution, ce qui empêche l’application de reprendre le travail tant que le Task sous-jacent n’est pas terminé :

Remarque

Les exemples de la documentation Blazor qui utilisent les méthodes de blocage de thread mentionnées dans cette section sont fournis à titre de démonstration et ne constituent pas une approche recommandée en matière de programmation. Par exemple, quelques démonstrations de code de composant simulent un processus de longue durée en appelant Thread.Sleep.

Appeler des méthodes de composant en externe pour mettre à jour l’état

Dans le cas où un composant doit être mis à jour en fonction d’un événement externe, comme un minuteur ou une autre notification, utilisez la méthode InvokeAsync qui distribue l’exécution du code au contexte de synchronisation de Blazor. Par exemple, prenez le service de notification suivant qui peut notifier n’importe quel composant d’écoute de l’état mis à jour. La méthode Update peut être appelée de n’importe où dans l’application.

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

Inscrivez les services :

  • Pour le développement côté client, inscrivez les services en tant que bases de données unique dans le fichier Program côté client :

    builder.Services.AddSingleton<NotifierService>();
    builder.Services.AddSingleton<TimerService>();
    
  • Pour le développement côté serveur, inscrivez les services tels qu’ils sont définis dans le fichier Program du serveur :

    builder.Services.AddScoped<NotifierService>();
    builder.Services.AddScoped<TimerService>();
    

Utilisez NotifierService pour mettre à jour un composant.

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

Dans l'exemple précédent :

  • Le minuteur est lancé en dehors du contexte de synchronisation de donnéesBlazor avec _ = Task.Run(Timer.Start).
  • NotifierService invoque la méthode OnNotify du composant. InvokeAsync sert à basculer vers le bon contexte et à mettre en file d’attente un rendu. Pour plus d’informations, consultez le rendu de composants Razor ASP.NET Core.
  • Le composant implémente IDisposable. Le délégué OnNotify est désabonné dans la méthode Dispose, qui est appelée par le framework quand le composant est supprimé. Pour plus d’informations, consultez le cycle de vie des composants Razor ASP.NET Core.
  • NotifierService appelle la méthode OnNotify du composant en dehors du contexte de synchronisation de Blazor. InvokeAsync sert à basculer vers le bon contexte et à mettre en file d’attente un rendu. Pour plus d’informations, consultez le rendu de composants Razor ASP.NET Core.
  • Le composant implémente IDisposable. Le délégué OnNotify est désabonné dans la méthode Dispose, qui est appelée par le framework quand le composant est supprimé. Pour plus d’informations, consultez le cycle de vie des composants Razor ASP.NET Core.

Important

Si un composant Razor définit un événement déclenché à partir d’un thread d’arrière-plan, il peut être nécessaire pour capturer et restaurer le contexte d’exécution (ExecutionContext) au moment où le gestionnaire est inscrit. Pour plus d’informations, consultez L’appel à InvokeAsync(StateHasChanged) cause le repli de la page sur la culture par défaut (dotnet/aspnetcore #28521).

Pour envoyer les exceptions capturées de l’arrière-plan TimerServicevers le composant afin de les traiter comme des exceptions d’événements normaux du cycle de vie, voir la section Gérer les exceptions capturées en dehors du Razorcycle de vie d’un composant.

Gérer les exceptions interceptées en dehors du cycle de vie d’un composant Razor

Utilisez ComponentBase.DispatchExceptionAsync dans un composant Razor pour traiter les exceptions levées en dehors de la pile des appels de cycle de vie du composant. Cela permet au code du composant de traiter les exceptions comme si elles sont des exceptions de méthode de cycle de vie. Par la suite, les mécanismes de gestion des erreurs de Blazor, tels que limites d’erreur, peuvent traiter les exceptions.

Remarque

ComponentBase.DispatchExceptionAsync est utilisé dans les fichiers de composants Razor (.razor) qui héritent de ComponentBase. Lors de la création de composants qui implement IComponent directly, utilisez RenderHandle.DispatchExceptionAsync.

Pour gérer les exceptions interceptées en dehors du cycle de vie d’un composant Razor, transmettez l’exception à DispatchExceptionAsync et attendez le résultat :

try
{
    ...
}
catch (Exception ex)
{
    await DispatchExceptionAsync(ex);
}

Un scénario courant pour l’approche précédente est le moment où un composant démarre une opération asynchrone, mais n’attend pas un Task, souvent appelé le modèle fire and forget (déclencher et oublier), car la méthode est déclenchée (démarrée) et le résultat de la méthode est oublié (jeté). Si l’opération échoue, vous souhaiterez peut-être que le composant traite l’échec comme exception de cycle de vie des composants pour l’un des objectifs suivants :

  • Placez le composant dans un état d’erreur, par exemple pour déclencher une limite d’erreur .
  • Terminez le circuit en l’absence de limite d’erreur.
  • Déclenchez la même journalisation que pour les exceptions de cycle de vie.

Dans l’exemple suivant, l’utilisateur sélectionne le bouton Envoyer un rapport pour déclencher une méthode en arrière-plan, ReportSender.SendAsync, qui envoie un rapport. Dans la plupart des cas, un composant attend la Task d’un appel asynchrone et met à jour l’interface utilisateur pour indiquer que l’opération s’est terminée. Dans l’exemple suivant, la méthode SendReport n’attend pas de Task et ne signale pas le résultat à l’utilisateur. Étant donné que le composant ignore intentionnellement la Task dans SendReport, les défaillances asynchrones se produisent hors de la pile d’appels de cycle de vie normale, par conséquent ne sont pas visibles par Blazor:

<button @onclick="SendReport">Send report</button>

@code {
    private void SendReport()
    {
        _ = ReportSender.SendAsync();
    }
}

Pour traiter les défaillances telles que les exceptions de méthode de cycle de vie, renvoyez explicitement des exceptions au composant avec DispatchExceptionAsync, comme l’illustre l’exemple suivant :

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

Une autre approche tire profit de Task.Run :

private void SendReport()
{
    _ = Task.Run(async () =>
    {
        try
        {
            await ReportSender.SendAsync();
        }
        catch (Exception ex)
        {
            await DispatchExceptionAsync(ex);
        }
    });
}

Pour une démonstration opérationnelle, implémentez l’exemple de notification du minuteur dans Appeler des méthodes de composant en externe pour mettre à jour l’état. Dans une application Blazor, ajoutez les fichiers suivants à partir de l’exemple de notification du minuteur et inscrivez les services dans le fichier Program, comme l’explique la section :

  • TimerService.cs
  • NotifierService.cs
  • Notifications.razor

L’exemple utilise un minuteur en dehors du cycle de vie d’un composant Razor, où une exception non gérée n’est normalement pas traitée par les mécanismes de gestion des erreurs de Blazor, tels qu’une limite d’erreur.

Tout d’abord, modifiez le code de TimerService.cs pour créer une exception artificielle en dehors du cycle de vie du composant. Dans la boucle while de TimerService.cs, levez une exception lorsque le elapsedCount atteint la valeur de deux :

if (elapsedCount == 2)
{
    throw new Exception("I threw an exception! Somebody help me!");
}

Placez une limite d’erreur dans la disposition principale de l’application. Remplacez le balisage <article>...</article> par le balisage suivant.

Dans 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>

Dans Blazor Web Apps avec la limite d’erreur appliquée uniquement à un composant MainLayout statique, la limite est active uniquement pendant la phase de rendu statique côté serveur (SSR statique). La limite ne s’active pas juste parce qu’un composant plus bas dans la hiérarchie des composants est interactif. Pour activer l’interactivité à grande échelle pour le composant MainLayout et le reste des composants situés plus bas dans la hiérarchie des composants, activez le rendu interactif pour les instances de composant HeadOutlet et Routes dans le composant App (Components/App.razor). L’exemple suivant adopte le mode de rendu Serveur interactif (InteractiveServer) :

<HeadOutlet @rendermode="InteractiveServer" />

...

<Routes @rendermode="InteractiveServer" />

Si vous exécutez l’application à ce stade, l’exception est levée lorsque le nombre écoulé atteint une valeur de deux. Toutefois, l’interface utilisateur ne change pas. La limite d’erreur n’affiche pas le contenu de l’erreur.

Pour répartir les exceptions du service du minuteur vers le composant Notifications, les modifications suivantes sont apportées au composant :

  • Démarrez le minuteur dans une instruction try-catch. Dans la clause catch du bloc try-catch, les exceptions sont renvoyées au composant en passant le Exception à DispatchExceptionAsync et en attendant le résultat.
  • Dans la méthode StartTimer, démarrez le service de minuteur asynchrone dans le délégué Action de Task.Run et abandonnez intentionnellement le Task retourné.

La méthode StartTimer du composant Notifications (Notifications.razor) :

private void StartTimer()
{
    _ = Task.Run(async () =>
    {
        try
        {
            await Timer.Start();
        }
        catch (Exception ex)
        {
            await DispatchExceptionAsync(ex);
        }
    });
}

Lorsque le service du minuteur s’exécute et atteint le nombre de deux, l’exception est envoyée au composant Razor, ce qui déclenche à son tour la limite d’erreur pour afficher le contenu de l’erreur du <ErrorBoundary> dans le composant MainLayout :

Oh, mon Dieu ! Mon Dieu ! - George Takei