実行時に各エレメントが生成される仕組みとその順番

The Danger of Assigning Event Handlers in XAML

上記のblogの内容は、知らないと「なぜ?」と止まってしまうような現象だと思います。ということで、ここではその内容をコードをVisual Basicにして日本語で紹介します。


まず、下記のコードをWindow1.xamlとWindow1.xaml.vbとして作成し、実行してみてください。
XAML

<Window x:Class="Window1"
   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   Title="Window1" Height="300" Width="300">
    <StackPanel>
        <ComboBox x:Name="combo" SelectedIndex="0" SelectionChanged="combo_SelectionChanged" >
            <ComboBoxItem>こんにちは</ComboBoxItem>
            <ComboBoxItem>さようなら</ComboBoxItem>
        </ComboBox>
        <TextBlock x:Name="textBlk"/>
    </StackPanel>
</Window>


Visual Basic
Class Window1
 
    Private Sub combo_SelectionChanged(ByVal sender As System.Object, ByVal e As System.Windows.Controls.SelectionChangedEventArgs)
        textBlk.Text = CType(combo.SelectedItem, ComboBoxItem).Content.ToString
    End Sub
 
End Class


なんてことのないコードで、ComboBoxのアイテムの選択が変化したときにTextBlockにその選択されたアイテムを表示させるというものです。しかしながら、これを実行するとなぜか下記のようなExceptionが発生してしまいす。

この時点で「あたりまえじゃん」と思われた方はこの先を読む必要はありません。「なぜ?」と思われた方は続きをどうぞ。


では、XAMLのほうを下記のようにComboBoxとTextBlockの順番を入れ替えてみてください。
XAML
<Window x:Class="Window1"
   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   Title="Window1" Height="300" Width="300">
    <StackPanel>
        <TextBlock x:Name="textBlk"/>
        <ComboBox x:Name="combo" SelectedIndex="0" SelectionChanged="combo_SelectionChanged" >
            <ComboBoxItem>こんにちは</ComboBoxItem>
            <ComboBoxItem>さようなら</ComboBoxItem>
        </ComboBox>
    </StackPanel>
</Window>


そうすると、今度はExceptionが発生せず期待した通りの実行となるはずです。


これは各エレメントが生成される仕組みとその順番による現象です。WPFでは、XAML部分はBAMLと呼ばれるバイナリファイルに変換されリソースとしてアセンブリに埋め込まれます。実行時には、リソースからBAMLファイルが抽出されこれが解析、処理されて各エレメントのオブジェクトが生成されます。


この処理はどうもXAMLで書かれた順番に従って行われるようです。つまり、上記の例で言うとComboBoxの生成時にTextBlockはまだ生成されていないということになります。XAMLの中にイベントを記述していると、当然そのイベントも生成時のタイミングで実行されることになります。つまり、その時点で実行されたcombo_SelectionChangedイベントハンドラ内でTextBlockを参照しようとしても、まだTextBlockは生成されていないためNullReferenceExceptionが発生してしまうということです。


このような問題は、先ほどのXAMLのエレメントの順番を変えるという方法でも一応回避することができますが、StackPanel上の配置などの場合にはこの順番自体が重要な要素となるので適用できない場合も多いかと思います。
ほかの方法としては、まずXAML上でイベントを記述するのをやめて、コードからイベントハンドラを登録するという方法があります(なおHandles句を使ったイベントハンドラの登録方法だとNGです)。
Visual Basic
Public Sub New()
 
    ' この呼び出しは、Windows フォーム デザイナで必要です。
    InitializeComponent()
 
    ' InitializeComponent() 呼び出しの後で初期化を追加します。
    AddHandler combo.SelectionChanged, AddressOf combo_SelectionChanged
End Sub


ただし、この方法の場合XAMLでSelectedIndexプロパティを設定していてもcombo_SelectionChangedイベントハンドラは実行されませんね。combo_SelectionChangedイベントハンドラの処理内容をコンストラクタ内でも実行してやる必要があります。


もう1つの方法としては、XAMLでSelectedIndexプロパティを設定せずに、ComboBoxのLoadedイベントなどでSelectedIndexプロパティを設定してやるようにする方法です。
XAML
<ComboBox x:Name="combo" SelectionChanged="combo_SelectionChanged" Loaded="combo_Loaded">


Visual Basic
Private Sub combo_Loaded(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs)
    combo.SelectedIndex = 0
End Sub


この方法が、単純でトラブルも少なそうなので一番良いかもしれません。


しかしながら、参照元のblogにも書いてあるように、WPFの場合にはこのような処理はイベントを使わずにデータバインディングを利用したほうが良いかもしれません。何らかの処理をはさむような場合であっても、Converterを作成して対応することができます。