かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

WPF4.5入門 その46 「WPFのイベントシステム」

WPFは、イベントも独自の機構を構築しています。WPFのイベントシステムの特徴を説明する前に、なぜその仕組みが必要になるかというシンプルな例を示したいと思います。以下のようにButtonの中にButtonがあるシンプルなケースでのイベントについて考えてみます。

<StackPanel Margin="10">
    <Button Click="Button_Click">
        <Button Content="Button" />
    </Button>
</StackPanel>

外側のButtonのClickイベントには、以下のようなMessageBoxを表示するコードを記述しています。

private void Button_Click(object sender, RoutedEventArgs e)
{
    MessageBox.Show("Click");
}

外側のボタンをクリックしたときには、このイベントハンドラが呼び出されてMessageBoxが表示されることは予想できますが、ボタンの中に置かれたボタンをクリックしたときにはどうなるでしょうか?答えは、MessageBoxが表示されます。

WPFは、複雑にコントロールを組み合わせたUIを作ることができます。そのため、上記のような露骨なものではなくても上記の例のようにコントロール内のコントロールによって通常のCLRのイベントではボタンが本来のボタンの役割を果たさない可能性が出てきます。WPFのイベントシステムは、通常のイベントを拡張して、親要素へイベントを伝搬するバブルイベントという仕組みを提供しています。HTMLになじみのある人にとってはおなじみの動きです。

バブルイベントとは、イベント発生元でイベントが処理されなかった場合親要素へイベントを伝搬させる機能をもったイベントです。上記のボタンの例では、ボタンの中に置いたボタンがクリックされたときに、中に置いたボタンでイベントが処理されなかったため、親要素のボタンにクリックイベントが伝搬して、親要素のクリックイベントハンドラが呼び出されてMessageBoxが表示されるという動きになります。

WPFでは、バブルイベントの他にトンネルイベントという形のイベントも提供しています。一般的にPreviewという命名規則で始まるイベントがそれになります。バブルイベントが、イベントの発生元から親要素・親要素…へ伝搬していくのに対して、トンネルイベントはルートの要素からイベント発生元のオブジェクトの順番でイベントが伝搬していきます。(ちょうどトンネルイベントの逆の動きになります)トンネルイベントは、ユーザーの入力を処理するイベントに対して、プログラムが処理前に割り込むポイントを提供するために使用されます。そのため、ユーザーの入力を処理するカスタムコントロールを作成する場合以外は、自分で定義することはないでしょう。

これらの、バブルイベントやトンネルイベントなどのように、イベントの発生元だけでなくWPFのコントロールのツリー上の他のオブジェクトに対しても影響を与えるイベントをルーティングイベントと言います。

ルーティングイベントの定義方法

ルーティングイベントの定義は、EventManagerのRegisterEventメソッドを使って行います。定義の例を以下に示します。

class Person : FrameworkElement
{
    // イベント名Eventの命名規約のstaticフィールドに格納する
    public static RoutedEvent ToAgeEvent = EventManager.RegisterRoutedEvent(
        "ToAge", // イベント名
        RoutingStrategy.Tunnel, // イベントタイプ
        typeof(RoutedEventHandler), // イベントハンドラの型
        typeof(Person)); // イベントのオーナー
    // CLRのイベントのラッパー
    public event RoutedEventHandler ToAge
    {
        add { this.AddHandler(ToAgeEvent, value); }
        remove { this.RemoveHandler(ToAgeEvent, value); }
    }

    // 子を追加するメソッド
    public void AddChild(Person child)
    {
        this.AddLogicalChild(child);
    }
}

基本的には、依存関係プロパティなどと同じで専用の登録メソッドを使ってイベントを登録して、それのCLR用のラッパーを作成するという流れになります。第二引数に、トンネルかバブルかを指定します。一般的にトンネルイベントの名前はPreviewイベント名になります。

今回定義したイベントはトンネルイベントなのでイベント発生元へ親から伝搬していく形になります。以下にイベントを発行するプログラムの例を示します。

var parent = new Person { Name = "parent" };
var child = new Person { Name = "child" };

parent.AddChild(child);

parent.ToAge += (object s, RoutedEventArgs e) =>
{
    Console.WriteLine(((Person)e.Source).Name);
};

parent.RaiseEvent(new RoutedEventArgs(Person.ToAgeEvent));
child.RaiseEvent(new RoutedEventArgs(Person.ToAgeEvent));

まず、トンネルイベントの挙動を確認するための親子関係を構築しています。そして親のオブジェクトのほうでToAgeイベントハンドラの登録を行っています。ルーティングイベントでは、イベントの発生元がsenderとは限りません。(今回の例ではsenderにはparentが入ってきます)イベントの発生元を取得するには、イベント引数のSourceプロパティを利用します。今回の例では、イベントの発生元のNameプロパティの値を表示しています。

最後の2行は、parentとchildで、イベントの発行を行っています。ルーティングイベントの発行は、RaiseEventメソッドにRoutedEventArgsを渡す形で行います。RoutedEventArgsは、イベントの種類を表すRoutedEventを受け取ります。

このプログラムを実行すると以下のように表示されます。

parent
child

childには、イベントハンドラを登録していませんが、親のイベントハンドラが呼び出されてることが確認できます。

イベントのキャンセル

ルーティングイベントは、RoutedEventArgsのHandledプロパティをtrueにすることで、後続のイベントをキャンセルすることが出来ます。この機能を使うと、トンネルイベントやバブルイベントを途中でインターセプトして後続のイベントの処理をキャンセルすることが出来ます。

添付イベント

WPFのイベントシステムは、トンネルイベント、バブルイベントがあることを説明しました。このようなイベントがあるとクリックイベントがボタンで発生したとき、WindowやPanel系コントロールでもClickイベントが発生することになります。このような状況に対応するためにWindowやPanel系コントロールに全てのオブジェクトの全てのイベントを実装するのは現実的ではありません。WPFでは添付イベントという仕組みで、本来そのオブジェクトで定義されてないルーティングイベントを処理する方法を提供しています。

StackPanelにButtonのClickイベントを添付イベントとして設定するXAMLを以下に示します。

<StackPanel Button.Click="StackPanel_Click">
    <Button Content="Button1" />
</StackPanel>

基本的に添付プロパティと同じような記述になります。これと同じことをコードで記述する場合は以下のようになります。stackPanelという変数にButtonが置いてあるStackPanelが入っている場合のコード例です。

this.stackPanel.AddHandler(Button.ClickEvent, new RoutedEventHandler(this.StackPanel_Click));

過去記事