Fermer

février 27, 2024

Récupération et partage de données avec le mode de rendu automatique de Blazor

Récupération et partage de données avec le mode de rendu automatique de Blazor


Avec le mode de rendu automatique, votre composant s’exécutera soit sur le serveur, soit dans le navigateur, en fonction de la vitesse à laquelle Blazor WASM peut tourner. Mais vous devez maintenant comprendre comment récupérer et partager des données entre les composants. Voici quelques techniques pratiques que vous pouvez utiliser.

.NET 8 apporte un certain nombre de modifications à Blazor, mais la plus importante arrive peut-être avec ses multiples modes de rendu.

Il est désormais possible de faire fonctionner vos composants de différentes manières :

  • Sur le serveur, en utilisant le rendu statique côté serveur
  • Utiliser le serveur Blazor
  • Utiliser Blazor WASM
  • Utilisation du nouveau mode Auto (WASM si disponible, sinon Serveur)

Le défi, si vous choisissez de mélanger et assortir les modes de rendu, consiste à trouver comment gérer la récupération (et le partage) des données lorsque vos composants peuvent s’exécuter à différents endroits.

Dans cet article, nous explorerons quelques options de gestion des données lorsque vos composants s’exécutent à l’aide du nouveau mode automatique, ainsi que quelques conseils pour partager l’état au fur et à mesure.

Routage vers un composant rendu à l’aide d’InteractiveAuto

Commençons par un scénario dans lequel nous souhaitons restituer les données météorologiques dans un composant exécuté via InteractiveAuto mode de rendu.

@page "/Weather"
@rendermode InteractiveAuto

Avec InteractiveAuto comme mode de rendu, Blazor tentera d’exécuter ce composant via Blazor WASM lorsqu’un utilisateur visite /Weather.

Si le runtime WASM n’est pas dans le cache du navigateur et/ou ne peut pas se charger assez rapidement, alors le serveur sera utilisé à la place, comme solution de secours.

Ajoutez à cela le fait que les composants .NET 8 Razor sont également pré-rendus par défaut et vous vous retrouvez avec un composant qui pourrait être rendu :

  • Une fois sur le serveur (prérendu), en utilisant le rendu côté serveur (où le HTML brut sera renvoyé au navigateur)
  • Là encore, en utilisant soit :
    • Serveur Blazor, ou
    • BlazorWASM

Le résultat est que nous devons nous assurer que notre humble composant fonctionne dans tous ces scénarios !

Maintenant, votre première pensée pourrait être de créer un composant routable et d’exécuter le tout en utilisant InteractiveAuto.

Dans une minute, nous explorerons une alternative, qui s’avère être une option plus simple.mais restons-en là pour l’instant.

Voici un exemple de page simple pour récupérer des données météorologiques.

BlazorDemoApp.Client

@page "/Weather"
@using BlazorDemoApp.Shared.Weather
@rendermode InteractiveAuto
@inject HttpClient HttpClient

<h1>Weather</h1>

@if (_model?.Forecasts == null)
{
    <p>
        <em>Loading...</em>
    </p>
}
else
{
    
}
@code {

    private WeatherForecastModel? _forecasts;

    protected override async Task OnInitializedAsync()
    {
        _forecasts = await HttpClient.GetFromJsonAsync<WeatherForecastModel>("api/weather");
    }
}

Remarquez comment la page spécifie InteractiveAuto comme mode de rendu.

Le défi de cette approche vient du fait que ce composant sera, à un moment donné, rendu via Blazor WASM. Lorsque c’est le cas, le composant devra effectuer des appels au backend via HTTP, car il n’a pas d’accès direct au serveur et/ou aux ressources (telles que les bases de données).

Voici le point de terminaison de démonstration qui renvoie des données météorologiques aléatoires.

public static class WeatherEndpoints
{
    public static void MapWeatherEndpoints(this WebApplication app)
    {
        app.MapGet("api/weather", Handler);
    }

    private static IResult Handler(HttpContext context)
    {
        var startDate = DateOnly.FromDateTime(DateTime.Now);
        var summaries = new[]
            { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };
        var forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = startDate.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = summaries[Random.Shared.Next(summaries.Length)]
        }).ToList();

    	return Results.Ok(new WeatherForecastModel { Forecasts = forecasts });
    }
}

Lorsque nous testons cela dans le navigateur, tout semble fonctionner correctement.

La page se charge, les données sont récupérées et affichées.

Page de démonstration météo affichant les données de prévisions dans un tableau

Nous pouvons voir que l’appel HTTP est effectué pour récupérer des données météorologiques et que les données sont affichées dans l’interface utilisateur.

Mais il y a quelques problèmes.

Problème 1 : les données sont récupérées deux fois

En examinant cela dans le navigateur, nous voyons des données initiales qui sont ensuite remplacées par de nouvelles données. Parce que nous générons des données aléatoires, nous pouvons voir cela se produire, où un ensemble de données météorologiques est affiché puis remplacé par un autre ensemble de données météorologiques (différentes).

Tableau météo qui commence par un ensemble de prévisions puis, après une seconde ou deux, affiche des données différentes même si l'utilisateur regarde toujours la même page

Cela est dû au prérendu, où le composant est rendu en premier sur le serveur. Cela permet de garantir que l’utilisateur semble quelque chose pendant que l’un des modes interactifs entre en jeu.

Lorsque le composant s’affiche à nouveau, à l’aide de Blazor Server ou de Blazor WASM, un nouvel appel est effectué à l’API. À ce stade, de nouvelles données sont récupérées et le composant affiche ces nouvelles données.

Problème 2 : le serveur effectue un saut inutile via HTTP

Le deuxième problème n’est pas un obstacle, mais il est un peu inutile.

Même lorsque le composant est (pré) rendu sur le serveur ou rendu via Blazor Server, nous utilisons HttpClient pour appeler, via le réseau, notre API backend.

Étant donné que l’API et l’application ne font qu’un, cette indirection et cet appel via HTTP sont inutiles, alors que nous pourrions simplement accéder directement au service DB/Backend.

Bien sûr, lors de l’exécution sur WASM, nous avons toujours besoin de cet appel HTTP vers le backend.

Abordons ces deux problèmes dans l’ordre.

Éviter plusieurs appels

Tout d’abord, abordons la double charge de données.

C’est bien pour notre composant de récupérer les données pendant le pré-rendu, mais idéalement, nous aimerions ensuite réutiliser ces données lorsque le composant restitue à nouveau via le serveur ou WASM.

Une façon d’y parvenir est d’utiliser PersistentComponentState pour stocker les données récupérées lors du premier (pré)rendu, puis les récupérer lorsque le composant effectue un rendu interactif.

BlazorDemoApp.Client

@page "/Weather"
@using BlazorDemoApp.Shared.Weather

@rendermode InteractiveAuto
@inject HttpClient HttpClient
@inject PersistentComponentState ApplicationState
@implements IDisposable

<h1>Weather</h1>

@if (_model?.Forecasts == null)
{
    <p>
        <em>Loading...</em>
    </p>
}
else
{
    
}
@code {

    private WeatherForecastModel? _model;
    private PersistingComponentStateSubscription _subscription;

    protected override async Task OnInitializedAsync()
    {
        _subscription = ApplicationState.RegisterOnPersisting(Persist);

        var foundInState = ApplicationState.TryTakeFromJson<WeatherForecastModel>("weather", out var forecasts);

        _model = foundInState
            ? forecasts
            : await HttpClient.GetFromJsonAsync<WeatherForecastModel>("api/weather");
    }

    private Task Persist()
    {
       ApplicationState.PersistAsJson("weather", _model);
       return Task.CompletedTask;
    }

    public void Dispose()
    {
        _subscription.Dispose();
    }

}

Il se passe un peu de choses ici, alors décomposons-le.

Dans OnInitializedAsync Nous vérifions ApplicationState pour voir si nous avons déjà stocké des données météorologiques auparavant.

Lors du premier (pré)rendu, cela reviendra false (introuvable dans l’état), nous allons donc chercher les données météorologiques.

Lors des rendus (interactifs) suivants, cela renverra true (état trouvé), nous pouvons donc utiliser cet état persistant (et enregistrer un nouvel appel pour récupérer les données).

var foundInState = ApplicationState.TryTakeFromJson<WeatherForecastModel>("weather", out var forecasts);

_model = foundInState
            ? forecasts 
            : await HttpClient.GetFromJsonAsync<WeatherForecastModel>("api/weather"); 

Nous avons également mis en place un abonnement à ApplicationState.RegisterOnPersistingque Blazor invoquera lors du rendu et enregistrera notre Persist méthode en tant que gestionnaire pour cela.

Dans le Persist méthode, nous prenons les données météorologiques et les conservons ApplicationState.

private Task Persist()
{
   ApplicationState.PersistAsJson("weather", _model);
   return Task.CompletedTask;
}

Cela garantit que nos données météorologiques récupérées seront conservées par Blazor lorsque le composant sera rendu.

Enfin, nous devons mettre en œuvre IDisposable et supprimez l’abonnement (pour éviter toute fuite de mémoire potentielle). Avec cela, les données sont récupérées une fois, conservées dans l’état du composant (sur le serveur), puis récupérées à partir de l’état du composant (sur le client, exécuté dans le navigateur).

Évitez l’appel HTTP lors du rendu sur le serveur

Le deuxième défi consiste à ignorer l’appel HTTP inutile lors du rendu sur le serveur et à accéder directement à la base de données. Il s’agit d’un cas classique de besoin de deux implémentations de la même exigence, qui est plus facilement résolu en utilisant une interface.

Nous appelons actuellement HttpClient direct, mais si nous mettons cela derrière une interface, nous pouvons avoir deux implémentations du code pour récupérer les données météorologiques : une qui passe via HTTP et une qui passe directement.

Nous pouvons d’abord définir une interface pour récupérer les données :

BlazorDemoApp.Shared/Weather/IWeatherService.cs

public interface IWeatherService
{
    public Task<WeatherForecastModel?> FetchAsync();
}

Refactorisez ensuite notre code existant dans une implémentation pour le client.

BlazorDemoApp.Client/Weather/ClientWeatherService

public class ClientWeatherService(HttpClient httpClient) : IWeatherService
{
    public async Task<WeatherForecastModel?> FetchAsync()
    {
        return await httpClient.GetFromJsonAsync<WeatherForecastModel>("api/weather");
    }
}

Nous devons enregistrer cette implémentation dans le dossier du projet client. Programme.cs.

var builder = WebAssemblyHostBuilder.CreateDefault(args);

builder.Services.AddScoped<IWeatherService, ClientWeatherService>();

...

Nous pouvons maintenant porter notre attention sur le serveur.

BlazorDemoApp/Endpoints/ServerWeatherService.cs

public class ServerWeatherService : IWeatherService
{
    public async Task<WeatherForecastModel?> FetchAsync()
    {
        var startDate = DateOnly.FromDateTime(DateTime.Now);
        var summaries = new[]
            { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };
        var forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = startDate.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = summaries[Random.Shared.Next(summaries.Length)]
        }).ToList();

        return new WeatherForecastModel { Forecasts = forecasts };
    }
}

Cette fois, nous pouvons récupérer les données directement. En pratique, cela interagirait probablement avec une base de données ou une autre ressource.

Comme nous l’avons fait avec le projet client, nous devons enregistrer cette implémentation dans le fichier du projet serveur. Programme.cs.

builder.Services.AddScoped<IWeatherService, ServerWeatherService>();

À ce stade, nous pouvons mettre à jour le point de terminaison de l’API pour utiliser cette même implémentation.

De cette façon, nous avons une version de la vérité en ce qui concerne la récupération des données météorologiques, et les deux implémentations finiront par utiliser exactement le même code sur le serveur.

public static class WeatherEndpoints
{
    public static void MapWeatherEndpoints(this WebApplication app)
    {
        app.MapGet("api/weather", Handler);
    }

    private static async Task<IResult> Handler(HttpContext context, IWeatherService weatherService)
    {
        return Results.Ok(await weatherService.FetchAsync());
    }
}

Enfin, nous devons modifier le composant Météo pour utiliser ce nouveau service pour récupérer les données :

@page "/ServerWeather"
@using BlazorDemoApp.Shared.Weather
@inject IWeatherService WeatherService

...
@code {

    private WeatherForecastModel? _model;
    private PersistingComponentStateSubscription _subscription;

    protected override async Task OnInitializedAsync()
    {
        _subscription = ApplicationState.RegisterOnPersisting(Persist);

        var foundInState = ApplicationState.TryTakeFromJson<WeatherForecastModel>("weather", out var forecasts);

        _model = foundInState
            ? forecasts
            : await WeatherService.FetchAsync(); 
    }

    private Task Persist()
    {
       ApplicationState.PersistAsJson("weather", _model);
       return Task.CompletedTask;
    }

    public void Dispose()
    {
        _subscription.Dispose();
    }

}

Une option plus simple ?

Nous sommes maintenant arrivés à un point où tout fonctionne, mais nous avons dû mettre en œuvre pas mal de modèles pour mettre en place cette configuration.

Nous avons fini par interagir avec ApplicationState pour réduire le nombre de fois où nous récupérons des données.

Nous nous sommes également retrouvés avec plusieurs implémentations d’un service pour récupérer des données, afin d’éviter les appels réseau inutiles si nous effectuons la récupération sur le client.

Avec ces changements, nous avons couvert toutes les bases.

Que nous gardions le prérendu activé ou que nous le désactivions finalement pour cette page, le composant pourra toujours récupérer des données, qu’il soit exécuté via Blazor Server ou Blazor WASM.

Mais si nous examinons simplement les exigences sous un angle légèrement différent, nous pouvons voir une autre façon de faire fonctionner cela avec .NET 8.

Pour le moment, notre Weather Le composant récupère ses propres données. En conséquence, comme il est configuré pour s’exécuter via InteractiveAutoil doit être capable de récupérer des données dans tous les modes de rendu.

Mais que se passerait-il si nous devions récupérer les données dans un composant « hôte » distinct, qui s’exécute toujours sur le serveur ?

On pourrait alors avoir un deuxième composant, qui accepterait les données météo via un paramètre. Ce nouveau deuxième composant pourrait fonctionner en utilisant InteractiveAuto mode de rendu.

Diagramme montrant un composant Index exécuté sur un serveur, qui récupère les données d'une base de données puis les transmet à un autre composant appelé WeatherTable

Voici un exemple pour que cela soit un peu plus clair !

BlazorDemoApp/…/Weather/Index.razor

@page "/ServerWeather"
@using BlazorDemoApp.Shared.Weather
@inject ServerWeatherService WeatherService

<WeatherTable Model="_model" @rendermode="InteractiveAuto"/>
@code {
    private WeatherForecastModel? _model;

    protected override async Task OnInitializedAsync()
    {
        _model = await WeatherService.FetchAsync();
    }
}

Ce composant sera rendu sur le serveur (car aucun autre mode de rendu n’est spécifié).

Sur le serveur, il récupérera les données météorologiques (et pourra accéder directement à la base de données car tout cela s’exécute sur le serveur).

Une fois qu’il dispose de ces données, il peut les transmettre à un nouveau composant, appelé WeatherTable.

WeatherTable n’a qu’un seul travail : récupérer les données qui lui sont fournies via son Model paramètre et l’afficher.

WeatherTable.rasoir

@using BlazorDemoApp.Shared.Weather
<h3>WeatherTable</h3>

@if (Model?.Forecasts == null)
{
    <p>
        <em>Loading...</em>
    </p>
}
else
{
    <table class="table">
        <thead>
        <tr>
            <th>Date</th>
            <th>Temp. (C)</th>
            <th>Temp. (F)</th>
            <th>Summary</th>
        </tr>
        </thead>
        <tbody>
        @foreach (var forecast in Model.Forecasts)
        {
            <tr>
                <td>@forecast.Date.ToShortDateString()</td>
                <td>@forecast.TemperatureC</td>
                <td>@forecast.TemperatureF</td>
                <td>@forecast.Summary</td>
            </tr>
        }
        </tbody>
    </table>
}
@code {

    [Parameter] public WeatherForecastModel? Model { get; set; }
    
}

Comme vous pouvez le constater, ce composant est beaucoup plus simple. Il n’a pas besoin de se soucier des services injectés ou de l’état persistant des composants.

Ce WeatherTable Le composant peut s’exécuter dans n’importe quel mode de rendu, à condition qu’il reçoive des données via son Model paramètre.

Comment les données sont-elles partagées entre les modes de rendu ?

Tout cela vous amène peut-être à vous demander comment Blazor gère réellement cela.

Compte tenu de l’hôte Météo/Index.razor le composant est en cours d’exécution sur le serveur et WeatherTable est potentiellement exécuté dans le navigateur, via WASM, comment Blazor transmet-il les données de l’un à l’autre ?

La réponse se trouve dans le code HTML généré lorsque Index, rasoir est rendu sur le serveur.

Voici un exemple simplifié.

<article>
    
    <h3>WeatherTable</h3>
    <table class="table">
        <thead>
            <tr>
                <th>Date</th>
                <th>Temp. (C)</th>
                <th>Temp. (F)</th>
                <th>Summary</th>
            </tr>
        </thead>
        <tbody>
			...
        </tbody>
    </table>
</article>

Pour les données qui doivent « traverser les frontières », Blazor (sur le serveur) les code et les inclut dans le code HTML rendu.

Blazor sur le client décode ensuite ces données et les utilise pour remplir le Model paramètre activé WeatherTable.

En pratique, vous ne devriez pas avoir besoin d’entrer dans les détails de la façon dont cela fonctionne, mais il est utile de savoir ce qui se passe lorsque vous partagez des données entre des composants exécutés avec différents modes de rendu.

Enfin, il convient de noter que cette table Météo n’est pas réellement le meilleur exemple de composant que vous voudriez exécuter en utilisant InteractiveAuto mode de rendu.

Comme nous rendons simplement des données, ce composant fonctionnerait probablement mieux en tant que composant rendu par le serveur. InteractiveAuto est utile si vous disposez d’un composant avec lequel les utilisateurs interagiront, en cliquant sur des boutons, en basculant des curseurs, etc.

Dans ce cas, il est logique d’utiliser l’un des modes interactifs de Blazor et les techniques que nous avons explorées ici pour gérer la récupération et le partage des données.

En résumé

.NET 8 offre plus d’outils et d’options pour créer des applications à l’aide de Razor Components et Blazor. Parmi ceux-ci, la possibilité de mélanger et de faire correspondre les modes de rendu est essentielle.

Le mode Interactive Auto permet d’exécuter vos composants à l’aide de Blazor WASM, mais de revenir à Blazor Server si WASM n’est pas disponible.

Lorsque vous utilisez ce mode, cela peut ajouter un certain degré de complexité à vos composants, pour garantir qu’ils fonctionnent dans tous les modes de rendu.

Il existe deux manières principales de faire fonctionner votre composant InteractiveAuto mode.

La première consiste à rendre le composant lui-même capable de récupérer des données dans tous les modes de rendu. Avec cette option, vous pouvez limiter le nombre de fois où vous récupérez les mêmes données en conservant l’état du composant entre les rendus.

Vous pouvez également éviter les appels HTTP inutiles en implémentant deux versions de votre logique métier/code de récupération de données : une pour le client qui passe via HTTP et une pour le serveur qui contourne cet appel HTTP.

Cependant, une approche sans doute plus simple consiste à utiliser un composant « hôte » qui s’exécute sur le serveur et récupère les données, puis à disposer d’un composant distinct qui récupère ces données via un paramètre.

Ce « deuxième » composant peut ensuite s’exécuter en utilisant le mode de rendu que vous préférez et travailler avec les données qui lui ont été fournies, mais sans avoir à gérer la complexité de la récupération de ces données elle-même.


Utilisez une bibliothèque de composants synchronisée avec la cadence de publication de Microsoft. Interface utilisateur Telerik pour Blazor propose plus de 110 composants Blazor véritablement natifs pour permettre de développer rapidement et facilement de nouvelles applications Blazor ou de moderniser des applications existantes. Essayez-le gratuitement pendant 30 jours.




Source link