Fermer

janvier 17, 2023

Simplifiez vos applications Blazor à l’aide des nouveaux modificateurs de liaison de .NET 7

Simplifiez vos applications Blazor à l’aide des nouveaux modificateurs de liaison de .NET 7


Si vous avez besoin d’exécuter du code asynchrone en réponse à l’utilisation de l’entrée, .NET 7 est là pour vous avec ses nouveaux modificateurs de liaison.

Avez-vous déjà eu besoin d’exécuter du code asynchrone en réponse à une entrée utilisateur dans votre application Blazor ? Si tel est le cas, vous avez probablement réalisé que ce n’était pas une tâche triviale avant .NET 7.

Voici un exemple. Supposons que vous souhaitiez que les utilisateurs saisissent leur biographie, puis synchronisez la valeur saisie avec le stockage local (dans le navigateur).

Vous pouvez bien sûr stocker la valeur n’importe où, y compris en faisant un appel API à un backend pour la stocker dans une base de données, mais nous nous en tiendrons au stockage local pour l’instant, comme un moyen pratique d’explorer comment tout cela fonctionne.

Nous avons d’abord besoin d’une entrée de texte quelconque.

@page "/BindModifiers"

<textarea @bind="bio" placeholder="Introduce yourself"></textarea>

<p>
    @bio
</p>
@code {
    
    string bio;
    
}

j’ai défini un textarea input et lié à un champ appelé bio.

Avec cela, toute modification de la textarea la valeur se reflétera dans bio et si bio est modifié directement (via le code, en réponse à un autre événement) le textarea reflétera automatiquement la nouvelle valeur.

Par défaut, cette liaison s’exécutera lorsque le textarea perd le focus donc, si nous voulons que la liaison prenne effet immédiatement, nous pouvons modifier l’événement de liaison comme suit :

<textarea @bind="bio" @bind:event="oninput" placeholder="Introduce yourself"></textarea>

@bind:event="oninput" assure ici la bio Le champ est mis à jour avec la nouvelle valeur dès qu’il change.

Exécution du code asynchrone après la liaison (avant .NET 7)

Qu’en est-il maintenant de cette exigence de stocker la valeur saisie dans le stockage local ?

Nous voulons assurer notre liaison bidirectionnelle (pour stocker la valeur dans le bio ) continue de fonctionner et stocke également la valeur saisie dans le stockage local.

Il s’agissait d’une tâche non triviale avec Blazor avant .NET 7.

Le problème, en essayant d’effectuer des tâches asynchrones comme celle-ci dans le cadre du processus de liaison, est que vous pouvez finir par ralentir l’interface utilisateur par inadvertance.

Toute tentative d’écriture sur le stockage local pendant la liaison entraînera des problèmes (en particulier si nous devions passer du stockage local à une base de données ou à un appel d’API) et pourrait entraîner le blocage des mises à jour de l’interface utilisateur jusqu’à la fin du processus asynchrone plus lent.

Il est facile d’imaginer qu’un utilisateur soit frustré si chaque pression de touche entraîne un délai visible avant que la valeur n’apparaisse dans le textarea.

La meilleure option serait d’effectuer la tâche asynchrone une fois la liaison terminée. De cette façon, l’interface utilisateur reste réactive, mais vous avez toujours un moyen de déclencher la logique chaque fois que le processus de liaison s’est produit (dans ce cas, chaque fois que la valeur change).

Avant .NET 7, cela était délicat en raison du fonctionnement de la liaison bidirectionnelle.

Vous voyez, sous le capot, Blazor utilise le oninput événement pour gérer sa liaison.

Bien que vous définissiez vos composants Blazor à l’aide de Razor, .NET n’exécute pas ces composants directement. Au lieu de cela, il les convertit en code C# pur.

Si nous regardons ce code généré pour notre composant Blazor (tel qu’il est actuellement), nous voyons quelque chose comme ceci :

protected override void BuildRenderTree(RenderTreeBuilder __builder)
{
    
    
    __builder.OpenElement(0, "textarea"); 
    __builder.AddAttribute(2, "value", BindConverter.FormatValue(this.bio));
    __builder.AddAttribute<ChangeEventArgs>(3, "oninput", EventCallback.Factory.CreateBinder((object) this, (Action<string>) (__value => this.bio = __value), this.bio));
    __builder.CloseElement(); 
}

Remarquez comment Blazor a attaché un gestionnaire au oninputévénement qui définit this.bio à la valeur saisie.

Le défi, si nous voulons également effectuer des actions pour ce même oninput événement, est que nous ne pouvons pas attacher un autre gestionnaire à ce même événement.

La solution de contournement dans .NET 6 (et ci-dessous) consistait à gérer oninput vous-même, vous pouvez ensuite écrire votre propre code pour mettre à jour le champ bio et exécuter votre code asynchrone.

Mais même dans ce cas, vous deviez vous assurer que votre code asynchrone s’exécutait sans bloquer l’interface utilisateur entre-temps.

Heureusement, .NET 7 rend cela beaucoup, beaucoup plus facile.

Exécution du code asynchrone une fois la liaison terminée (.NET 7 et supérieur)

.NET 7 répond à cette exigence avec un nouveau bind:after modificateur de liaison.

Avec cela, vous pouvez conserver vos liaisons existantes et attacher un gestionnaire pour exécuter la logique une fois la liaison terminée.

<textarea @bind="bio" @bind:event="oninput" @bind:after="Sync" placeholder="Introduce yourself"></textarea>

<p>
    @bio
</p>
@code {

    string bio;

    private async Task Sync()
    {
        
        Console.WriteLine("Syncing to local storage");
    }
    
}

Remarquez comment nous utilisons @bind:after pour exécuter une logique supplémentaire, dans ce cas en la pointant vers notre Sync méthode.

Avec cela, notre interface utilisateur restera réactive et le code permettant de synchroniser la valeur avec le stockage local sera également appelé.

Voici le code généré qui s’exécute réellement lorsque nous l’exécutons dans le navigateur (notez que vous n’avez pas besoin de connaître ou de bien comprendre cela pour utiliser @bind:after mais cela peut être utile pour savoir ce qui se passe !)

namespace BlazorExamples.WASM.Pages
{
  [Route("/BindModifiers")]
  public class BindModifiers : ComponentBase
  {
    private 
    #nullable disable
    string bio;

    protected override void BuildRenderTree(RenderTreeBuilder __builder)
    {
      __builder.OpenElement(0, "textarea");
      __builder.AddAttribute(1, "placeholder", "Introduce yourself");
      __builder.AddAttribute(2, "value", BindConverter.FormatValue(this.bio));
      __builder.AddAttribute<ChangeEventArgs>(3, "oninput", EventCallback.Factory.CreateBinder((object) this, RuntimeHelpers.CreateInferredBindSetter<string>((Func<string, Task>) (__value =>
      {
        this.bio = __value;
        return RuntimeHelpers.InvokeAsynchronousDelegate(new Func<Task>(this.Sync));
      }), this.bio), this.bio));
      __builder.SetUpdatesAttributeName("value");
      __builder.CloseElement();
  	
        
    }

    private async Task Sync() => Console.WriteLine("Syncing to local storage");
  }
}

Blazor est toujours en train de gérer oninput mais maintenant il effectue deux tâches : mettre à jour le bio domaine et invoquant notre Sync méthode.

@bind:afterest super pratique pour ce genre d’exigence.

Il fournit un moyen sûr d’exécuter du code asynchrone une fois la liaison terminée tout en laissant Blazor gérer la liaison elle-même (lecture et mise à jour de la valeur) afin que nous ne puissions pas accidentellement casser cette fonctionnalité.

Blazor attribue la nouvelle valeur à bio avant notre logique dans Sync est invoqué, garantissant ainsi que les mécanismes de sécurité existants restent en place (par exemple, Blazor Server implémente une logique pour s’assurer que les frappes ne sont pas accidentellement perdues).

Stocker les valeurs dans le stockage local

Pour être complet, pour synchroniser réellement avec le stockage local, nous pouvons utiliser le pratique Bibliothèque de stockage local Blazored.

@page "/BindModifiers"
@using Blazored.LocalStorage
@inject ILocalStorageService localStorage

<textarea @bind="bio" @bind:event="oninput" @bind:after="Sync" placeholder="Introduce yourself"></textarea>

<p>
    @bio
</p>
 @code {
 
     string bio;
 
     private async Task Sync()
     {
         await localStorage.SetItemAsStringAsync("bio", bio);
     }
     
 }

Que se passe-t-il si nous voulons modifier la valeur saisie ?

Maintenant, tout va bien si vous voulez juste prendre la valeur et en faire quelque chose, mais que se passe-t-il si vous devez également modifier cette valeur lorsque la liaison a lieu ?

Par exemple, disons que pour une raison quelconque, nous devons nous assurer que l’utilisateur n’entre pas d’adresse e-mail dans la biographie et que nous voulons bloquer le @ symbole.

Dans cet exemple (certes artificiel !), Nous voudrions ajouter une étape au processus de liaison qui supprime tout @ symboles, peut-être en les remplaçant par le mot « à » à la place.

Essayons d’abord une approche naïve dans laquelle nous utilisons Sync méthode pour changer la valeur de bio.

@code {

    string bio;

    private async Task Sync()
    {
        bio = bio.Replace("@", "at");
        await localStorage.SetItemAsStringAsync("bio", bio);
    }
    
}

Cela suppose que bio a déjà été mis à jour avec la nouvelle valeur, lit cette valeur et la met à jour avec une version « assainie » (avec les symboles « @ » remplacés par le mot « at »).

Avec cela en place, si nos utilisateurs essaient d’entrer un « @ » dans le textareail sera remplacé par « à ».

Cependant, il y a un problème avec cette approche. Si nous regardons le code généré pour notre composant, nous pouvons voir comment nous mettons essentiellement à jour la valeur de bio deux fois.

protected override void BuildRenderTree(RenderTreeBuilder __builder)
{
    
    __builder.OpenElement(0, "textarea");
    __builder.AddAttribute(2, "value", BindConverter.FormatValue(this.bio));
    __builder.AddAttribute<ChangeEventArgs>(3, "oninput", EventCallback.Factory.CreateBinder((object) this, RuntimeHelpers.CreateInferredBindSetter<string>((Func<string, Task>) (__value =>
      {
          this.bio = __value;
          return RuntimeHelpers.InvokeAsynchronousDelegate(new Func<Task>(this.Sync));
      }), this.bio), this.bio));   
    __builder.CloseElement();
}

private async Task Sync()
{
    this.bio = this.bio.Replace("@", "at");
    await this.localStorage.SetItemAsStringAsync("bio", this.bio);
}

D’abord le gestionnaire d’événements pour oninput ensembles this.bio = __value.

 __builder.AddAttribute<ChangeEventArgs>(3, "oninput", EventCallback.Factory.CreateBinder((object) this, RuntimeHelpers.CreateInferredBindSetter<string>((Func<string, Task>) (__value =>
      {
          this.bio = __value;
          return RuntimeHelpers.InvokeAsynchronousDelegate(new Func<Task>(this.Sync));
      }), this.bio), this.bio)); 

Puis, dans le Sync méthode, nous avons du code pour lire cette valeur, la modifier et la réaffecter à la bio domaine:

this.bio = this.bio.Replace("@", "at");

Enfin on lit la valeur de this.bio encore une fois afin que nous puissions le stocker dans le stockage local.

await this.localStorage.SetItemAsStringAsync("bio", this.bio);

En pratique, ce code fonctionne, mais il semble un peu redondant de lire et de modifier la valeur de notre bio champ plusieurs fois de cette façon.

Prenez le contrôle avec bind:set et bind:get

Si vous souhaitez contrôler le processus de liaison, peut-être pour modifier la valeur liée comme décrit ci-dessus, vous devrez peut-être supprimer @bind:after et utilisez quelques autres nouveaux modificateurs introduits dans .NET 7.

@bind:after est un moyen pratique et simple d’exécuter du code une fois la liaison terminée, @bind:get et @bind:set permettent un contrôle plus étroit du processus de liaison lui-même, tout en permettant d’exécuter du code asynchrone dans le cadre de ce processus.

Avec @bind:set et @bind:get nous pouvons spécifier à la fois le champ où notre valeur liée doit être stockée et la méthode qui doit gérer la mise à jour de ce champ lorsque l’utilisateur entre une valeur différente.

Voici notre exemple modifié pour utiliser @bind:set et @bind:get.

<input @bind:get="bio" @bind:set="OnInput" @bind:event="oninput" placeholder="Introduce yourself"></input>
@code {
    string bio;

    private async Task OnInput(string value)
    {
        var newValue = value.Replace("@", "at");
        bio = newValue;
        await localStorage.SetItemAsStringAsync("bio", newValue);
    }
}

Cela fonctionnera de manière très similaire à notre @bind:after exemple mais si on regarde le code généré, on voit la différence :

protected override void BuildRenderTree(RenderTreeBuilder __builder)
{
     
    __builder.OpenElement(0, "input");
    __builder.AddAttribute(1, "placeholder", "Introduce yourself");
    __builder.AddAttribute(2, "value", BindConverter.FormatValue(this.bio));
    __builder.AddAttribute<ChangeEventArgs>(3, "oninput", EventCallback.Factory.CreateBinder((object) this, RuntimeHelpers.CreateInferredBindSetter<string>(new Func<string, Task>(this.OnInput), this.bio), this.bio));
    __builder.SetUpdatesAttributeName("value");
    __builder.CloseElement();
    __builder.AddMarkupContent(4, "\r\n");
}

private async Task OnInput(string value)
{
    string newValue = value.Replace("@", "at");
    this.bio = newValue;
    await this.localStorage.SetItemAsStringAsync("bio", newValue);
    newValue = (string) null;
}

Finie la double affectation de this.bio.

Dans cette version, la zone de texte oninput événement est géré via notre OnInput méthode qui attribue de manière synchrone une nouvelle valeur à la bio champ avant d’exécuter le code asynchrone pour mettre à jour le stockage local.

L’ordre est important ici. Si nous essayons d’attribuer une valeur à bio après avoir exécuté notre code asynchrone, nous rencontrerons des problèmes :

private async Task OnInput(string value)
{
    
    await localStorage.SetItemAsStringAsync("bio", value);
    
    
    var newValue = value.Replace("@", "at");
    bio = newValue;        
}

L’interface utilisateur se comportera de manière imprévisible, les anciennes valeurs pouvant être affichées pendant l’exécution du code asynchrone (surtout si cela prend un peu de temps).

En utilisant les modificateurs get et set explicites, nous sommes responsables de l’attribution d’une nouvelle valeur à bioalors qu’avec @bind:after qui a été géré pour nous automatiquement.

Pour cette raison, si vous le pouvez, il est généralement plus sûr (et plus facile) d’utiliser bind:after qui fait cette affectation pour vous automatiquement (et au bon moment).

Mais si vous voulez plus de contrôle sur le processus de liaison, vous pouvez passer au plus spécifique @bind:set et @bind:get modificateurs à la place.

En résumé

L’exécution de tâches asynchrones après la liaison est beaucoup plus facile avec .NET 7. Vous pouvez utiliser le nouveau @bind:after modificateur pour pointer vers un gestionnaire qui sera invoqué une fois la liaison terminée.

De cette façon, vous pouvez garder votre interface utilisateur réactive tout en effectuant des tâches asynchrones en même temps.

Si vous avez besoin de mieux contrôler le processus de liaison, par exemple pour modifier la valeur entrante, vous pouvez passer au niveau inférieur @bind:get et @bind:set modificateurs à la place.




Source link