選択情報を持った、一つだけ選択できるCollection

コントローラを使用すれば一つのみ選択するということも簡単にできますが、それをC#コード上でやりたいケースがありました。MVVMで言うModel内で排他選択を行う方法。

そもそも、こういう風にコレクションを扱っていいのかわからないのだけど、親と子のクラスがあってコレクションの追加を親クラスを通して行う。ただし、選択状態は子ノードから変更できるようにしたい。

子ノード Item

namespace TestApp.Models
{
    using System;
    using System.Collections.ObjectModel;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Media;
    using Livet;

    /// <summary>
    /// 既存モデル単体の情報
    /// </summary>
    public class MovieFileItem : NotificationObject
    {
        public MovieFileItem(MovieFile parent)
        {
            unselectItem = parent.UnselectItem;
        }

        /// <summary>
        /// 兄弟アイテムの選択を削除
        /// </summary>
        public Action unselectItem = null;

        /// <summary>
        /// 選択されているかどうか
        /// </summary>
        private bool isSelected = true;

        /// <summary>
        /// 選択されているかどうか
        /// </summary>
        public bool IsSelected
        {
            get
            {
                return isSelected;
            }
            set
            {
                if(value && !(isSelected))
                {
                    // 新たに選択する場合は削除
                    if(unselectItem != null)
                        unselectItem();
                }
                isSelected = value;
                RaisePropertyChanged(() => IsSelected);
            }
        }

    }
}


親ノード

namespace TestApp.Models
{
    using System;
    using System.Collections.ObjectModel;
    using System.ComponentModel;
    using System.Linq.Expressions;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Media;
    using Livet;

    /// <summary>
    /// 既存モデル単体の情報
    /// </summary>
    public class MovieFile : ObservableCollection<MovieFileItem>, INotifyPropertyChanged
    {
        public MovieFile()
        {
        }


        #region 選択

        /// <summary>
        /// 全て選択解除する
        /// </summary>
        public void UnselectItem()
        {
            foreach(var item in this)
            {
                item.IsSelected = false;
            }
            RaisePropertyChanged(() => SelectedItem);
        }

        /// <summary>
        /// 指定されたアイテムを選択する
        /// </summary>
        /// <param name="selectItem">選択したいアイテム</param>
        public void SelectItem(MovieFileItem selectItem)
        {
            foreach(var item in this)
            {
                item.IsSelected = false;
            }

            selectItem.IsSelected = true;
            RaisePropertyChanged(() => SelectedItem);
        }

        /// <summary>
        /// 選択されたアイテム
        /// </summary>
        private MovieFileItem selectedItem = null;

        /// <summary>
        /// 選択されたアイテム
        /// </summary>
        public MovieFileItem SelectedItem
        {
            get
            {
                return selectedItem;
            }
            set
            {
                selectedItem = value;
                RaisePropertyChanged(() => SelectedItem);
            }
        }

        #endregion // 選択

        #region ObservableCollection override メンバー

        protected override void ClearItems()
        {
            SelectedItem = null;

            base.ClearItems();
        }

        protected override void RemoveItem(int index)
        {
            if(this[index].IsSelected)
            {
                SelectedItem = null;
            }

            base.RemoveItem(index);
        }

        protected override void SetItem(int index, MovieFileItem item)
        {
            if(this[index].IsSelected)
            {
                SelectedItem = null;
            }

            base.SetItem(index, item);
        }

        #endregion ObservableCollection override メンバー

        #region INotifyPropertyChanged メンバー

        public event PropertyChangedEventHandler PropertyChanged;

        protected void RaisePropertyChanged(string name)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(name));
        }
        
        protected void RaisePropertyChanged<T>(Expression<Func<T>> propertyExpression)
        {
            // Livetの実装
            if (propertyExpression == null) 
                throw new ArgumentNullException("propertyExpression");
            if (!(propertyExpression.Body is MemberExpression)) 
                throw new NotSupportedException("このメソッドでは ()=>プロパティ の形式のラムダ式以外許可されません");
            var memberExpression = (MemberExpression)propertyExpression.Body;
            RaisePropertyChanged(memberExpression.Member.Name);
        }
        
        #endregion
    }
}

更新通知のRaisePropertyChangedはLivetを使用。親ノードの更新通知はLivetコードをコピペ。