WPFのデータバインディングで納得いかない挙動

大阪でのマイクロソフト系の勉強会(VSハッカソン倶楽部、Room metro、Cafe Blend)に出るたびに、新しいことを教えてもらったり、懇親会でわからんことを質問したりしてる。
その話の流れっつーか続き的な感じでツイッター越しに教えてもらったりも最近した。

なんやかんや言うて本気でC#の良い感じのところを目指そうと思ったらやらなアカンことが山ほどあるんやけど、純粋な言語仕様に関しては非同期処理(TPLとかasync/awaitあたり)、LINQ、関数オブジェクト系(デリゲートとか)が1つの到達点になってくると思う。
さらにGUI関連でWPFを良い感じにしようと思うとこれがかなりハードルが高いのな。要素技術が多岐に渡るっつーか、定義ファイルによる抽象化のデメリットがあるっつーか。ざっくり言っちゃえばC#の言語仕様じゃ無い内部DSLを新たに習得せなアカンと言うか。

このエントリには大した情報が無いんで前振りに逃げてるけど、ほどほどにしとこう。

で、今日はWPFのデータバインディングの納得いかない挙動の話を書く。
WPFのデータバインディングってのは、いわゆるMVVMパターンの実現手段のことで、内部的なデータモデルであるModelとそれを表示するViewに加えて、ビューとほぼ1:1で対応するViewModelも含むアプリケーション・アーキテクチャ(あえてパターンとは言わない)のこと。
この辺の話もどっかで書きたいと思うけど、今日はそういう概念的な話より実装上の話。

まずはコードから。@yone64 さんに書いてもらったサンプルをベースに自分で書き起こした。
MainWindow.xaml

<Window x:Class="DataBindTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:DataBindTest"
        Title="MainWindow" Height="350" Width="525">
    <Window.DataContext>
        <local:CustomDs></local:CustomDs>
    </Window.DataContext>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="100"/>
        </Grid.RowDefinitions>
        <ListBox ItemsSource="{Binding Items}"
            IsSynchronizedWithCurrentItem="True"
            DisplayMemberPath="Name" 
            SelectedItem="{Binding SelectedItem,Mode=TwoWay}"/>
        <TextBox HorizontalAlignment="Left" Height="23" Text="{Binding SelectedItem.Name}" VerticalAlignment="Top" Width="120" Grid.Row="1"/>
        <Button Content="Button" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="141,0,0,0" Click="Button_Click" Grid.Row="1"/>
    </Grid>
</Window>

MainWindow.xaml.cs

using DataBindTest.Annotations;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Windows;

namespace DataBindTest
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            var ds = (CustomDs)DataContext;
            var kato = ds.Items.First(i => i.Name == "かとー");

            ds.SelectedItem = kato;
        }
    }

    public class CustomDs : INotifyPropertyChanged
    {
        public CustomDs()
        {
            _items = new List<Item>
                {
                    new Item {Name = "かーく"},
                    new Item {Name = "すぽっく"},
                    new Item {Name = "まっこい"},
                    new Item {Name = "かとー"},
                    new Item {Name = "ちゃーりー"}
                };
        }

        private List<Item> _items;
        private Item _selectedItem;

        public List<Item> Items
        {
            get
            {
                return _items;
            }
            set
            {
                _items = value;
                OnPropertyChanged1("Items");
            }
        }
        public Item SelectedItem
        {
            get
            {
                return _selectedItem;
            }
            set
            {
                OnPropertyChanged1("SelectedItem");
                _selectedItem = value;
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;

        [NotifyPropertyChangedInvocator]
        protected virtual void OnPropertyChanged1(string propertyName)
        {
            PropertyChangedEventHandler handler = PropertyChanged;
            if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    public class Item
    {
        public string Name { get; set; }
    }
}

コードはこんだけ。

画面上にはリストボックスが1つ、テキストボックスが1つ、ボタンが1つのウィンドウがある。
そのリストボックスに表示される中身(ViewModel)がItemクラスのリストになってる。
また、リストボックスのSelectedItemプロパティをViewModelのSelectedItemプロパティに双方向バインディングしてる。

実行結果は↑こんな感じで、リストボックスで選択中の要素が連動してテキストボックスに表示される。これがデータバインディング
さらに、ボタンを押すとViewModelのSelectedItemプロパティに強制的に「かとー」のItemを設定することでリストボックスで「かとー」が選択されて欲しい。
なぜならIsSynchronizedWithCurrentItemをTrueに設定しているから。

だが現実は無常。なぜか1テンポ遅れて設定されるのな。理由はわからない。

もっと調べるけど、なんでこういう挙動になるんかまったくなっとくいかん。最少構成のコードのつもりなんでバグって可能性は低いと思ってるんで、なんかわかってない部分で設定のやり方間違ってるとかなんやろなあ。

今日はこの辺まで。(眠い)