WPF --- Bindingで配列をObservableCollectionに変換

現状、ItemsControlのItemsSourceに通常の配列をバインドした際、配列を新しくする度にコントロールが全て再作成されます。

通常の配列の代わりにObservableCollectionをバインドすることにより、変更箇所のみViewが更新されるようになります。(要素の追加でViewが作成、要素の削除でViewも削除、要素の更新でViewの再利用が行われます。)

これによりパフォーマンスの向上だけでなく、マウスなどのフォーカスも正常に動作するようになります。

今回は配列をObservableCollectionに変換する拡張マークアップを作成してみたいと思います。

コード内で利用しているReplaceAt、ForEachオペレータは下記記事を参照してください。
LINQ独自オペレータ メモ - 何でもプログラミング

通常のBindingで実装

今回はカウントアップを行うボタンを複数作成するアプリケーションを例に実装していきます。

ボタンはリピートボタンを利用しているため、マウスダウンの状態で、カウントがどんどん進むことを想定しています。

f:id:any-programming:20170606001007p:plain

Xaml

RepeatButtonのCommandで、ItemsControlのDataContextのIncrementコマンドに自身のインデックスを渡すよう実装してあります。

<ItemsControl ItemsSource="{Binding Counts}" AlternationCount="100">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <StackPanel />
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <RepeatButton Content="{Binding}"
                          Command="{Binding DataContext.Increment, RelativeSource={RelativeSource AncestorType=ItemsControl}}" 
                          CommandParameter="{Binding Path=(ItemsControl.AlternationIndex), RelativeSource={RelativeSource TemplatedParent}}" />
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>


C#

Commandクラスは、単にExecuteをラムダで登録できるようにしたクラスです。

DataContext = new ViewModel();

class ViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    public int[] Counts { get; private set; } = new int[3];
    public Command<int> Increment { get; }
    public ViewModel()
    {
        Increment = new Command<int>(i =>
        {
            Counts = Counts.ReplaceAt(i, Counts[i] + 1).ToArray();
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Counts)));
        });
    }
}


実行して、ボタンの上でマウスダウンの状態を保っても、カウントアップが継続しないことが確認できます。

これはCountsが更新されてボタンが再作成され、マウスのフォーカスが外されてしまっているからです。

ToObservableCollectionExtension

ObservableCollectionを返すValueConverteを作成し、Bindingにセットして返しています。

Convertの中でObservableCollectionを更新しています。(更新アルゴリズムは適宜変更してください。)

ValueConverterクラスは、単にConvertをラムダで登録できるようにしたクラスです。

public class ToObservableCollectionExtension : MarkupExtension
{
    string _path;
    public ToObservableCollectionExtension(string path) => 
        _path = path;
    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        var target = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;
        var element = target.TargetObject as DependencyObject;
        // DependencyObjectでない場合(Templateのパース時)はthisを返す。
        if (element == null)
            return this;

        var collection = new ObservableCollection<object>();
        var converter = new ValueConverter<IEnumerable, ObservableCollection<object>>(src =>
        {
            src.Cast<object>().ForEach((i, x) =>
            {
                // 要素が不足しているので追加
                if (collection.Count <= i)
                    collection.Add(x);

                // 内容が異なれば更新
                else if (x != collection[i])
                    collection[i] = x;
            });
            // 余分な要素を削除
            for (int i = src.Cast<object>().Count(); i < collection.Count; ++i)
                collection.RemoveAt(collection.Count - 1);

            return collection;
        });

        return new Binding(_path) { Converter = converter }.ProvideValue(serviceProvider);
    }
}


ToObservableCollectionの利用

ItemsSourceのBindingの部分を、ToObservableCollectionに置き換えます。

<ItemsControl ItemsSource="{local:ToObservableCollection Counts}" AlternationCount="100">

これで、ボタン上でマウスダウンを保持するとカウントが進むようになります。