Rendu de composants ASP.NET Core Razor

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.

Cet article explique le rendu des composants Razor dans les applications ASP.NET Core Blazor, notamment quand appeler StateHasChanged pour déclencher manuellement un composant à afficher.

Conventions de rendu pour ComponentBase

Les composants doivent être rendus lorsqu’ils sont ajoutés pour la première fois à la hiérarchie des composants par un composant parent. Il s’agit du seul moment où un composant doit obligatoirement être rendu. Les composants peuvent être rendus à d’autres moments selon leur propre logique et leurs propres conventions.

Par défaut, les composants Razor héritent de la classe de base ComponentBase, qui contient la logique pour déclencher une nouvelle génération aux moments suivants :

Les composants hérités de ComponentBase ignorent les nouveaux rendus en raison des mises à jour de paramètres si l’une des valeurs suivantes est true :

  • Tous les paramètres proviennent d’un ensemble de types connus† ou d’un type primitif qui n’a pas changé depuis le jeu de paramètres précédent.

    †Le framework Blazor utilise un ensemble de règles intégrées et des vérifications explicites de type de paramètre pour la détection des modifications. Ces règles et les types sont susceptibles d’être modifiés à tout moment. Pour plus d’informations, consultez l’API ChangeDetection dans la source de référence ASP.NET Core.

    Remarque

    Les liens de documentation vers la source de référence .NET chargent généralement la branche par défaut du référentiel, qui représente le développement actuel pour la prochaine version de .NET. Pour sélectionner une balise pour une version spécifique, utilisez la liste déroulante Échanger les branches ou les balises. Pour plus d’informations, consultez Comment sélectionner une balise de version du code source ASP.NET Core (dotnet/AspNetCore.Docs #26205).

  • Le remplacement de la méthode ShouldRender du composant retourne false (l’implémentation ComponentBase par défaut retourne toujours true).

Contrôler le flux de rendu

Dans la plupart des cas, les conventions ComponentBase entraînent le sous-ensemble correct de nouveaux rendus de composant après qu’un événement se soit produit. Les développeurs ne sont généralement pas tenus de fournir une logique manuelle pour indiquer au framework les composants à restituer à nouveau et quand le faire. L’effet global des conventions du framework est que le composant qui reçoit un événement effectue son rendu à nouveau lui-même, ce qui déclenche de manière récursive le nouveau rendu des composants descendants dont les valeurs de paramètre peuvent avoir changé.

Pour plus d’informations sur les implications en termes de performances des conventions du framework et sur la façon d’optimiser la hiérarchie des composants d’une application pour le rendu, consultez Meilleures pratiques en matière de performances ASP.NET Core Blazor.

Rendu en streaming

Utilisez le rendu en streaming avec le rendu côté serveur statique (SSR statique) ou le prérendu pour diffuser les mises à jour de contenu dans le flux de réponses et améliorer l’expérience de l’utilisateur pour les composants qui exécutent des tâches asynchrones de longue durée pour effectuer un rendu complet.

Par exemple, considérez un composant qui effectue une requête de base de données longue ou un appel d’API web pour afficher des données lorsque la page se charge. Normalement, les tâches asynchrones exécutées dans le cadre du rendu d’un composant côté serveur doivent être terminées avant l’envoi de la réponse rendue, ce qui peut retarder le chargement de la page. Tout retard significatif dans le rendu de la page nuit à l’expérience utilisateur. Pour améliorer l’expérience utilisateur, le rendu en streaming affiche initialement la page entière rapidement, avec du contenu d’espace réservé, pendant que les opérations asynchrones s’exécutent. Une fois les opérations terminées, le contenu mis à jour est envoyé au client sur la même connexion de réponse et corrigé dans le DOM.

Le rendu de diffusion en continu exige que le serveur évite de mettre la sortie en mémoire tampon. Les données de réponse doivent être transmises au client à mesure que les données sont générées. Pour les hôtes qui appliquent la mise en mémoire tampon, le rendu de diffusion en continu se dégrade sans perte de données et la page se charge sans rendu de diffusion en continu.

Appliquez l’attribut [StreamRendering(true)] au composant pour diffuser en continu des mises à jour de contenu lors de l’utilisation d’un rendu côté serveur statique (SSR statique) ou d’un prérendu. Le rendu en streaming doit être explicitement activé, car les mises à jour diffusées en continu peuvent entraîner le déplacement du contenu sur la page. Les composants sans l’attribut adoptent automatiquement le rendu en streaming si le composant parent utilise la fonctionnalité. Passez false à l’attribut d’un composant enfant pour désactiver la fonctionnalité à ce stade et descendre plus loin dans la sous-arborescence du composant. L’attribut est fonctionnel lorsqu’il est appliqué aux composants fournis par une Razor bibliothèque de classes.

L’exemple suivant est basé sur le composant Weather d’une application créée à partir du Blazor modèle de projet Web App. L’appel à Task.Delay simule la récupération asynchrone de données météorologiques. Le composant affiche initialement le contenu de l’espace réservé (« Loading... ») sans attendre la fin du délai asynchrone. Lorsque le délai asynchrone se termine et que le contenu des données météorologiques est généré, le contenu est diffusé en continu vers la réponse et corrigé dans la table des prévisions météorologiques.

Weather.razor:

@page "/weather"
@attribute [StreamRendering(true)]

...

@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        ...
        <tbody>
            @foreach (var forecast in forecasts)
            {
                <tr>
                    <td>@forecast.Date.ToShortDateString()</td>
                    <td>@forecast.TemperatureC</td>
                    <td>@forecast.TemperatureF</td>
                    <td>@forecast.Summary</td>
                </tr>
            }
        </tbody>
    </table>
}

@code {
    ...

    private WeatherForecast[]? forecasts;

    protected override async Task OnInitializedAsync()
    {
        await Task.Delay(500);

        ...

        forecasts = ...
    }
}

Supprimer l’actualisation de l’interface utilisateur (ShouldRender)

ShouldRender est appelé chaque fois qu’un composant est rendu. Remplacez ShouldRender pour gérer l’actualisation de l’interface utilisateur. Si l’implémentation retourne true, l’interface utilisateur est actualisée.

Même si ShouldRender est remplacé, le composant est toujours rendu initialement.

ControlRender.razor:

@page "/control-render"

<PageTitle>Control Render</PageTitle>

<h1>Control Render Example</h1>

<label>
    <input type="checkbox" @bind="shouldRender" />
    Should Render?
</label>

<p>Current count: @currentCount</p>

<p>
    <button @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;
    private bool shouldRender = true;

    protected override bool ShouldRender()
    {
        return shouldRender;
    }

    private void IncrementCount()
    {
        currentCount++;
    }
}
@page "/control-render"

<label>
    <input type="checkbox" @bind="shouldRender" />
    Should Render?
</label>

<p>Current count: @currentCount</p>

<p>
    <button @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;
    private bool shouldRender = true;

    protected override bool ShouldRender()
    {
        return shouldRender;
    }

    private void IncrementCount()
    {
        currentCount++;
    }
}
@page "/control-render"

<label>
    <input type="checkbox" @bind="shouldRender" />
    Should Render?
</label>

<p>Current count: @currentCount</p>

<p>
    <button @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;
    private bool shouldRender = true;

    protected override bool ShouldRender()
    {
        return shouldRender;
    }

    private void IncrementCount()
    {
        currentCount++;
    }
}
@page "/control-render"

<label>
    <input type="checkbox" @bind="shouldRender" />
    Should Render?
</label>

<p>Current count: @currentCount</p>

<p>
    <button @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;
    private bool shouldRender = true;

    protected override bool ShouldRender()
    {
        return shouldRender;
    }

    private void IncrementCount()
    {
        currentCount++;
    }
}
@page "/control-render"

<label>
    <input type="checkbox" @bind="shouldRender" />
    Should Render?
</label>

<p>Current count: @currentCount</p>

<p>
    <button @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;
    private bool shouldRender = true;

    protected override bool ShouldRender()
    {
        return shouldRender;
    }

    private void IncrementCount()
    {
        currentCount++;
    }
}

Pour plus d’informations sur les meilleures pratiques en matière de performances relatives à ShouldRender, consultez Meilleures pratiques en matière de performances ASP.NET Core Blazor.

Quand appeler StateHasChanged

L’appel à StateHasChanged vous permet de déclencher un rendu à tout moment. Toutefois, veillez à ne pas appeler StateHasChanged inutilement, ce qui est une erreur courante qui impose des coûts de rendu inutiles.

Le code ne doit pas avoir besoin d’appeler StateHasChanged pour :

  • La gestion régulière des événements, de manière synchrone ou asynchrone, car ComponentBase déclenche un rendu pour la plupart des gestionnaires d’événements de routine.
  • L’implémentation d’une logique de cycle de vie classique, comme OnInitialized ou OnParametersSetAsync, de manière synchrone ou asynchrone, car ComponentBase déclenche un rendu pour les événements de cycle de vie classiques.

Toutefois, il peut être judicieux d’appeler StateHasChanged dans les cas décrits dans les sections suivantes de cet article :

Un gestionnaire asynchrone implique plusieurs phases asynchrones

En raison de la façon dont les tâches sont définies dans .NET, un récepteur d’un Task peut uniquement observer son achèvement final, et non les états asynchrones intermédiaires. Par conséquent, ComponentBase ne peut déclencher un nouveau rendu que lorsque le Task est retourné pour la première fois et que le Task se termine. Le framework ne peut pas savoir comment rendre un composant à d’autres points intermédiaires, par exemple quand un IAsyncEnumerable<T>retourne des données dans une série de Task intermédiaires. Si vous souhaitez effectuer une nouvelle création à des points intermédiaires, appelez StateHasChanged sur ces points.

Considérez le composant CounterState1 suivant, qui met à jour le compte quatre fois chaque fois que la méthode IncrementCount s’exécute :

  • Les rendus automatiques se produisent après le premier et le dernier incréments de currentCount.
  • Les rendus manuels sont déclenchés par des appels à StateHasChanged lorsque le framework ne déclenche pas automatiquement de nouveaux rendus aux points de traitement intermédiaires où currentCount est incrémenté.

CounterState1.razor:

@page "/counter-state-1"

<PageTitle>Counter State 1</PageTitle>

<h1>Counter State Example 1</h1>

<p>
    Current count: @currentCount
</p>

<p>
    <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;

    private async Task IncrementCount()
    {
        currentCount++;
        // Renders here automatically

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        // Renders here automatically
    }
}
@page "/counter-state-1"

<p>
    Current count: @currentCount
</p>

<p>
    <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;

    private async Task IncrementCount()
    {
        currentCount++;
        // Renders here automatically

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        // Renders here automatically
    }
}
@page "/counter-state-1"

<p>
    Current count: @currentCount
</p>

<p>
    <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;

    private async Task IncrementCount()
    {
        currentCount++;
        // Renders here automatically

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        // Renders here automatically
    }
}
@page "/counter-state-1"

<p>
    Current count: @currentCount
</p>

<p>
    <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;

    private async Task IncrementCount()
    {
        currentCount++;
        // Renders here automatically

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        // Renders here automatically
    }
}
@page "/counter-state-1"

<p>
    Current count: @currentCount
</p>

<p>
    <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;

    private async Task IncrementCount()
    {
        currentCount++;
        // Renders here automatically

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        // Renders here automatically
    }
}

Réception d’un appel d’un élément externe au système de rendu et de gestion des événements Blazor

ComponentBase connaît uniquement ses propres méthodes de cycle de vie et ses événements déclenchés par Blazor. ComponentBase ne connaît pas les autres événements qui peuvent se produire dans le code. Par exemple, tous les événements C# déclenchés par un magasin de données personnalisé sont inconnus de Blazor. Pour que de tels événements déclenchent une nouvelle mise à jour afin d’afficher les valeurs mises à jour dans l’interface utilisateur, appelez StateHasChanged.

Considérez le composant CounterState2 suivant qui utilise System.Timers.Timer pour mettre à jour un nombre à intervalles réguliers et appelle StateHasChanged pour mettre à jour l’interface utilisateur :

  • OnTimerCallback s’exécute en dehors d’un flux de rendu managé par Blazor ou d’une notification d’événement. Par conséquent, OnTimerCallback doit appeler StateHasChanged, car Blazor n’a pas connaissance des modifications apportées à currentCount dans le rappel.
  • Le composant implémente IDisposable, où le Timer est supprimé lorsque le framework appelle la méthode Dispose. Pour plus d’informations, consultez le cycle de vie des composants Razor ASP.NET Core.

Étant donné que le rappel est appelé en dehors du contexte de synchronisation de Blazor, le composant doit encapsuler la logique de OnTimerCallback dans ComponentBase.InvokeAsync pour le déplacer vers le contexte de synchronisation du système de rendu. Cela revient à marshaler vers le thread d’interface utilisateur dans d’autres frameworks d’interface utilisateur. StateHasChanged ne peut être appelé qu’à partir du contexte de synchronisation du convertisseur et lève une exception dans le cas contraire :

System.InvalidOperationException : « Le thread actuel n’est pas associé au répartiteur. Utilisez InvokeAsync() pour basculer l’exécution vers le répartiteur lors du déclenchement de l’état du rendu ou du composant. »

CounterState2.razor:

@page "/counter-state-2"
@using System.Timers
@implements IDisposable

<PageTitle>Counter State 2</PageTitle>

<h1>Counter State Example 2</h1>

<p>
    This counter demonstrates <code>Timer</code> disposal.
</p>

<p>
    Current count: @currentCount
</p>

@code {
    private int currentCount = 0;
    private Timer timer = new(1000);

    protected override void OnInitialized()
    {
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            currentCount++;
            StateHasChanged();
        });
    }

    public void Dispose() => timer.Dispose();
}
@page "/counter-state-2"
@using System.Timers
@implements IDisposable

<h1>Counter with <code>Timer</code> disposal</h1>

<p>
    Current count: @currentCount
</p>

@code {
    private int currentCount = 0;
    private Timer timer = new(1000);

    protected override void OnInitialized()
    {
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            currentCount++;
            StateHasChanged();
        });
    }

    public void Dispose() => timer.Dispose();
}
@page "/counter-state-2"
@using System.Timers
@implements IDisposable

<h1>Counter with <code>Timer</code> disposal</h1>

<p>
    Current count: @currentCount
</p>

@code {
    private int currentCount = 0;
    private Timer timer = new(1000);

    protected override void OnInitialized()
    {
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            currentCount++;
            StateHasChanged();
        });
    }

    public void Dispose() => timer.Dispose();
}
@page "/counter-state-2"
@using System.Timers
@implements IDisposable

<h1>Counter with <code>Timer</code> disposal</h1>

<p>
    Current count: @currentCount
</p>

@code {
    private int currentCount = 0;
    private Timer timer = new(1000);

    protected override void OnInitialized()
    {
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            currentCount++;
            StateHasChanged();
        });
    }

    public void Dispose() => timer.Dispose();
}
@page "/counter-state-2"
@using System.Timers
@implements IDisposable

<h1>Counter with <code>Timer</code> disposal</h1>

<p>
    Current count: @currentCount
</p>

@code {
    private int currentCount = 0;
    private Timer timer = new Timer(1000);

    protected override void OnInitialized()
    {
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            currentCount++;
            StateHasChanged();
        });
    }

    public void Dispose() => timer.Dispose();
}

Pour afficher le composant en dehors de la sous-arborescence qui est rendue à nouveau par un événement particulier

L’interface utilisateur peut impliquer :

  1. La distribution d’un événement à un composant.
  2. La modification d’un état.
  3. Le nouveau rendu d’un composant complètement différent qui n’est pas un descendant du composant recevant l’événement.

Une façon de gérer ce scénario consiste à fournir une classe de gestion d’état, souvent en tant que service d’injection de dépendances (DI) injecté dans plusieurs composants. Quand un composant appelle une méthode sur le gestionnaire d’état, le gestionnaire d’état déclenche un événement C# qui est ensuite reçu par un composant indépendant.

Pour connaître les approches de gestion de l’état, consultez les ressources suivantes :

Pour l’approche du gestionnaire d’état, les événements C# sont en dehors du pipeline de rendu Blazor. Appelez StateHasChanged sur les autres composants que vous souhaitez rendre à nouveau en réponse aux événements du gestionnaire d’état.

L’approche du gestionnaire d’état est similaire au cas précédent avec System.Timers.Timer dans la section précédente. Étant donné que la pile des appels d’exécution reste généralement sur le contexte de synchronisation du convertisseur, l’appel InvokeAsync n’est normalement pas nécessaire. L’appel InvokeAsync n’est requis que si la logique échappe au contexte de synchronisation, par exemple en appelant ContinueWith sur un Task ou en attendant un Task avec ConfigureAwait(false). Pour plus d’informations, consultez la section Réception d’un appel provenant d’un élément externe au système de rendu et de gestion des événements Blazor.

Indicateur de progression de chargement WebAssembly pour Web Apps Blazor

Un indicateur de progression de chargement n’est pas présent dans les applications créées à partir du modèle de projet Application web Blazor. Une nouvelle fonction d'indicateur de progression du chargement est prévue pour une prochaine version de .NET. En attendant, une application peut adopter un code personnalisé pour créer un indicateur de progression du chargement. Pour plus d’informations, consultez Démarrage ASP.NET Core Blazor.