Beibehalten von Element-, Komponenten- und Modellbeziehungen in ASP.NET Core Blazor

Hinweis

Dies ist nicht die neueste Version dieses Artikels. Die aktuelle Version finden Sie in der .NET 9-Version dieses Artikels.

Warnung

Diese Version von ASP.NET Core wird nicht mehr unterstützt. Weitere Informationen finden Sie in der Supportrichtlinie für .NET und .NET Core. Informationen zum aktuellen Release finden Sie in der .NET 8-Version dieses Artikels.

Wichtig

Diese Informationen beziehen sich auf ein Vorabversionsprodukt, das vor der kommerziellen Freigabe möglicherweise noch wesentlichen Änderungen unterliegt. Microsoft gibt keine Garantie, weder ausdrücklich noch impliziert, hinsichtlich der hier bereitgestellten Informationen.

Die aktuelle Version finden Sie in der .NET 9-Version dieses Artikels.

In diesem Artikel wird erläutert, wie Sie das @key-Anweisungsattribut verwenden, um Element-, Komponenten- und Modellbeziehungen beim Rendern beizubehalten, wenn sich die Elemente oder Komponenten anschließend ändern.

Verwenden des @key-Anweisungsattributs

Wenn Sie eine Element- oder Komponentenliste rendern und die Elemente oder Komponenten nachfolgend geändert werden, muss Blazor bestimmen, welche der vorherigen Elemente oder Komponenten beibehalten werden und wie Modellobjekte diesen zugeordnet werden sollen. Normalerweise ist dieser Prozess automatisch und für das allgemeine Rendering ausreichend, aber es gibt häufig Fälle, in denen die Steuerung des Prozesses mithilfe des Anweisungsattributs @key erforderlich ist.

Sehen Sie sich das folgende Beispiel an, das ein Problem bei einer Sammlungszuordnung veranschaulicht, das mithilfe von @keygelöst wird.

Für die folgenden Komponenten:

  • Die Details-Komponente empfängt Daten (Data) von der übergeordneten Komponente, die in einem <input>-Element angezeigt wird. Jedes angezeigte <input>-Element kann den Fokus der Seite vom Benutzer erhalten, wenn er eines der <input>-Elemente auswählt.
  • Die übergeordnete Komponente erstellt mithilfe der Details-Komponente eine Liste anzuzeigender person-Objekte. Alle drei Sekunden wird der Sammlung eine neue Person hinzugefügt.

Diese Demonstration ermöglicht Ihnen Folgendes:

  • Auswählen eines <input> aus mehreren gerenderten Details-Komponenten
  • Untersuchen des Verhaltens des Fokus der Seite, wenn die Größe der people-Sammlung automatisch zunimmt

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; }
}
<input value="@Data" />

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

In der folgenden übergeordneten Komponente führt jede Iteration für das Hinzufügen einer Person in OnTimerCallback dazu, dass Blazor die gesamte Collection neu erstellt. Der Fokus der Seite bleibt auf derselben Indexposition der <input>-Elemente, sodass sich der Fokus jedes Mal verschiebt, wenn eine Person hinzugefügt wird. Das Abweichen des Fokus von der Auswahl durch den Benutzer ist kein wünschenswertes Verhalten. Nachdem mit der folgenden Komponente das unzureichende Verhalten veranschaulicht wurde, wird das @key-Anweisungsattribut verwendet, um die Benutzerfreundlichkeit zu verbessern.

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

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

Der Inhalt der people-Sammlung ändert sich durch eingefügte, gelöschte oder neu sortierte Einträge. Erneutes Rendering kann zu sichtbaren Verhaltensunterschieden führen. Beispielsweise geht bei jedem Einfügen einer Person in die Sammlung people der Bnutzerfokus verloren.

Das Zuordnen von Elementen oder Komponenten zu einer Sammlung kann mit dem @key-Anweisungsattribut gesteuert werden. Die Verwendung von @key garantiert, dass Elemente oder Komponenten basierend auf dem Wert von @key beibehalten werden. Wenn in der Details-Komponente im vorherigen Beispiel @key auf person festgelegt ist, erfolgt in Blazor kein erneutes Rendering von Details-Komponenten, die sich nicht geändert haben.

Um für die übergeordnete Komponente das @key-Anweisungsattribut mit der people-Collection zu verwenden, aktualisieren Sie das <Details>-Element wie folgt:

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

Wenn sich die people-Sammlung ändert, bleibt die Zuordnung zwischen Details-Instanzen und person-Instanzen erhalten. Wenn am Anfang der Sammlung eine Person eingefügt wird, wird an der entsprechenden Position eine neue Details-Instanz eingefügt. Andere Instanzen bleiben unverändert. Daher geht der Fokus des Benutzers nicht verloren, wenn der Sammlung Personen hinzugefügt werden.

Andere Sammlungsaktualisierungen weisen das gleiche Verhalten auf, wenn das @key-Anweisungsattribut verwendet wird:

  • Wenn eine Instanz aus der Sammlung gelöscht wird, wird nur die entsprechende Komponenteninstanz von der Benutzeroberfläche entfernt. Andere Instanzen bleiben unverändert.
  • Wenn Sammmlungseinträge neu sortiert werden, werden die entsprechenden Komponenteninstanzen beibehalten und auf der Benutzeroberfläche neu angeordnet.

Wichtig

Schlüssel sind für jedes Containerelement oder jede Komponente lokal. Schlüssel werden nicht dokumentübergreifend global verglichen.

Empfohlene Verwendung von @key

In der Regel ist es sinnvoll, @key zu verwenden, wenn eine Liste gerendert wird (z. B. in einem foreach-Block) und ein geeigneter Wert vorhanden ist, um @key zu definieren.

Sie können @key auch verwenden, um eine Element- oder Komponentenunterstruktur zu bewahren, wenn sich ein Objekt nicht ändert, wie in den folgenden Beispielen gezeigt.

Beispiel 1:

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

Beispiel 2:

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

Wenn sich eine person-Instanz ändert, erzwingt das @key-Anweisungsattribut in Blazor Folgendes:

  • Verwerfen des gesamten <li>- oder <div>-Elements und seiner Nachfolger
  • Neuerstellen der Unterstruktur innerhalb der Benutzeroberfläche mit neuen Elementen und Komponenten

Dies ist hilfreich, um zu gewährleisten, dass kein Benutzeroberflächenzustand beibehalten wird, wenn sich die Sammlung innerhalb einer Unterstruktur ändert.

Bereich von @key

Die @key-Attributanweisung ist auf ihre eigenen gleichgeordneten Elemente innerhalb des übergeordneten Elements begrenzt.

Betrachten Sie das folgende Beispiel. Die Schlüssel first und second werden im selben Bereich des <div>-Elements miteinander verglichen:

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

Im folgenden Beispiel werden die Schlüssel first und second in ihren eigenen Bereichen veranschaulicht, die unabhängig voneinander stehen und keinen Einfluss aufeinander haben. Jeder @key-Bereich gilt nur für sein übergeordnetes <div>-Element, nicht für die übergeordneten <div>-Elemente:

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

Für die zuvor gezeigte Details-Komponente rendern die folgenden Beispiele person-Daten innerhalb desselben @key-Bereichs und veranschaulichen typische Anwendungsfälle für @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>

Die folgenden Beispiele beschränken sich nur auf den Bereich @key für das <div>- oder <li>-Element, das jede Details-Komponenteninstanz umschließt. Daher werden person-Daten für jedes Member der people-Sammlung nicht für jede person-Instanz für die gerenderten Details-Komponenten schlüsselgebunden. Vermeiden Sie die folgenden Muster bei Verwendung von @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>

Ungeeignete Fälle für @key

Beim Rendern mit @key wird die Leistung beeinträchtigt. Die Beeinträchtigung der Leistung ist nicht erheblich. Sie sollten @key jedoch nur angeben, wenn sich die Beibehaltung des Elements oder der Komponente positiv auf die App auswirken.

Auch wenn @key nicht verwendet wird, behält Blazor die untergeordneten Element- und Komponenteninstanzen so weit wie möglich bei. Der einzige Vorteil bei der Verwendung von @key besteht in der Kontrolle darüber, wie Modellinstanzen den beibehaltenen Komponenteninstanzen zugeordnet werden, anstatt die Zuordnung durch Blazor bestimmen zu lassen.

Zu verwendende Werte für @key

Im Allgemeinen ist es sinnvoll, für @key folgende Arten von Werten bereitzustellen:

  • Modellobjektinstanzen. Beispielsweise wurde im vorherigen Beispiel die Person-Instanz (person) verwendet. Dadurch wird die Beibehaltung basierend auf der Objektverweisgleichheit sichergestellt.
  • Eindeutige Bezeichner. Eindeutige Bezeichner können beispielsweise auf Primärschlüsselwerten vom Typ int, string oder Guid basieren.

Stellen Sie sicher, dass die für @key verwendeten Werte nicht kollidieren. Wenn innerhalb desselben übergeordneten Elements kollidierende Werte erkannt werden, löst Blazor eine Ausnahme aus, da alte Elemente oder Komponenten nicht deterministisch neuen Elementen oder Komponenten zugeordnet werden können. Verwenden Sie nur eindeutige Werte wie Objektinstanzen oder Primärschlüsselwerte.