コントロールのイベントとバインドの実行タイミングを検証する

Silverlightのバインドは便利です。しかし、TwoWayモードで実行しても常にコントロールのプロパティとViewModelのプロパティの値が一致するわけではありません。タイミングによっては、不一致になることがあります。その一例として、コントロールのイベントが発行されたときにバインドが実行されているかを検証してみました。なお、この場合のバインドとは、TwoWayモード時に発生するコントロールのプロパティの値がViewModelのプロパティへセットされるときのことを指しています。

アジェンダ

  • RadioButtonのChecked, UnCheckedイベントとバインドのタイミング検証
  • ListBoxのSelectionChangedイベントとバインドのタイミング検証
  • まとめ

RadioButtonのChecked, UnCheckedイベントとバインドのタイミング検証

RadioButtonのChecked, UnCheckedイベントとバインドのタイミングを見てみるためのコードです。

<UserControl x:Class="BindTimingTest.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    d:DesignHeight="300" d:DesignWidth="400">

    <StackPanel x:Name="LayoutRoot" Background="White">
        <StackPanel Orientation="Horizontal">
            <RadioButton x:Name="RbtA" Content="A" 
                         IsChecked="{Binding IsA, Mode=TwoWay}" 
                         Checked="RbtA_Checked" Unchecked="RbtA_Unchecked"/>
            <RadioButton x:Name="RbtB" Content="B" 
                         IsChecked="{Binding IsB, Mode=TwoWay}" 
                         Checked="RbtB_Checked" Unchecked="RbtB_Unchecked"/>
        </StackPanel>
    </StackPanel>
</UserControl>
public partial class MainPage : UserControl
{
    private MainViewModel viewModel;
    public MainPage()
    {
        InitializeComponent();
        this.viewModel = new MainViewModel();
        DataContext = this.viewModel;
    }

    private void RbtA_Checked(object sender, RoutedEventArgs e)
    {
        System.Diagnostics.Debug.WriteLine("* RbtA_Checked");
        System.Diagnostics.Debug.WriteLine("   Property : " + this.RbtA.IsChecked + ", " + this.RbtB.IsChecked);
        System.Diagnostics.Debug.WriteLine("   ViewModel : " + this.viewModel.IsA + ", " + viewModel.IsB);
    }

    private void RbtB_Checked(object sender, RoutedEventArgs e)
    {
        System.Diagnostics.Debug.WriteLine("* RbtB_Checked");
        System.Diagnostics.Debug.WriteLine("   Property : " + this.RbtA.IsChecked + ", " + this.RbtB.IsChecked);
        System.Diagnostics.Debug.WriteLine("   ViewModel : " + this.viewModel.IsA + ", " + viewModel.IsB);
    }

    private void RbtA_Unchecked(object sender, RoutedEventArgs e)
    {
        System.Diagnostics.Debug.WriteLine("* RbtA_Unchecked");
        System.Diagnostics.Debug.WriteLine("   Property : " + this.RbtA.IsChecked + ", " + this.RbtB.IsChecked);
        System.Diagnostics.Debug.WriteLine("   ViewModel : " + this.viewModel.IsA + ", " + viewModel.IsB);
    }

    private void RbtB_Unchecked(object sender, RoutedEventArgs e)
    {
        System.Diagnostics.Debug.WriteLine("*RbtB_Unchecked");
        System.Diagnostics.Debug.WriteLine("   Property : " + this.RbtA.IsChecked + ", " + this.RbtB.IsChecked);
        System.Diagnostics.Debug.WriteLine("   ViewModel : " + this.viewModel.IsA + ", " + viewModel.IsB);
    }
}

public class MainViewModel : INotifyPropertyChanged
{
    private bool isA = true;
    private bool isB = false;

    public event PropertyChangedEventHandler PropertyChanged;

    public bool IsA
    {
        get { return this.isA; }
        set
        {
            System.Diagnostics.Debug.WriteLine("* IsA Setter Called : " + value);
            if (!Equals(this.isA, value))
            {
                this.isA = value;
                this.RaisePropertyChangedEventHandler("IsA");
            }
        }
    }

    public bool IsB
    {
        get { return this.isB; }
        set
        {
            System.Diagnostics.Debug.WriteLine("* IsB Setter Called : " + value);
            if (!Equals(this.isB, value))
            {
                this.isB = value;
                this.RaisePropertyChangedEventHandler("IsB");
            }
        }
    }

    private void RaisePropertyChangedEventHandler(string propertyName)
    {
        if (this.PropertyChanged != null)
        {
            this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

上記のコードを実行し、ラジオボタンAをクリックしたときの実行結果です。

* RbtA_Unchecked
   Property : False, True
   ViewModel : True, False
* IsA Setter Called : False
* RbtB_Checked
   Property : False, True
   ViewModel : False, False
* IsB Setter Called : True

実行結果からイベントとバインドのタイミングは下記のようになっていることがわかります。

  1. ラジオボタンAをクリックする。
  2. ラジオボタンAのUncheckedイベントが発行される。
    • この段階ではバインドが実行されていないので、ViewModelの値IsA,IsBとも古いまま
  3. ラジオボタンAのIsCheckedのバインドが実行されれ、ViewModelのIsAのセッターが呼ばれる。
  4. ラジオボタンBのCheckedが発行される。
    • この段階ではラジオボタンAのバインドのみ実行されてるので、ViewModelの値IsBは古いまま
  5. ラジオボタンBのIsCheckedのバインドが実行されれ、ViewModelのIsBのセッターが呼ばれる。

以上のことから、RadioButtonのChecked, UnCheckedイベントはバインドよりも早く実行されていることがわかります。

ListBoxのSelectionChangedイベントとバインドのタイミング検証

では、ListBoxのSelectionChangedイベントとバインドのタイミング検証もしてみましょう。

<UserControl x:Class="BindTimingTest.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    d:DesignHeight="300" d:DesignWidth="400">

    <StackPanel x:Name="LayoutRoot" Background="White">
        <ListBox x:Name="LbSampel" SelectionChanged="ListBox_SelectionChanged" 
                SelectedItem="{Binding SelectedItem, Mode=TwoWay}" 
                ItemsSource="{Binding Source}" Height="200"/>
    </StackPanel>
</UserControl>
public partial class MainPage : UserControl
{
    private MainViewModel viewModel;
    public MainPage()
    {
        InitializeComponent();
        this.viewModel = new MainViewModel();
        DataContext = this.viewModel;
    }

    private void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        System.Diagnostics.Debug.WriteLine("* ListBox_SelectionChanged");
        System.Diagnostics.Debug.WriteLine("   Property : " + this.LbSampel.SelectedItem);
        System.Diagnostics.Debug.WriteLine("   ViewModel : " + this.viewModel.SelectedItem);
    }
}

public class MainViewModel : INotifyPropertyChanged
{
    private ObservableCollection<Item> source
        = new ObservableCollection<Item>()
        {
            new Item { Name = "ItemA" },
            new Item { Name = "ItemB" },
            new Item { Name = "ItemC" },
        };

    private Item selectedItem = null;

    public event PropertyChangedEventHandler PropertyChanged;

    public ObservableCollection<Item> Source
    {
        get { return this.source; }
        set
        {
            System.Diagnostics.Debug.WriteLine("* Source Setter Called : " + value);
            if (!Equals(this.source, value))
            {
                this.source = value;
                this.RaisePropertyChangedEventHandler("Source");
            }
        }
    }

    public Item SelectedItem
    {
        get { return this.selectedItem; }
        set
        {
            System.Diagnostics.Debug.WriteLine("* SelectedItem Setter Called : " + value);
            if (!Equals(this.selectedItem, value))
            {
                this.selectedItem = value;
                this.RaisePropertyChangedEventHandler("SelectedItem");
            }
        }
    }

    private void RaisePropertyChangedEventHandler(string propertyName)
    {
        if (this.PropertyChanged != null)
        {
            this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

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

    public override string ToString()
    {
        return this.Name;
    }
}

上記のコードを実行し、1番目のアイテムをクリックしたときの実行結果です。

* SelectedItem Setter Called : ItemA
* ListBox_SelectionChanged
   Property : ItemA
   ViewModel : ItemA
  1. ListBoxのSelectedItemのバインドが実行されれ、ViewModelのSelectedItemのセッターが呼ばれる。
  2. ListBoxのSelectionChangedイベントが発行される。

ListBoxのSelectionChangedイベントよりも先にSelectedItemイベントが呼ばれていることがわかります。

まとめ

コントロールのイベントとバインドの実行タイミングを検証してみました。イベントによって、バインドの前に起きる場合と後に起きる場合があります。イベント・バインドを混載した構成を採用する場合、どちらが先に動作するか常に意識する必要があることがわかりました。