Conserver les relations entre éléments, composants et modèles dans ASP.NET Core Blazor

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 comment utiliser l’attribut de directive @key, pour la conservation des relations d’élément, de composant et de modèle, lors du rendu et lorsque les éléments ou composants changent par la suite.

Utilisation de l’attribut de directive @key

Si, après le rendu d’une liste d’éléments ou de composants, les éléments ou composants changent, Blazor doit décider lesquels des éléments ou composants précédents peuvent être conservés et déterminer le mode de mappage entre les objets de modèle et ces éléments ou composants. Normalement, ce processus est automatique et suffisant pour le rendu général, mais il existe souvent des cas où le contrôle du processus à l’aide de l’attribut @key de directive est nécessaire.

Considérez l’exemple suivant qui illustre un problème de mappage de collection résolu à l’aide de @key.

Pour les composants suivants :

  • Le composant Details reçoit du composant parent des données (Data), qui sont affichées dans un élément <input>. Tout élément <input> affiché donné peut recevoir le focus de la page de l’utilisateur lorsqu’il sélectionne l’un des éléments <input>.
  • Le composant parent crée une liste d’objets de personne à afficher avec le composant Details. Toutes les trois secondes, une nouvelle personne est ajoutée à la collection.

Cette démonstration vous permet de :

  • Sélectionner un <input> parmi plusieurs composants Details rendus.
  • Étudier le comportement du focus de la page à mesure que la collection de personnes augmente automatiquement.

Details.razor:

<input value="@Data" />

@code {
    [Parameter]
    public string? Data { get; set; }
}
<input value="@Data" />

@code {
    [Parameter]
    public string? Data { get; set; }
}
<input value="@Data" />

@code {
    [Parameter]
    public string? Data { get; set; }
}
<input value="@Data" />

@code {
    [Parameter]
    public string Data { get; set; }
}
<input value="@Data" />

@code {
    [Parameter]
    public string Data { get; set; }
}

Dans le composant parent suivant, chaque itération d’ajout d’une personne dans OnTimerCallback oblige Blazor à reconstruire l’intégralité de la collection. Le focus de la page restant sur la même position d’index des éléments <input>, le focus change chaque fois qu’une personne est ajoutée. Il n’est pas souhaitable de déplacer le focus hors de la sélection de l’utilisateur. Après avoir démontré le mauvais comportement avec le composant suivant, l’attribut de directive @key est utilisé pour améliorer l’expérience de l’utilisateur.

People.razor:

@page "/people"
@using System.Timers
@implements IDisposable

<PageTitle>People</PageTitle>

<h1>People Example</h1>

@foreach (var person in people)
{
    <Details Data="@person.Data" />
}

@code {
    private Timer timer = new Timer(3000);

    public List<Person> people =
        new()
        {
            { new Person { Data = "Person 1" } },
            { new Person { Data = "Person 2" } },
            { new Person { Data = "Person 3" } }
        };

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

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            people.Insert(0,
                new Person
                {
                    Data = $"INSERTED {DateTime.Now.ToString("hh:mm:ss tt")}"
                });
            StateHasChanged();
        });
    }

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

    public class Person
    {
        public string? Data { get; set; }
    }
}

PeopleExample.razor:

@page "/people-example"
@using System.Timers
@implements IDisposable

@foreach (var person in people)
{
    <Details Data="person.Data" />
}

@code {
    private Timer timer = new Timer(3000);

    public List<Person> people =
        new()
        {
            { new Person { Data = "Person 1" } },
            { new Person { Data = "Person 2" } },
            { new Person { Data = "Person 3" } }
        };

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

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            people.Insert(0,
                new Person
                {
                    Data = $"INSERTED {DateTime.Now.ToString("hh:mm:ss tt")}"
                });
            StateHasChanged();
        });
    }

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

    public class Person
    {
        public string? Data { get; set; }
    }
}

PeopleExample.razor:

@page "/people-example"
@using System.Timers
@implements IDisposable

@foreach (var person in people)
{
    <Details Data="person.Data" />
}

@code {
    private Timer timer = new Timer(3000);

    public List<Person> people =
        new()
        {
            { new Person { Data = "Person 1" } },
            { new Person { Data = "Person 2" } },
            { new Person { Data = "Person 3" } }
        };

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

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            people.Insert(0,
                new Person
                {
                    Data = $"INSERTED {DateTime.Now.ToString("hh:mm:ss tt")}"
                });
            StateHasChanged();
        });
    }

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

    public class Person
    {
        public string? Data { get; set; }
    }
}

PeopleExample.razor:

@page "/people-example"
@using System.Timers
@implements IDisposable

@foreach (var person in people)
{
    <Details Data="person.Data" />
}

@code {
    private Timer timer = new Timer(3000);

    public List<Person> people =
        new()
        {
            { new Person { Data = "Person 1" } },
            { new Person { Data = "Person 2" } },
            { new Person { Data = "Person 3" } }
        };

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

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            people.Insert(0,
                new Person
                {
                    Data = $"INSERTED {DateTime.Now.ToString("hh:mm:ss tt")}"
                });
            StateHasChanged();
        });
    }

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

    public class Person
    {
        public string Data { get; set; }
    }
}

PeopleExample.razor:

@page "/people-example"
@using System.Timers
@implements IDisposable

@foreach (var person in people)
{
    <Details Data="person.Data" />
}

@code {
    private Timer timer = new Timer(3000);

    public List<Person> people =
        new List<Person>()
        {
            { new Person { Data = "Person 1" } },
            { new Person { Data = "Person 2" } },
            { new Person { Data = "Person 3" } }
        };

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

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            people.Insert(0,
                new Person
                {
                    Data = $"INSERTED {DateTime.Now.ToString("hh:mm:ss tt")}"
                });
            StateHasChanged();
        });
    }

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

    public class Person
    {
        public string Data { get; set; }
    }
}

Le contenu de la collection people change quand des entrées sont insérées, supprimées ou réorganisées. La regénération du rendu peut entraîner des différences de comportement visibles. Par exemple, chaque fois qu’une personne est insérée dans la collection people, le focus de l’utilisateur est perdu.

Le processus de mappage d’éléments ou de composants à une collection peut être contrôlé avec l’attribut de directive @key. L’utilisation de @key garantit la conservation d’éléments ou de composants en fonction de la valeur de la clé. Si le composant Details dans l’exemple précédent est indexé sur l’élément person, Blazor ignore le nouveau rendu des composants Details qui n’ont pas changé.

Pour modifier le composant parent afin d’utiliser l’attribut de directive @key avec la collection people, mettez à jour l’élément <Details> comme suit :

<Details @key="person" Data="@person.Data" />

Quand la collection people change, l’association entre les instances de Details et de person est conservée. Quand Person est inséré au début de la collection, une nouvelle instance de Details est insérée à la position correspondante. Les autres instances sont inchangées. Le focus de l’utilisateur n’est donc pas perdu à mesure que des personnes sont ajoutées à la collection.

D’autres mises à jour de collection présentent le même comportement quand l’attribut de directive @key est utilisé :

  • Si une instance est supprimée de la collection, seule l’instance de composant correspondante est supprimée de l’interface utilisateur. Les autres instances sont inchangées.
  • Si les entrées de collection sont réorganisées, les instances de composant correspondantes sont conservées et réorganisées dans l’interface utilisateur.

Important

Les clés sont locales à chaque composant ou élément conteneur. Les clés ne sont pas comparées globalement dans le document.

Quand utiliser @key

En règle générale, il est judicieux d’utiliser @key chaque fois qu’une liste est rendue (par exemple, dans un bloc foreach) et qu’une valeur appropriée existe pour définir @key.

Vous pouvez également utiliser @key pour conserver une sous-arborescence d’éléments ou de composants quand un objet ne change pas, comme le montrent les exemples suivants.

Exemple 1 :

<li @key="person">
    <input value="@person.Data" />
</li>

Exemple 2 :

<div @key="person">
    @* other HTML elements *@
</div>

Si une instance de person change, la directive d’attribut @key force Blazor à :

  • Ignorer l’intégralité de <li> ou de <div> et les descendants.
  • Regénérer la sous-arborescence au sein de l’interface utilisateur avec de nouveaux éléments et composants.

Cela permet de garantir qu’aucun état de l’interface utilisateur n’est préservé quand la collection change dans une sous-arborescence.

Étendue de @key

La directive d’attribut @key est délimitée à ses propres frères au sein de son parent.

Prenons l'exemple suivant. Les clés first et second sont comparées l’une à l’autre dans la même étendue de l’élément <div> externe :

<div>
    <div @key="first">...</div>
    <div @key="second">...</div>
</div>

L’exemple suivant illustre les clés first et second dans leurs propres étendues, sans aucun rapport entre elles et sans aucune influence de l’une sur l’autre. Chaque étendue @key s’applique uniquement à son élément <div> parent, et non à travers les éléments <div> parents :

<div>
    <div @key="first">...</div>
</div>
<div>
    <div @key="second">...</div>
</div>

Pour le composant Details présenté précédemment, les exemples suivants génèrent le rendu des données person dans la même étendue @key et illustrent des cas d’usage standard pour @key :

<div>
    @foreach (var person in people)
    {
        <Details @key="person" Data="@person.Data" />
    }
</div>
@foreach (var person in people)
{
    <div @key="person">
        <Details Data="@person.Data" />
    </div>
}
<ol>
    @foreach (var person in people)
    {
        <li @key="person">
            <Details Data="@person.Data" />
        </li>
    }
</ol>

Les exemples suivants étendent uniquement @key à l’élément <div> ou <li> qui entoure chaque instance de composant Details. Les données person pour chaque membre de la collection peoplene sont donc pas indexées sur chaque instance de person à travers les composants Details rendus. Évitez les modèles suivants quand vous utilisez @key :

@foreach (var person in people)
{
    <div>
        <Details @key="person" Data="@person.Data" />
    </div>
}
<ol>
    @foreach (var person in people)
    {
        <li>
            <Details @key="person" Data="@person.Data" />
        </li>
    }
</ol>

Quand ne pas utiliser @key

Le rendu avec @key entraîne un coût en termes de performances. Ce coût en termes de performances n’est pas important, mais spécifiez uniquement @key si la conservation de l’élément ou du composant profite à l’application.

Même si @key n’est pas utilisé, Blazor conserve autant que possible les instances des composants et des éléments enfants. Le seul avantage à utiliser @key est de contrôler la façon dont les instances de modèle sont mappées aux instances de composant conservées afin d’éviter que Blazor ne sélectionne le mappage.

Valeurs à utiliser pour @key

En règle générale, il est judicieux de fournir l’une des valeurs suivantes pour @key :

  • Instances d’objet de modèle. Par exemple, l’instance Person (person) a été utilisée dans l’exemple précédent. Cela garantit une conservation basée sur l’égalité des références d’objet.
  • Identificateurs uniques. Par exemple, des identificateurs uniques peuvent être basés sur des valeurs de clé primaire de type int, string ou Guid.

Vérifiez que les valeurs utilisées pour @key ne sont pas en conflit. Si des valeurs en conflit sont détectées dans le même élément parent, Blazor lève une exception car il ne peut pas mapper de manière déterministe les anciens éléments ou composants aux nouveaux. Utilisez uniquement des valeurs distinctes, comme des instances d’objet ou des valeurs de clé primaire.