ASP.NET Core Blazor で要素、コンポーネント、モデルのリレーションシップを保持する

Note

これは、この記事の最新バージョンではありません。 現在のリリースについては、この記事の .NET 8 バージョンを参照してください。

警告

このバージョンの ASP.NET Core はサポート対象から除外されました。 詳細については、「.NET および .NET Core サポート ポリシー」を参照してください。 現在のリリースについては、この記事の .NET 8 バージョンを参照してください。

重要

この情報はリリース前の製品に関する事項であり、正式版がリリースされるまでに大幅に変更される可能性があります。 Microsoft はここに示されている情報について、明示か黙示かを問わず、一切保証しません。

現在のリリースについては、この記事の .NET 8 バージョンを参照してください。

この記事では、@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>

次の例では、互いに関係がなく、互いに影響を与えることのない、独自のスコープの firstsecond キーを示しています。 各 @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) が使用されていました。 これにより、オブジェクト参照の等価性に基づいて保持されます。
  • 一意識別子。 たとえば、一意識別子は intstring、または Guid 型の主キー値を基にすることができます。

@key に使用される値は競合しないようにしてください。 同じ親要素内で競合する値が検出された場合、Blazor では、古い要素やコンポーネントを新しい要素やコンポーネントに確定的にマップできないため、例外がスローされます。 個別の値 (オブジェクト インスタンスや主キー値など) のみを使用してください。