OITA: Oika's Information Technological Activities

@oika 情報技術的活動日誌。

ListBoxのItemsSource, SelectedItemへのBindingでバグ?

表題のとおり、WPFのListBoxで、ItemsSourceとSelectedItemを
それぞれバインディング経由で使っていたときに、
バグといっていいかもしれない不具合があったので、一応メモ。

ちなみに手元の環境だと、.NET Framework 4.5では再現せず、
4.0だと再現するPCとしないPCがあったので(どちらもWindows 7, 64bit)、
どこかの更新で修正されたのかな。
3.5だとどちらも再現した。  

なにをしたかというと、ItemsSourceとSelectedItemsにそれぞれ
オブジェクトをバインドしてる状態で、
ItemsSourceのコレクションに対し、選択中項目の要素を入れ替えてやったら
画面上で選択がずっと残ってしまい、SelectionMode="Single"なのに
2行選択されているような見え方をするようになってしまった。

以下、再現コード。
まず、XAMLのリストボックスはこんなの。

<ListBox ItemsSource="{Binding ItemList, Mode=OneWay}"
         SelectedItem="{Binding SelItem, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />

  この画面のDataContextとして使用するクラスが以下みたいなやつ。

public class MainWindowViewModel : INotifyPropertyChanged {
    /// <summary>
    /// ItemsSourceにバインドするやつ
    /// </summary>
    public ObservableCollection<ItemClass> ItemList { get; private set; }

    private ItemClass _selItem;
    /// <summary>
    /// SelectedItemにバインドするやつ
    /// </summary>
    public ItemClass SelItem {
        get {
            return _selItem;
        }
        set {
            _selItem = value;

            if (PropertyChanged != null) {
                PropertyChanged(this, new PropertyChangedEventArgs("SelItem"));
            }
        }
    }

    /// <summary>
    /// INotifyPropertyChanged実装イベント
    /// </summary>
    public event PropertyChangedEventHandler PropertyChanged;
}

ItemClassは、適当な文字列のプロパティとかを持ってて
ToString()をオーバーライドしてるような適当なやつ。
あとはItemListに適当な項目を適当に追加しておいて、
ListBoxの適当な行を選択した状態で、以下のようなことをやる。  

int index = ItemList.IndexOf(SelItem);
ItemList[index] = new ItemClass();   //選択項目のあった部分を入れ替え

すると、選択されていた行の選択状態が見た目だけ残ったままになって、
別の行を選択すると、2行選択されているようになってしまう。
その後SelectedItemにnullを入れたりしても選択が消えない…。

まあなんとなく書き方も悪いというか、事情を酌んでやれる気はする。
SelectedItemとしてバインドされていたオブジェクトが
いきなりItemsSourceのリストから消えてしまうので対応できない感じだろう。
対処としては、以下の一行を間に追加してやればOK。

int index = ItemList.IndexOf(SelItem);
SelItem = null;  //<-- 先にSelectedItemをnullにしておく
ItemList[index] = new ItemClass();

SelItem = ItemList[index];  //<-- 選択行を維持したければ再設定