[WPF] ListBoxで選択可能な数に上限を設定する

WPFの`ListBox`コントロールには`SelectionMode`プロパティがあり、`Multiple`を指定すると複数項目の選択が可能です。
しかし、「最大で５件まで」のような制御をXAMLから設定することはできません。本稿では、ViewModel側で最大選択可能数の制御を行う方法について紹介したいと思います。

# 事前準備

* `ListBox`にバインドするデータに、`bool`型の`IsSelected`プロパティを生やします。このプロパティは変更時に`PropertyChanged`が起きるようにしてください。
* 生やした`IsSelected`プロパティを`ListBox`にバインドし、`SelectedItemsChanged`イベントが起きるたびに選んだ項目の`IsSelected`が変更されるようにします。具体的には、以下のようにXAMLを設定します。

```xml:View.xaml
<ListBox
    ItemsSource="{Binding Items.Value}"
    SelectionMode="Multiple"
    <ItemsControl.ItemContainerStyle>
        <Style TargetType="{x:Type ListBoxItem}">
            <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
        </Style>
    </ItemsControl.ItemContainerStyle>
</ListBox>
```

また、ViewModelも先にバインドするデータソースを定義しておきます。モデル部分は本質ではないので省略してますが、`IModel`を実装するクラスが`Items`プロパティで画面表示するデータの元を持っているとします。

```c#:ViewModel.cs
// バインドするデータ -> Modelから取得する
public class ItemData : BindableBase
{
    public bool IsSelected { get => _isSelected; set => SetProperty(ref _isSelected; value); }
    private bool _isSelected;
}

public class ViewModel
{
    public ReadOnlyReactiveCollection<ItemData> Items { get; }
    private readonly IModel _model;
    public ViewModel(IModel model)
    {
        _model = model;
        // もしModelがObservableCollection<T>で持つなら
        Items = _model.Items.ToReadOnlyReactiveCollection(/* 変換処理 */);
        // もしModelがIEnumerable<T>で持つなら
        Items = _model.ObserveProperty(m => m.Items).
            SelectMany(item => /* 変換処理 */).
            ToReadOnlyReactiveCollection();
    }
}
```

上記ではModelが**IEnumerable<T>でデータを持っている場合**と**ObservableCollection<T>でデータを持っている場合**の２通りで`ReadOnlyReactiveCollection<ItemData>`の変換処理を書いています。`ReactiveCollection`を使う理由は**コレクションの中身の`IsSelected`の変更状態を監視する必要がある**ためです。

# 上限の実装方法
「選択可能な数に上限を設定する」を実現するには、以下の方法が思いつきます。

1. 現在の選択数が上限数に達したら、未選択項目は選択不可にする。選択済み項目は選択解除が可能で、解除されて上限数を下回ったら未選択項目は再び選択可能になる。
1. 項目を選択したとき、すでに上限数と同じ数だけ選択されていたら、新しく選択した項目の選択を解除する。そうでなければ後続に選択項目を流す。

前者の方法では上限数と現在の選択数に応じて`ListBox`における各項目の選択可否を制御しなければなりません。後者の場合、とりあえず選択可能にしておいてあとから選択を解除し直せばよいことになります。ここでは後者のほうが簡単なので後者で実装します。

## 「項目を選択したとき」
準備段階で`IsSelected`プロパティに項目の選択状態をバインドしているので、データ全体を持つ`ReadOnlyReactiveCollection`から各要素の`IsSelected`の変更を監視します。それにはRxの`ObserveElementProperty`を使います。

```c#:ViewModel.cs
    Items.ObserveElementProperty(item => item.IsSelected).Subscribe(x => 
    {
        /* コレクションのどれかの要素のチェックが変更されたときの処理 */
    });
```
`Subscribe`に流れてくるのは、この例では`PropertyPack<ItemData, bool>`の変数となります。これは「`ObserveElementProperty`で監視しているプロパティが属するインスタンス(`ItemData`)」と「`ObserveElementProperty`で監視しているプロパティの変更後の値(`bool`)」を持つインスタンスです。

## 「すでに上限数と同じ数だけ選択されていたら、新しく選択した項目の選択を解除する」
現在の選択項目を取得するには、コレクション全体をなめて`IsSelected == true`なもののみ抽出すればよいでしょう。その項目の数を上限値と比較して超えていたら`IsSelected`を`false`に上書きします。

なお、上限値は`int`型のプロパティで持っておきましょう。未設定は無制限としておけばよいと思います。

```c#:ViewModel.cs
    public int MaxSelection { get; set; } = int.MaxValue;
~~
    Items.ObserveElementProperty(item => item.IsSelected).Subscribe(x => 
    {
        var selected = Items.Where(w => w.IsSelected);
        var count = selected.Count();
        if (count > MaxSelection)
        {
            // IsSelectedが変更されたインスタンスはx.Instanceで取得できる
            // x.Value は変更後のIsSelectedの値なのでx.Value = false;としても無意味
            x.Instance.IsSelected = false;
            return;
        }
    });
```

## 後続に選択項目を流す
選択されている項目を使って`ReactiveProperty<IEnumerable<T>>`や`ReactiveCollection<T>`を使いたい場合、`Subscribe`内でそれらのプロパティに値を設定してやる必要があります。しかしそのようなケースだと内容は選択状態に応じて自動で決まるべき(`ReadOnly`であるべき)ケースが多いです。これら２つを実現するには`System.Reactive.Subjects`の`Subject<T>`を使ってやると簡単です。

```c#:ViewModel.cs
// バインドするデータ -> Modelから取得する
public class ItemData : BindableBase
{
    public bool IsSelected { get => _isSelected; set => SetProperty(ref _isSelected; value); }
    private bool _isSelected;
}

public class ViewModel
{
    // ListBoxに設定するItemSource
    public ReadOnlyReactiveCollection<ItemData> Items { get; }
    // 元データの取得処理のModel
    private readonly IModel _model;
    // 最大選択可能数
    public int MaxSelection { get; set; } = int.MaxValue;
    // 選択項目の文字列表現を持つReactiveProperty
    public ReadOnlyReactivePropertySlim<IEnumerable<string>> SelectedItemTexts { get; }
    // 現在選択されている項目の文字列表現を流すSubject
    private Subject<IEnumerable<ItemData>> _selectedItemSubjects;
    public ViewModel(IModel model)
    {
        _model = model;
        // もしModelがObservableCollection<T>で持つなら
        Items = _model.Items.ToReadOnlyReactiveCollection(/* 変換処理 */);
        // もしModelがIEnumerable<T>で持つなら
        Items = _model.ObserveProperty(m => m.Items).
            SelectMany(item => /* 変換処理 */).
            ToReadOnlyReactiveCollection();

        // 正しい選択項目はSubject経由で流れてくるのでそれをもとにReactivePropertyを作る
        _selectedItemSubjects = new Subject<ItemData>();
        SelectedItemTexts = _selectedItemSubjects.
            SelectMany(s => s.ToString()).
            ToReadOnlyReactivePropertySlim();
        Items.ObserveElementProperty(item => item.IsSelected).Subscribe(x => 
        {
            var selected = Items.Where(w => w.IsSelected);
            var count = selected.Count();
            if (count > MaxSelection)
            {
                // IsSelectedが変更されたインスタンスはx.Instanceで取得できる
                // x.Value は変更後のIsSelectedの値なのでx.Value = false;としても無意味
                x.Instance.IsSelected = false;
                return;
           }
            // ここまで来たら現在の選択状態は正当なので後続に選択項目を流す
            _selectedItemSubjects.OnNext(selected);
        });
    }
}
```
これで「`MaxSelection`に指定された選択可能な上限まで選択可能」「上限を超えて選択しようとしたら選択が解除される」「選択状態が変わったとき、正当なときだけ値が流れてくる」を実現できました。

# 変更の通知について
`ObserveElementPropety(x => x.IsSelected)`は変更状態の監視をする`IObservable`の生成です。上限超過だと`Subscribe`内で`false`に値を更新しなおすので、`Subscribe`にもう一度値が流れてきます。具体的には、上限数が2のときに3つめを選択すると

1. 新しく選択されたため`ObserveElementProperty(~).Subscribe(~)`が3項目選択状態で流れる。内部で選択を解除
1. 選択解除により`IsSelected`が変更されたので、`ObserveElementProperty(~).Subscribe(~)`に2項目の選択状態で流れてくる。これは3項目を選択する前と同じ状態。
1. 選択内容が変わっていない状態で`OnNext`するが、**`OnNext`に流す`IEnumerable<ItemData>`を作り直している**ため、要素が実質同じでも後続の`ReactiveProperty`で値変更イベントが起きる。

という動きになります。最初の１・２については選択解除の都合上仕方がないのですが、３の「実質同じでも後続の`ReactiveProperty`で値変更が起きる」は無駄です。`Subject`にぶら下がる後続が多い場合などこれが気に入らないなら、現在の選択数を別にフィールドで保持（ローカル変数でもよい？）して同じ値なら`OnNext`しない、という実装が必要になります。

```c#
    // 現在選択中の数 選択前と選択後で数値が同じなら選択状態は変わっていない
    private int _selectedCount;
    ~~
    Items.ObserveElementProperty(x => x.IsSelected).Subscribe(x =>
    {
        var selected = Items.Where(w => w.IsSelected);
        var count = selected.Count();
        if (count > MaxSelection)
        {
            x.Instance.IsSelected = false; // 変更をもとに戻す
        } 
        else if (_selectedCount != count)
        {
            _selectedCount = count;
            _selectedItemSubjects.OnNext(selected);
        }
    })
```

こうしておくと上限以上を選択しようとしたときに`Subscribe`は選択→選択解除で2回起きますが、後続には値が流れません。