読者です 読者をやめる 読者になる 読者になる

ListBoxのSelectedItemsの変更をViewModelに反映させる方法

Model-View-ViewModel パターンで Silverlight アプリケーションを作成していて、ListBox で選択されている項目を ViewModel 側で取得したい場面に遭遇しました。

選択されている項目が1個だけなら、ViewModel に SelectedObject みたいなプロパティを用意して、それを ListView の SelectedItem プロパティにバインドすれば済みます。しかし、複数の項目が選択されている場合、この方法は使えません。ListBox には SelectedItems プロパティはありますが、これは依存関係プロパティではないので。

そこで、ListBox の SelectedItems プロパティの変更を ViewModel が持つコレクションに反映させるために、以下の添付ビヘイビアを作成してみました。

public static class ListBoxBehavior
{
    private static readonly DependencyProperty SelectedItemsBehaviorProperty =
        DependencyProperty.RegisterAttached(
            "SelectedItemsBehavior",
            typeof(SelectedItemsBehavior),
            typeof(ListBox),
            null);

    public static readonly DependencyProperty SelectedItemsProperty = DependencyProperty.RegisterAttached(
            "SelectedItems",
            typeof(IList),
            typeof(ListBoxBehavior),
            new PropertyMetadata(null, ItemsPropertyChanged));

    public static void SetSelectedItems(ListBox listBox, IList list)
    {
        listBox.SetValue(SelectedItemsProperty, list);
    }

    public static IList GetSelectedItems(ListBox listBox)
    {
        return listBox.GetValue(SelectedItemsProperty) as IList;
    }

    private static void ItemsPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var target = d as ListBox;
        if (target != null)
        {
            GetOrCreateBehavior(target, e.NewValue as IList);
        }
    }

    private static SelectedItemsBehavior GetOrCreateBehavior(ListBox target, IList list)
    {
        var behavior = target.GetValue(SelectedItemsBehaviorProperty) as SelectedItemsBehavior;
        if (behavior == null)
        {
            behavior = new SelectedItemsBehavior(target, list);
            target.SetValue(SelectedItemsBehaviorProperty, behavior);
        }

        return behavior;
    }

    private class SelectedItemsBehavior
    {
        private readonly ListBox _listBox;
        private readonly IList _boundList;
        private bool _listBoxSelectionChanging = false;
        private bool _collectionChanging = false;

        public SelectedItemsBehavior(ListBox listBox, IList boundList)
        {
            _boundList = boundList;
            if (_boundList is INotifyCollectionChanged)
            {
                ((INotifyCollectionChanged)_boundList).CollectionChanged += SelectedItemsBehavior_CollectionChanged;
            }

            _listBox = listBox;
            _listBox.SelectionChanged += OnSelectionChanged;
        }

        private void SelectedItemsBehavior_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            if (_listBoxSelectionChanging == false)
            {
                _collectionChanging = true;

                _listBox.SelectedItems.Clear();
                foreach (var item in _boundList)
                {
                    _listBox.SelectedItems.Add(item);
                }

                _collectionChanging = false;
            }
        }

        private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            if (_collectionChanging == false)
            {
                _listBoxSelectionChanging = true;

                _boundList.Clear();
                foreach (var item in _listBox.SelectedItems)
                {
                    _boundList.Add(item);
                }

                _listBoxSelectionChanging = false;
            }
        }
    } 
}

ViewModel に選択された項目を格納するためのコレクションを用意し、この添付プロパティにバインドしてやればいい。

<ListBox my:ListBoxBehavior.SelectedItems="{Binding Path=SelectedItems}"/>

バインドした ViewModel 内のコレクションが変更されたとき、ListBox に反映されるようにもなっています。

自分用に作ったものなので、十分に作り込んでいません。使っていて問題が見つかったら、その都度修正する予定。