制御構文
条件分岐とリストには、専用の構文があります。テンプレートのどの位置にも、ジェネレーターがコンパイル時にシーケンス番号を振れるようにするためです。
If#
If は、条件と内容のサンクを取ります。else の分岐は省けます。1回の描画は1つの状態なので、下の出力は _items.Length == 0 が選ばなかった側です。
protected override View Body => Div[ If(_items.Length == 0, () => P["Nothing here yet."], () => Span[$"{_items.Length} items"])];
<div><span>2 items</span></div>
互いに排他な分岐には、重ならないシーケンス番号の範囲を割り当てます。そのため分岐が切り替わっても、周りにある兄弟の位置は動きません。
ForEach とそのキー#
ForEach は、位置ではなく要素そのものを見分けるキーを取ります。キーはマークアップに出力されません。属性ではなく、差分計算への指示だからです。
protected override View Body => Ul[ ForEach(_items, key: item => item.Id, content: item => Li[item.Name])];
<ul> <li>Alpha</li> <li>Beta</li> </ul>
シーケンス番号はテンプレートの位置を、キーはデータの実体を見分けます。インデックスをキーに渡すと、差分の計算は意味を失います。並び替えたときに、Blazor が別の要素の状態を使い回すからです。
自分の要素をまったく参照しないキーは報告します。次の3つは、どれも BCF3002 です。
key: _ => 0- ラムダの外にあるカウンターから読んだキー
- 入れ子のループで、外側の要素しか参照していない内側のキー
ForEach(_groups, key: g => g.Id, content: g =>
Div[ForEach(g.Items, key: i => g.Id, content: i => Span[i.Name])]) // 内側のキーで BCF3002
これはエラーではなく警告で、コンポーネントの出力は止めません。リストの表示そのものは正しく、差分の計算だけが非効率になるからです。検査もあえて控えめです。見るのは要素を参照したかどうかだけで、その値が要素を見分けられるかは検査しません。要素から作ってはいても実際には位置と変わらないキーは、この検査を通ります。BCF3002 は保証ではなく下限です。
どちらのラムダも、その場に書いた式のラムダである必要があります。メソッドをそのまま渡さず、呼び出しで包んでください。Row ではなく item => Row(item) と書きます(BCF3004)。内容のルートは単一の要素かコンポーネントである必要があり(BCF3003)、そのルートが自分でキーを書くこともできません(BCF3032)。
キーを使わない#
キーの引数に既定値はありません。見分ける材料を持たないリストは、key: null と明示します。
Ul[ForEach(_columns, key: null, content: c => Li[c.Header])]
固定のメニュー、決まった列の集合、並び替わらない Select の結果には、この書き方が適します。代償は、BCF3002 が警告しているものと同じです。差分の計算はインデックスをキーにしたときと同じになり、先頭に1件挿入すると全行が書き直され、行ごとの状態が失われます。SetKey を出さないので、BCF3002 は検査する対象を持たず、BCF3003 も適用されません。内容のルートには、
Fragment、Raw、単独の If のいずれも置けます。
Select の結果を子に展開する#
子のリストには、通常の射影、つまり Select の結果をスプレッドで展開する書き方もあります。
Ul[[.. _columns.Select(c => Li[c.Header])]]
上のキーを使わない ForEach の、2つ目の書き方です。同じ foreach になります。展開した項目はその位置に並ぶので、手で書いた子と混ぜられます。
Ul[[Li["先頭"], .. _columns.Select(c => Li[c.Header]), Li["末尾"]]]
畳めるのは <source>.Select(<その場に書いた式のラムダ>) と、イテレータ [ViewPart] の呼び出し(下の[ViewPart] でイテレートする)です。それ以外のスプレッド、たとえば保存した View の配列や、それを返すメソッドは、静的に順序付けできる子ではないので
BCF1003 を報告します。保存した View を子として1つ書いたときと同じ結果です。
[ViewPart] でイテレートする#
[ViewPart] はイテレータにもできます。IEnumerable<View> を返す static メソッドで、本体の末尾で C# 本来の foreach が繰り返しごとに1つ yield return します。
[ViewPart] private static IEnumerable<View> Rows(IReadOnlyList<Item> items) { foreach (var item in items) { yield return Li.Key(item.Id)[item.Name]; } }
呼び出しは他のスプレッドと同じ形です。
Ul[[.. Rows(_items)]]
これは通常の [ViewPart] 呼び出しとは違う経路です。通常の呼び出しは呼び出し箇所ごとに本体を展開しますが、繰り返す回数は実行時にしか分かりません。そのためスプライスしたイテレータ部品は
ForEach 自身の出力をそのまま使い回します。静的なシーケンス空間を1つだけ持ち、それを繰り返しのたびに使い回すのは、上のキーを使わないスプレッドと同じで、呼び出しごとに本体をコピーするわけではありません。
.Key(...) は省略でき、書く場合は yield した要素自身の装飾として書きます。要素自身のキーであり、
ForEach の key: 引数のように別の引数へは渡しません。C# 本来の foreach のヘッダーには、それを渡す引数の場所がないからです。省略すると SetKey は出ません。ForEach でキーを使わないときと同じです。
ここで受け付けるのは yield return だけで、それも [ViewPart] でだけです。foreach を return
で終える書き方では、最初の1件で抜けてしまい、全件を作ることになりません。C# が yield return を許すのは本物のイテレータの中だけで、それになれるのはメソッドだけです。プロパティのゲッターやラムダはなれません。そのためこの形は foreach/if/switch を受け付けるどの位置にも通らず、
[ViewPart] のこの位置だけで受け付けます(BCF1002)。
[ViewPart] は他と同じく static である必要があります。本体からインスタンスフィールドを直接読むことはできないので、ループの元になる値は常に引数として渡します(上の items)。他の [ViewPart]
の引数と同じです。
Fragment#
Fragment は、ラッパーの要素を出さずに、複数の子を1つの View にまとめます。
Fragment(H2["Title"], P["Body"])
要素を開かないので、装飾は付けられず(BCF3008)、ForEach の内容のルートにもできません(BCF3003)。
Raw#
Raw は、HTML の文字列をそのまま流し込みます。MarkupString にあたるものです。
Raw はエスケープせずに DOM へ書き込みます。渡してよいのは、自分で作った内容だけです。利用者が入力した文字列や、外部のサービスから返ってきた文字列を渡すと、そのまま HTML として解釈され、含まれていたスクリプトが動きます。
このページも Raw で表示しています。Markdown をビルド時に HTML へ変換し、その結果を Raw に渡しています。変換するのは、このリポジトリにあるツールです。
Article.Class("prose")[Raw(entry.Html)]
次に読むもの#
- 書ける要素の一覧は要素と装飾へ。
- コンポーネントから別のコンポーネントを呼ぶ方法はコンポーネントと再利用へ。