Сохранение связей элементов, компонентов и моделей в ASP.NET Core Blazor
Примечание.
Это не последняя версия этой статьи. В текущем выпуске см . версию .NET 9 этой статьи.
Предупреждение
Эта версия ASP.NET Core больше не поддерживается. Дополнительные сведения см. в статье о политике поддержки .NET и .NET Core. В текущем выпуске см . версию .NET 8 этой статьи.
Внимание
Эта информация относится к предварительному выпуску продукта, который может быть существенно изменен до его коммерческого выпуска. Майкрософт не предоставляет никаких гарантий, явных или подразумеваемых, относительно приведенных здесь сведений.
В текущем выпуске см . версию .NET 9 этой статьи.
В этой статье объясняется, как использовать @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 выдает исключение, поскольку не может детерминированно сопоставлять старые элементы или компоненты с новыми. Используйте только уникальные значения, такие как экземпляры объекта или значения первичного ключа.
ASP.NET Core