Сохранение связей элементов, компонентов и моделей в ASP.NET Core Blazor

Примечание.

Это не последняя версия этой статьи. В текущем выпуске см . версию .NET 8 этой статьи.

Предупреждение

Эта версия ASP.NET Core больше не поддерживается. Дополнительные сведения см. в статье о политике поддержки .NET и .NET Core. В текущем выпуске см . версию .NET 8 этой статьи.

Внимание

Эта информация относится к предварительному выпуску продукта, который может быть существенно изменен до его коммерческого выпуска. Майкрософт не предоставляет никаких гарантий, явных или подразумеваемых, относительно приведенных здесь сведений.

В текущем выпуске см . версию .NET 8 этой статьи.

В этой статье объясняется, как использовать @key атрибут директивы для сохранения связей элементов, компонентов и моделей при отрисовке и последующих изменениях элементов или компонентов.

Использование атрибута @key директивы

При отрисовке списка элементов или компонентов и элементов, которые впоследствии изменяются, необходимо решить, Blazor какие из предыдущих элементов или компонентов сохраняются и как объекты модели должны сопоставляться с ними. Как правило, этот процесс является автоматическим и достаточным для общей отрисовки, но часто возникают случаи, когда требуется управление процессом с помощью атрибута @key директивы.

Рассмотрим следующий пример, демонстрирующий проблему сопоставления коллекций, которая решена с помощью @key.

Для следующих компонентов:

  • Компонент Details получает данные (Data) от родительского компонента, который отображается в элементе <input> . Пользователь может установить фокус на любой заданный отображаемый элемент <input> страницы при выборе одного из элементов <input>.
  • Родительский компонент создает список объектов person для отображения с помощью Details компонента. Каждые три секунды в коллекцию добавляется новый пользователь.

В этой демонстрации можно выполнить следующие действия:

  • Выбрать <input> из нескольких отрисованных компонентов Details.
  • Изучить поведение фокуса страницы по мере автоматического роста коллекции пользователей.

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

В следующем родительском компоненте каждая итерация добавления пользователя OnTimerCallback приводит к Blazor перестроению всей коллекции. Фокус страницы остается на одном и том же положении указателя элементов <input>, поэтому при каждом добавлении пользователя фокус сдвигается. Сдвиг фокуса с выбранного пользователем элемента нежелателен. После демонстрации нежелательного поведения с помощью следующего компонента атрибут директивы @key используется для повышения удобства работы пользователя.

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

Содержимое коллекции people изменяется при вставке, удалении или повторном упорядочении записей. Повторная отрисовка может привести к появлению видимых различий в поведении. Например, каждый раз, когда пользователь вставляется в people коллекцию, фокус пользователя теряется.

Процесс сопоставления элементов или компонентов с коллекцией можно контролировать с помощью атрибута @key директивы. Использование @key гарантирует сохранение элементов или компонентов на основе значения ключа. Если фокус компонента Details в предыдущем примере установлен на элемент person, Blazor игнорирует повторную отрисовку компонентов Details, которые не изменились.

Чтобы изменить родительский компонент, чтобы использовать @key атрибут директивы с people коллекцией, обновите <Details> элемент следующим образом:

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

При изменении коллекции people связь между экземплярами Details и экземплярами person сохраняется. При вставке Person в начало коллекции один новый экземпляр Details вставляется на соответствующую позицию. Другие экземпляры остаются без изменений. Таким образом, по мере добавления пользователей в коллекцию установленный пользователем фокус не теряется.

Другие обновления коллекции при использовании атрибута @key директивы ведут себя точно так же:

  • Если экземпляр удаляется из коллекции, то из пользовательского интерфейса удаляется только соответствующий экземпляр компонента. Другие экземпляры остаются без изменений.
  • При переупорядочении записей коллекции соответствующие экземпляры компонентов сохраняются и переупорядочиваются в пользовательском интерфейсе.

Внимание

Ключи являются локальными для каждого компонента или элемента контейнера. Ключи не сравниваются глобально по всему документу.

Когда следует использовать @key

Как правило, @key имеет смысл использовать при отрисовке списка (например, в блоке foreach) и при наличии подходящего значения для определения @key.

Если объект не изменяется, @key можно также использовать для сохранения поддерева элемента или компонента, как показано в следующих примерах.

Пример 1:

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

Пример 2:

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

При изменении экземпляра person атрибут @key директивы заставляет Blazor выполнить следующие действия:

  • Полностью отменить <li> или <div>, а также их потомков.
  • Перестроить поддерево в пользовательском интерфейсе с помощью новых элементов и компонентов.

Это позволяет гарантировать сохранение состояния пользовательского интерфейса при изменении коллекции в поддереве.

Область действия @key

Областью действия директивы атрибута @key являются другие дочерние элементы в ее родителе.

Рассмотрим следующий пример. Ключи first и second сравниваются друг с другом в общей области внешнего элемента <div>:

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

В следующем примере ключи first и second находятся каждый в своей области, которые не связаны и никак не влияют друг на друга. Область каждого @key относится только к его собственному родительскому элементу <div>, а не к общим родительским элементам <div>:

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

В следующих примерах для упомянутого ранее компонента Details выводятся данные person в области @key, а также показаны основные варианты использования @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>

В следующих примерах областью действия @key является только элемент <div> или <li>, охватывающий соответствующий экземпляр компонента Details. В этом случае данные person для каждого элемента коллекции people не соответствуют по ключу экземплярам person в выводимых компонентах Details. Избегайте при использовании @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>

Когда не следует использовать @key

Отрисовка с использованием @key подразумевает определенное снижение производительности. Это снижение производительности незначительно, но указывать @key следует только в том случае, если сохранение элементов или компонентов выгодно для приложения.

Даже если @key не используется, Blazor сохраняет экземпляры дочерних элементов и компонентов в максимально возможной степени. Единственным преимуществом использования @key является контроль над тем, как экземпляры модели сопоставляются с сохраненными экземплярами компонентов, вместо выбора сопоставления с помощью Blazor.

Используемые значения для @key

Как правило, для @key имеет смысл указать одно из следующих значений:

  • Экземпляры объектов моделей. Например, в предыдущем примере использовался экземпляр Person (person). Это гарантирует сохранение на основе равенства ссылок на объекты.
  • Уникальные идентификаторы. Например, уникальные идентификаторы могут основываться на значениях первичного ключа типа int, string или Guid.

Убедитесь, что значения, используемые для @key, не конфликтуют. Если в одном родительском элементе обнаруживаются конфликтующие значения, Blazor выдает исключение, поскольку не может детерминированно сопоставлять старые элементы или компоненты с новыми. Используйте только уникальные значения, такие как экземпляры объекта или значения первичного ключа.