在 ASP.NET Core Blazor 中保留元素、组件和模型关系
注意
此版本不是本文的最新版本。 有关当前版本,请参阅本文的 .NET 9 版本。
警告
此版本的 ASP.NET Core 不再受支持。 有关详细信息,请参阅 .NET 和 .NET Core 支持策略。 有关当前版本,请参阅本文的 .NET 9 版本。
本文介绍了如何使用 @key
指令属性在呈现以及元素或组件相继更改时,保留元素、组件和模型的关系。
@key
指令属性的名称
在呈现元素或组件的列表并且元素或组件随后发生更改时,Blazor 必须决定保留哪些元素或组件,以及模型对象应如何映射到这些元素或组件。 通常,此过程是自动完成的,并且足以用于常规呈现,但在某些情况下,需要使用 @key
指令属性来控制进程。
请考虑以下示例,该示例演示了通过使用 @key
解决的集合映射问题。
对于下列组件:
Details
组件从父组件接收数据 (Data
),它显示在<input>
元素中。 当用户选择一个<input>
元素时,显示的任何给定<input>
元素都可以从用户接收页面焦点。- 父组件使用
Details
组件来创建人员对象列表以进行显示。 每三秒向集合中添加一个新人员。
此演示使你可以:
- 从多个呈现的
Details
组件中选择一个<input>
。 - 随着人员集合自动增长,研究页面焦点的行为。
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
指令特性时,其他集合更新会表现出相同的行为:
- 如果从集合中删除实例,则仅从 UI 中删除相应的组件实例。 其他实例保持不变。
- 如果对集合项进行重新排序,则保留相应的组件实例,并在 UI 中重新排序。
重要
对于每个容器元素或组件而言,键是本地的。 不会在整个文档中全局地比较键。
何时使用 @key
通常,每当呈现列表(例如,在 foreach
块中)以及存在适当的值定义 @key
时,都可以使用 @key
。
在对象未更改时,也可以使用 @key
保留元素或组件子树,如下面的示例所示。
示例 1:
<li @key="person">
<input value="@person.Data" />
</li>
示例 2:
<div @key="person">
@* other HTML elements *@
</div>
如果 person
实例更改,则 @key
特性指令会强制 Blazor:
- 放弃整个
<li>
或<div>
及其后代。 - 在 UI 中使用新元素和组件重新生成子树。
这有助于保证在子树中的集合发生更改时不保留 UI 状态。
@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
组件,以下示例显示位于同一 @key
范围的 person
数据,并演示 @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
的范围仅设为围绕每个 Details
组件实例的 <div>
或 <li>
元素。 因此,people
集合的每个成员的 person
数据的键不指向所呈现的 Details
组件中的每个 person
实例。 使用 @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 引发异常,因为它无法明确地将旧元素或组件映射到新元素或组件。 仅使用非重复值,例如对象实例或主键值。