ASP.NET Core Blazor で要素、コンポーネント、モデルのリレーションシップを保持する
Note
これは、この記事の最新バージョンではありません。 現在のリリースについては、この記事の .NET 9 バージョンを参照してください。
警告
このバージョンの ASP.NET Core はサポート対象から除外されました。 詳細については、「.NET および .NET Core サポート ポリシー」を参照してください。 現在のリリースについては、この記事の .NET 8 バージョンを参照してください。
重要
この情報はリリース前の製品に関する事項であり、正式版がリリースされるまでに大幅に変更される可能性があります。 Microsoft はここに示されている情報について、明示か黙示かを問わず、一切保証しません。
現在のリリースについては、この記事の .NET 9 バージョンを参照してください。
この記事では、@key
ディレクティブ属性を使って、レンダリング時とその後に要素やコンポーネントが変更された場合でも、要素、コンポーネント、モデルのリレーションシップを保持する方法について説明します。
@key
ディレクティブ属性の使用
要素またはコンポーネントのリストをレンダリングし、その後に要素またはコンポーネントが変更された場合、Blazor では、前のどの要素やコンポーネントを保持できるか、およびモデル オブジェクトをそれらにどのようにマップするかを決定する必要があります。 通常、このプロセスは自動であり、一般的なレンダリングには十分ですが、@key
ディレクティブ属性を使ってプロセスを制御することが必要な場合もよくあります。
@key
を使って解決できるコレクション マッピングの問題を示す次の例を考えてみましょう。
次のコンポーネントについて考えてみます。
Details
コンポーネントは、<input>
要素に表示される親コンポーネントからデータ (Data
) を受け取ります。 表示される任意の<input>
要素では、<input>
要素のいずれかを選択すると、ユーザーからページのフォーカスを受け取ることができます。- 親コンポーネントは、
Details
コンポーネントを使用して表示する person オブジェクトの一覧を作成します。 3 秒ごとに、新しい個人がコレクションに追加されます。
このデモで次のことを行うことができます。
- レンダリングされた複数の
Details
コンポーネントの中から<input>
を選択する。 - people コレクションの自動拡大時のページのフォーカスの動作を調べる。
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; }
}
次の親コンポーネントでは、person の 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
コンポーネントのレンダリングが無視されます。
people
コレクションを持つ @key
ディレクティブ属性を使用するように親コンポーネントを変更するには、<Details>
要素を次のように更新します。
<Details @key="person" Data="@person.Data" />
people
コレクションが変更されても、Details
インスタンスと person
インスタンス間の関連付けは保持されます。 コレクションの先頭に Person
が挿入されると、その対応する位置に 1 つの新しい 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 では、古い要素やコンポーネントを新しい要素やコンポーネントに確定的にマップできないため、例外がスローされます。 個別の値 (オブジェクト インスタンスや主キー値など) のみを使用してください。
ASP.NET Core