マルチバインディングを使ったデータ仮想化リスト上のアイテムに対するページ内検索＆ハイライト表示の実装

## 前提

UWPとWinUI3におけるXamlが対象です。 WPFや.Net MauiのXamlは取り扱いません。

## やりたいこと

別々のクラスに存在する文字列プロパティの変更通知に反応して、ページ内検索の結果となるテキストのハイライト表示のOn/Offを切り替えたい。

変更通知を受け取りたいプロパティは、この記事では以下の２つとします。

* ページ内検索用のクエリ文字列
* 検索対象文字列（ ObservableCollection< ItemVM > 内の ItemVM.Text プロパティ）

２つの文字列から検索結果を得るメソッド（自作の `bool IsHitSearch(string)` など）を実行し、検索結果を表示に反映するまでの流れを考えていきます。


### 制約その１：２つの文字列に依存する

検索を処理する際の「検索キーワード」と「検索される側の文字列」の２つが登場します。

バインディングする際に両方の文字列に対する変更通知を受け取ってハイライト表示等に反映されないといけません。

WPFにあった MultiBinding はUWPやWinUI3には無いため、x:Bind のメソッド実行と複数引数対応を使うとより簡単だと思います。（具体的なコードは後述）

### 制約その２：大量の表示対象データがある場合（及びデータ仮想化）

大量の表示対象データがある場合にはデータ仮想化に対応したコントロールを使わないとパフォーマンスが得られません。

https://learn.microsoft.com/ja-jp/windows/uwp/debug-test-perf/listview-and-gridview-data-optimization


### 案１：ItemVM全てをforループで評価する

件数が500以下（スペック次第で数字は上下する）で済む場合はこれでもいいかもしれません。

ItemVMにIsHighlightプロパティを追加して、検索評価時に全ItemVMに対するIsHighlightを設定します。

検索結果の評価実行のコストは掛かりますが、仮想化データを利用していれば表示更新のコストはリアライズしたアイテムのみに限定されるため実用範囲かもしれません。

### 案２：x:Bind を使って表示アイテムのみを評価する

x:Bind は x:DataTypeで指定した型が持つメソッドを呼び出せます。メソッド引数には、x:Bind としてバインド可能な変数を与えられますし、なおかつ引数それぞれの変更通知を受け取ってメソッドが再実行されます。この動作を利用して２つの文字列に対する変更通知を受け取る動作を実現していきます。

https://learn.microsoft.com/ja-jp/windows/apps/develop/data-binding/function-bindings

> モードが OneWay/TwoWay である場合、関数のパスでは変更の検出が実行され、それらのオブジェクトに変更があるとバインディングが再評価されます。
> 
> バインディングされる関数には以下のことが要求されます。
> 
> * コードとメタデータにアクセスできること。そのため、C# では internal/private が機能しますが、C++ ではメソッドをパブリック WinRT メソッドにする必要があります
> * オーバーロードは引数の型ではなく数に基づき、同じ数の引数を持つ最初のオーバーロードとの一致が試みられます
> * 引数の型は渡されるデータと一致する必要があります。縮小変換は行われません。
> * 関数の戻り値の型は、バインディングを使用しているプロパティの型と一致する必要があります。
> 
> バインディング エンジンでは、関数名で起動されたプロパティ変更通知に反応し、必要に応じてバインディングが再評価されます。

（下記コードには CommunityToolkit.MVVM のnugetパッケージのインストールが必要です）

```xml
<DataTemplate x:Key="MyItemDataTemplate" x:DataType="viewModels:ItemVM">
    <UserControl>
        <Grid>        
            <VisualStateManager.VisualStateGroups>
                <VisualStateGroup>
                    <VisualState>
                        <VisualState.StateTriggers>
                            <StateTrigger IsActive="{x:Bind IsHighlight(Text, SearchContext.Text), Mode=OneWay" />
                        </VisualState.StateTriggers>        
                        <VisualState.Setters>
                            <Setter Target="MyTextBox.BorderBrush" Value="{StaticResource SystemColorControlAccentBrush}" />
                        </VisualState.Setters>
                    </VisualState>                
                </VisualStateGroup>            
            </VisualStateManager.VisualStateGroups>

            <TextBox Text="{x:Bind Text, Mode=TwoWay}"
                x:Name="MyTextBox"
                />
        </Grid>
    </UserControl>
</DataTemplate>
```

```csharp
public sealed partial class ItemVM : ObservableObject
{
    public SearchContext SearchContext { get; }
    
    public ItemVM(SearchContext searchContext)
    {
        SearchContext = searchContext;
    }

    // Note: 第２引数の searchText は使っていませんが、x:Bind経由で変更通知を受け取るために必要
    public bool IsHighlight(string text, string searchText)
    {                
        return SearchContext.IsHitSearch(text);
    }

    [ObservableProperty]
    private string _text = "";
}

public sealed partial class SearchContext : ObservableObject
{
    [ObservableProperty]
    private string _text = "";

    public bool IsHitSearch(string text)
    {
        // TODO: 検索評価の実装
        throw new NotImplementedException();
    }
}
```

別途ページ側に 

* ListViewなどに ItemVM のコレクションを与えてアイテムをリスト表示するUIを追加する
* TextBox を配置し、 TextBox.Text に SearchContext.Text プロパティをバインディングする
 
とすることで完成します。

１ページ内の文字列の量にもよりますが、検索文字列が一文字変わるごとに再評価しても実用に耐えるだろうと思います。