[WPF] WPF入門 ~ルーティングイベント~

投稿日 : 2009年3月24日 15:55
今回は「ルーティングイベント」について取り上げたいと思います。
ルーティングイベントはコントロールを使用する上でとても重要な物になります。
.NET Frameworkにて採用されているイベントについて知らない場合は、「イベントとデリゲート」を参照して下さい。
まずはMSDNでの説明を見てみましょう。

ルーティング イベントとは、RoutedEvent クラスおよび Windows Presentation Foundation (WPF) イベント システムによってサポートされるイベントです。ルーティング イベントは、アプリケーションの要素ツリー内のさまざまなリスナ上に存在するハンドラを呼び出すことができます。
一般的な WPF アプリケーションには、多数の要素が含まれます。コードで作成したか XAML の読み込みによって作成したかにかかわらず、これらの要素は互いに要素ツリー リレーションシップにあります。ルーティング イベント モデルでは、要素ツリーを使用して、発生したイベントを経路に沿って永続化できます。経路は、2 つの方向のいずれかへたどることができます。イベントは、要素ツリー ルートにあるリスナ上でハンドラを呼び出した後、ツリー ノード経路沿いにイベント ソースのノード要素まで、連続する子要素にルーティングすることができます。また、イベントは、ソース要素上のリスナを呼び出した後、要素ツリー ルートに到達するまで、連続する親要素にルーティングすることもできます。

上記説明を理解するために、まずは従来のWindows Formアプリケーションを例にとって見てみましょう。
まずは下記のようなフォームがあるとします。
(サンプルで使用している画像は書籍「Pro WPF 2008 C# 2008」のサンプルソースのhappyface.jpgを使用しています。こちらより取得できます。)



このフォームにはButton(青色)があり、その中にLabel(水色)とPictureBoxがあります。
このようなレイアウトの場合、今までは「Buttonをクリックする処理」をButtonのClickイベントハンドラに記述するだけでは全てを網羅出来ませんでした。
その理由として、Button内のLabelやPictureBoxの領域内でクリックを行うと、ButtonのClickイベントではなくLabelやPictureBoxのClickイベントが発生し、ButtoのClickイベントが発生しない為です。
LabelやPictureBoxがクリックされた場合も「Buttonがクリックされたように扱いたい」場合はそのような処理をLabelやPictureBoxのClickイベントハンドラ内にて実装する必要がありました。
または、ButtonやLabelやPictureBoxのイベントハンドラを集約したイベントハンドラを実装する方法もあります。(VBではHandlesキーワードを使用してイベントハンドラを集約したり)
今までのWindows Formでは「LabelもPictureBoxもButtonの装飾品にすぎないから、Clickされた場合にはButtonがClickされたようにしたい」といったものを実装するにも不必要な実装を行わなければなりませんでした。

WPFでは上記のような事が無いように、WPFイベントシステムが導入されました。これがルーティングイベントになります。
まず上記問題をルーティングイベントがどのように解決しているかを見る前に、基本的なイベントのハンドリング(イベントのアタッチ)を見てみましょう。

イベントのアタッチ
- 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" SizeToContent="WidthAndHeight">
    <Grid>
        <Button x:Name="Button1" Margin="10" Width="100" Height ="50"Click="Button1_Click">
            <StackPanel Orientation="Horizontal">
                <TextBlock Margin="5" VerticalAlignment="Center">Button</TextBlock>
                <Image Source="happyface.jpg" Margin="5" VerticalAlignment="Center" />
            </StackPanel>
        </Button>
    </Grid>
</Window>
- VB(コード) -
Class Window1
    Public Sub New()
 
        ' この呼び出しは、Windows フォーム デザイナで必要です。
        InitializeComponent()
 
        ' InitializeComponent() 呼び出しの後で初期化を追加します。
 
        AddHandler Button1.Click, AddressOf Button1_Click
 
        'または
        'AddHandler Button1.Click, New RoutedEventHandler(AddressOf Button1_Click)
 
        'またはHandlesキーワードを使用してアタッチする
    End Sub

    Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs)
    End Sub

    'Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs) Handles Button1.Click
    'End Sub
End Class
- C#(コード) -
public partial class Window1 : Window
{
    public Window1()
    {
        InitializeComponent();
 
        this.Button1.Click += new RoutedEventHandler(Button1_Click);
    }
 
    private void Button1_Click(object sender, RoutedEventArgs e)
    {
    }
}

このようにイベントのアタッチはXAMLかコードより行う事が出来ます。
コードによるイベントのアタッチは従来のイベントハンドラの実装と同様になりますが、XAMLでは「イベント名="イベントハンドラ名"」の定義によってイベントのアタッチを行います。

ルーティングイベントのアタッチ
では次に、ルーティングイベントのアタッチを見てみましょう。

- 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" SizeToContent="WidthAndHeight" Button.Click="Button1_Click">
    <Grid>
        <Button x:Name="Button1" Margin="10" Width="100" Height="50">
            <StackPanel Orientation="Horizontal">
                <TextBlock Margin="5" VerticalAlignment="Center">Button</TextBlock>
                <Image Source="happyface.jpg" Margin="5" VerticalAlignment="Center"/>
            </StackPanel>
        </Button>
    </Grid>
</Window>
- VB(コード) -
Class Window1
    Public Sub New()
 
        ' この呼び出しは、Windows フォーム デザイナで必要です。
        InitializeComponent()
 
        ' InitializeComponent() 呼び出しの後で初期化を追加します。
 
        Me.AddHandler(Button.ClickEvent, New RoutedEventHandler(AddressOf Button1_Click))
    End Sub
 
    Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs)
    End Sub
End Class
- C#(コード) -
public partial class Window1 : Window
{
    public Window1()
    {
        InitializeComponent();
 
        this.AddHandler(Button.ClickEvent, new RoutedEventHandler(Button1_Click));
    }
 
    private void Button1_Click(object sender, RoutedEventArgs e)
    {
    }
}

上記例ではWindowがButtonのClickイベントをアタッチしています。XAMLではイベントアタッチを添付プロパティを使用しているような定義を行っているのが特徴です。
コードではWindow自身のAddHandlerメソッドによってアタッチしているのが解ります。AddHandlerメソッドについては後ほど見てみたいと思います。

イベントでは、イベントを発生した対象を「イベントソース」と呼び、イベント発生を捕らえた対象を「イベントリスナ」と呼びます。
よって例ではButtonがイベントソース、Windowがイベントリスナとなります。
ここで、上記例のWindowはどのようにしてButtonのClickを捕らえているのでしょうか?
これはMSDNの説明文でもあるように、
  • 「ルーティング イベントは、アプリケーションの要素ツリー内のさまざまなリスナ上に存在するハンドラを呼び出すことができます。」
  • 「イベントは、要素ツリー ルートにあるリスナ上でハンドラを呼び出した後、ツリー ノード経路沿いにイベント ソースのノード要素まで、連続する子要素にルーティングすることができます。また、イベントは、ソース要素上のリスナを呼び出した後、要素ツリー ルートに到達するまで、連続する親要素にルーティングすることもできます。」
のように、さまざまなイベントリスナを呼び出すことができ、要素間で連続してある方向にイベントがルーティングされている為です。
「ルーティングされている」と書いているように、ルーティングイベントには「ルーティング方法」と言うものがあり、そのルーティング方法には3種類あります。
  • Tunnel(トンネル)
  • Bubble(バブル)
  • Direct(直接)
Tunnel(トンネル)とは、ルート要素(上記ではWindow)のイベントハンドラが初めに呼び出され、イベントソース(上記ではButton)まで構成されている要素間のイベントハンドラが呼ばれる事です。
Bubble(バブル)とは、文字の如く「泡のように」上へ向かってイベントソース(上記ではButton)からルート要素(上記ではWindow)まで構成されている要素間のイベントハンドラが呼ばれる事です。
Direct(直接)とは、従来のイベントと同じように、イベントソース(上記ではButton)のイベントハンドラだけが直接呼ばれる事です。

このルーティング方法がどのようにWPFイベントシステムで使用されているか順にその詳細を見てみましょう。

ルーティングイベントの実装方法
ルーティング方法の定義はルーティングイベントの定義時に指定します。
まずはルーティングイベントの実装方法を見る前に、WindowがButtonのClickイベントをアタッチする方法を、CLRイベントのアタッチとルーティングイベントのアタッチの実装方法を比較してみます。

- VB(CLRイベントアタッチ) -
AddHandler Button1.Click, AddressOf Button1_Click
- C#(CLRイベントアタッチ) -
this.Button1.Click += new RoutedEventHandler(Button1_Click);

- VB(ルーティングイベントアタッチ) -
Me.AddHandler(Button.ClickEvent, New RoutedEventHandler(AddressOf Button1_Click))
- C#(ルーティングイベントアタッチ) -
this.AddHandler(Button.ClickEvent, new RoutedEventHandler(Button1_Click));

CLRイベントアタッチではWindowの子要素のButtonオブジェクトを通してイベントのアタッチを行っています。
ルーティングイベントでは自身のAddHandlerメソッドによってイベントをアタッチしているのが解ります。
このAddHandlerメソッドと、AddHandlerメソッドの第1引数の「Button.ClickEvent」と言うのがルーティングイベントの肝になります。
WPFではルーティングイベントのルーティング方法を実現する為に、UIElementクラス(WindowクラスはUIElementクラスの派生クラス)のAddHandlerメソッドと、ButtonBaseクラス(ButtonクラスはButtonBaseクラスの派生クラス)のClickEvent識別フィールドが使用されます。

CLRイベントではイベント発生オブジェクトのイベントにイベントハンドラを登録する方法が使用されていますが、ルーティングイベントではイベントリスナのハンドラコレクションにルーティングイベントを登録する方法となっています。
WPFではUIElementクラスにルーティングイベント群を格納する「ハンドラコレクション」と言うものが実装されており、ハンドラコレクションに登録されている様々なルーティングイベントがWPFイベントシステムによって呼ばれるようになっています。

ハンドラコレクションに登録するルーティングイベントの実装例をButtonBaseクラスのClickイベントを用いて見てみましょう。

- VB -
Public MustInherit Class ButtonBase
    Inherits ContentControl
    Public Shared ReadOnly ClickEvent As RoutedEvent
 
    Shared Sub New()
        ClickEvent = _
        EventManager.RegisterRoutedEvent("Click", RoutingStrategy.Bubble, GetType(RoutedEventArgs), GetType(ButtonBase))
    End Sub
 
    Public Custom Event Click As RoutedEventHandler
        AddHandler(ByVal value As RoutedEventHandler)
            MyBase.AddHandler(ClickEvent, value)
        End AddHandler
        RemoveHandler(ByVal value As RoutedEventHandler)
            MyBase.RemoveHandler(ClickEvent, value)
        End RemoveHandler
        RaiseEvent(ByVal sender As Object, ByVal e As System.Windows.RoutedEventArgs)
        End RaiseEvent
    End Event
 
    Protected Overridable Sub OnClick()
        Dim e As New RoutedEventArgs(ButtonBase.ClickEvent, Me)
        MyBase.RaiseEvent(e)
    End Sub
End Class
- C# -
public abstract class ButtonBase : ContentControl
{
    public static readonly RoutedEvent ClickEvent;
 
    static ButtonBase()
    {
        ClickEvent =
            EventManager.RegisterRoutedEvent("Click", RoutingStrategy.Bubble, typeof(RoutedEventArgs), typeof(ButtonBase));
    }
 
    public event RoutedEventHandler Click
    {
        add {base.AddHandler(ButtonBase.ClickEvent,value);}
        remove { base.RemoveHandler(ButtonBase.ClickEvent, value); }
    }
 
    protected virtual void OnClick()
    {
        RoutedEventArgs e = new RoutedEventArgs(ButtonBase.ClickEvent,this);
        base.RaiseEvent(e);
    }
}

このようになります。

上記例を順序を追って見てみましょう。
  1. ルーティングイベントとして識別フィールドを作成する為に、Publicでかつ静的なRoutedEvent型のフィールドを定義する
  2. EventManagerクラスのRegisterRoutedEventメソッドを使用して識別フィールドを作成する
  3. ルーティングイベントをCLRイベントとしても公開する為のラッパーイベントとイベント発行用メソッドを作成する
  4. ラッパーイベントとイベント発行用メソッド内で、AddHandlerメソッド・RemoveHandlerメソッド・RaiseEventメソッドを使用してWPFイベントシステムを使用する
1.ではRoutedEvent型のフィールドとしてルーティングイベントの識別フィールドを定義します。ここで定義される識別フィールドが、ルーティングイベントのアタッチする際に使用するAddHandlerやRemoveHandlerの第1引数に指定されます。
2.でルーティングイベントの識別フィールドをEventManagerクラスのRegisterRoutedEventメソッド作成しますが、この第2引数のRoutingStrategy列挙値(ButtonBase.ClickEventではBubble)を指定してルーティング方法を決定します。
3.で依存関係プロパティのように、ルーティングイベントをCLRイベントとしても使用出来るようにする為に、CLRイベント定義のAdd実装とRemove実装を使用して定義し、イベントの発行はCLRイベント発行の実装ルールに基づきProtectedな仮想メソッドを使用してイベント発行用メソッドを実装する。
4.でCLRイベントとイベント発行用メソッドの内部にてUIElementクラスのAddHandlerメソッド・RemoveHandlerメソッド・RaiseEventメソッドを使用しルーティングイベントを処理する。

このようにルーティングイベントを実装し、ルーティング方法を指定します。

ルーティング方法(Tunnel - トンネル)
ではルーティング方法がどのようにWPFイベントシステムによって機能しているかを簡単なContentControlクラスを継承したクラスを作成してみてみましょう。
ContentControlクラスはUIElementクラスの派生クラスです。UIElementクラスではルーティングイベントの基本実装は持っていますが、親子要素間を作成する為の機構が用意されていません。ContentControlクラスではContentプロパティが実装されており、Contentプロパティは単一のコンテンツを含む事が出来るプロパティです。そのContentプロパティに指定したオブジェクトは内部で親子関係を作成してくれる為今回使用します。

まずはルーティング方法の「Tunnel(トンネル)」を実装する例です。

- VB(Tunnel) -
Public Class RoutedEventSample
    Inherits ContentControl
    Public Shared ReadOnly PreviewSampleEvent As RoutedEvent
 
    Shared Sub New()
        PreviewSampleEvent = _
        EventManager.RegisterRoutedEvent("PreviewSample", RoutingStrategy.Tunnel, GetType(RoutedEventArgs), GetType(RoutedEventSample))
    End Sub
 
    Public Custom Event PreviewSample As RoutedEventHandler
        AddHandler(ByVal value As RoutedEventHandler)
            MyBase.AddHandler(PreviewSampleEvent, value)
        End AddHandler
        RemoveHandler(ByVal value As RoutedEventHandler)
            MyBase.RemoveHandler(PreviewSampleEvent, value)
        End RemoveHandler
        RaiseEvent(ByVal sender As Object, ByVal e As System.Windows.RoutedEventArgs)
        End RaiseEvent
    End Event
 
    Protected Overridable Sub OnPreviewSample(ByVal e As RoutedEventArgs)
        MyBase.RaiseEvent(e)
    End Sub
 
    Public Sub Fire()
        Dim e As New RoutedEventArgs(PreviewSampleEvent, Me)
        Me.OnPreviewSample(e)
    End Sub
End Class
- C#(Tunnel) -
public class RoutedEventSample : ContentControl
{
    public static readonly RoutedEvent PreviewSampleEvent;
 
    static RoutedEventSample()
    {
        PreviewSampleEvent =
            EventManager.RegisterRoutedEvent("PreviewSample", RoutingStrategy.Tunnel, typeof(RoutedEventArgs), typeof(RoutedEventSample));
    }
 
    public event RoutedEventHandler PreviewSample
    {
        add { base.AddHandler(PreviewSampleEvent, value); }
        remove { base.RemoveHandler(PreviewSampleEvent, value); }
    }
 
    protected virtual void OnPreviewSample(RoutedEventArgs e)
    {
        base.RaiseEvent(e);
    }
 
    public void Fire()
    {
        RoutedEventArgs e = new RoutedEventArgs(PreviewSampleEvent, this);
        this.OnPreviewSample(e);
    }
}

「Tunnel」の実装として下記の幾つかのルールがあります。
  • 「Tunnel」方法のルーティングイベント名にはプレフィックス「接頭辞」として "Preview" を付ける。(Preview + イベント名)
  • ルーティングイベント識別子フィールドのサフィックス「接尾辞」には "Event" を付ける。(イベント名 + Event)
  • EventManager.RegisterRoutedEventメソッドにて登録するイベント名はCLRラッパーイベント名に合わせる。(今回ではPreviewSample)
例のRoutedEventSampleクラスはPreviewSampleルーティングイベントを実装しています。またルーティング方法として「Tunnel」として指定しています。通常ではイベント発行はイベントを実装しているクラスが行いますが、サンプルの為に「Fireメソッド」を作成し、イベントを外部より発行出来るように実装しています。
ではこのルーティング方法の「Tunnel」を確認する為に、簡単なコンソール出力をしてみます。

- VB -
Public Class Program
    Public Shared Sub Main()
        Dim rootElement As New RoutedEventSample()
        rootElement.Name = "RootElement"
        Dim leafElement As New RoutedEventSample()
        leafElement.Name = "LeafElement"
        Dim child1Element As New RoutedEventSample()
        child1Element.Name = "Child1Element"
        Dim child2Element As New RoutedEventSample()
        child2Element.Name = "Child2Element"
 
        rootElement.Content = leafElement
        leafElement.Content = child1Element
        child1Element.Content = child2Element
 
        rootElement.AddHandler(RoutedEventSample.PreviewSampleEvent, New RoutedEventHandler(AddressOf RoutedEventSample_PreviewSample))
        leafElement.AddHandler(RoutedEventSample.PreviewSampleEvent, New RoutedEventHandler(AddressOf RoutedEventSample_PreviewSample))
        child1Element.AddHandler(RoutedEventSample.PreviewSampleEvent, New RoutedEventHandler(AddressOf RoutedEventSample_PreviewSample))
        child2Element.AddHandler(RoutedEventSample.PreviewSampleEvent, New RoutedEventHandler(AddressOf RoutedEventSample_PreviewSample))
 
        child1Element.Fire()
 
        Console.ReadLine()
    End Sub
 
    Private Shared Sub RoutedEventSample_PreviewSample(ByVal sender As Object, ByVal e As RoutedEventArgs)
        Dim element As ContentControl = DirectCast(sender, ContentControl)
        Console.WriteLine("イベント名 = {0}", e.RoutedEvent.Name)
        Console.WriteLine("イベントをハンドルしたオブジェクト = {0}", element.Name)
        Console.WriteLine()
    End Sub
End Class
- C# -
public class Program
{
    [STAThread]
    static void Main()
    {
        RoutedEventSample rootElement = new RoutedEventSample();
        rootElement.Name = "RootElement";
        RoutedEventSample leafElement = new RoutedEventSample();
        leafElement.Name = "LeafElement";
        RoutedEventSample child1Element = new RoutedEventSample();
        child1Element.Name = "Child1Element";
        RoutedEventSample child2Element = new RoutedEventSample();
        child2Element.Name = "Child2Element";
 
        rootElement.Content = leafElement;
        leafElement.Content = child1Element;
        child1Element.Content = child2Element;
 
        rootElement.AddHandler(RoutedEventSample.PreviewSampleEvent, new RoutedEventHandler(RoutedEventSample_PreviewSample));
        leafElement.AddHandler(RoutedEventSample.PreviewSampleEvent, new RoutedEventHandler(RoutedEventSample_PreviewSample));
        child1Element.AddHandler(RoutedEventSample.PreviewSampleEvent, new RoutedEventHandler(RoutedEventSample_PreviewSample));
        child2Element.AddHandler(RoutedEventSample.PreviewSampleEvent, new RoutedEventHandler(RoutedEventSample_PreviewSample));
 
        child1Element.Fire();
 
        Console.ReadLine();
    }
 
    private static void RoutedEventSample_PreviewSample(object sender,RoutedEventArgs e)
    {
        ContentControl element = (ContentControl)sender;
        Console.WriteLine("イベント名 = {0}", e.RoutedEvent.Name);
        Console.WriteLine("イベントをハンドルしたオブジェクト = {0}", element.Name);
        Console.WriteLine();
    }
}

上記の出力結果はこのようになります。

イベント名 = PreviewSample
イベントをハンドルしたオブジェクト = RootElement

イベント名 = PreviewSample
イベントをハンドルしたオブジェクト = LeafElement

イベント名 = PreviewSample
イベントをハンドルしたオブジェクト = Child1Element

上記出力例ではこのような要素関係が出来ています。

RootElement(ルート要素)
    ∟ Leaf1Element(子要素1)
           ∟ Child1Element(子要素2) ← イベントソース(イベント発生元)
                  ∟ Child2Element(子要素3)

このように「Tunnel」方法のルーティングイベントは、ルート要素(RootElement)からイベントソース(Child1Element)に向かってイベントが要素の経路を辿っているのが解ります。
よってイベントソースの子要素(Child2Element)にはルーティングイベントが伝わっていません。「Tunnel」方法のルーティングイベントは通常「Bubble」方法のイベントと対に実装されます。
WPFで提供されているルーティングイベントで「PreviewMouseDown」と「MouseDown」などがそれに該当します。「Tunnel」方法は通常「Bubble」方法のイベントの検査用に使用されます。

ルーティング方法(Bubble - バブル)
次はルーティング方法の「Bubble(バブル)」の実装例です。

- VB(Bubble) -
Public Class RoutedEventSample
    Inherits ContentControl
    Public Shared ReadOnly SampleEvent As RoutedEvent
 
    Shared Sub New()
        SampleEvent = _
        EventManager.RegisterRoutedEvent("SampleEvent", RoutingStrategy.Bubble, GetType(RoutedEventArgs), GetType(RoutedEventSample))
    End Sub
 
    Public Custom Event Sample As RoutedEventHandler
        AddHandler(ByVal value As RoutedEventHandler)
            MyBase.AddHandler(SampleEvent, value)
        End AddHandler
        RemoveHandler(ByVal value As RoutedEventHandler)
            MyBase.RemoveHandler(SampleEvent, value)
        End RemoveHandler
        RaiseEvent(ByVal sender As Object, ByVal e As System.Windows.RoutedEventArgs)
        End RaiseEvent
    End Event
 
    Protected Overridable Sub OnSample(ByVal e As RoutedEventArgs)
        MyBase.RaiseEvent(e)
    End Sub
 
    Public Sub Fire()
        Dim e As New RoutedEventArgs(SampleEvent, Me)
        Me.OnSample(e)
    End Sub
End Class
- C#(Bubble) -
public class RoutedEventSample : ContentControl
{
    public static readonly RoutedEvent SampleEvent;
 
    static RoutedEventSample()
    {
        SampleEvent =
            EventManager.RegisterRoutedEvent("Sample", RoutingStrategy.Bubble, typeof(RoutedEventArgs), typeof(RoutedEventSample));
    }
 
    public event RoutedEventHandler Sample
    {
        add { base.AddHandler(SampleEvent, value); }
        remove { base.RemoveHandler(SampleEvent, value); }
    }
 
    protected virtual void OnSample(RoutedEventArgs e)
    {
        base.RaiseEvent(e);
    }
 
    public void Fire()
    {
        RoutedEventArgs e = new RoutedEventArgs(SampleEvent, this);
        this.OnSample(e);
    }
}

「Bubble」の実装として下記の幾つかのルールがあります。
  • ルーティングイベント識別子フィールドのサフィックス(接尾辞)には "Event" を付ける。(イベント名 + Event)
  • EventManager.RegisterRoutedEventメソッドにて登録するイベント名はCLRラッパーイベント名に合わせる。(今回ではSample)
ではこのルーティング方法の「Bubble」を確認する為に、コンソール出力をしてみます。

- VB -
Public Class Program
    Public Shared Sub Main()
        Dim rootElement As New RoutedEventSample()
        rootElement.Name = "RootElement"
        Dim leafElement As New RoutedEventSample()
        leafElement.Name = "LeafElement"
        Dim child1Element As New RoutedEventSample()
        child1Element.Name = "Child1Element"
        Dim child2Element As New RoutedEventSample()
        child2Element.Name = "Child2Element"
 
        rootElement.Content = leafElement
        leafElement.Content = child1Element
        child1Element.Content = child2Element
 
        rootElement.AddHandler(RoutedEventSample.SampleEvent, New RoutedEventHandler(AddressOf RoutedEventSample_Sample))
        leafElement.AddHandler(RoutedEventSample.SampleEvent, New RoutedEventHandler(AddressOf RoutedEventSample_Sample))
        child1Element.AddHandler(RoutedEventSample.SampleEvent, New RoutedEventHandler(AddressOf RoutedEventSample_Sample))
        child2Element.AddHandler(RoutedEventSample.SampleEvent, New RoutedEventHandler(AddressOf RoutedEventSample_Sample))
 
        child2Element.Fire()
 
        Console.ReadLine()
    End Sub
 
    Private Shared Sub RoutedEventSample_Sample(ByVal sender As Object, ByVal e As RoutedEventArgs)
        Dim element As ContentControl = DirectCast(sender, ContentControl)
        Console.WriteLine("イベント名 = {0}", e.RoutedEvent.Name)
        Console.WriteLine("イベントをハンドルしたオブジェクト = {0}", element.Name)
        Console.WriteLine()
    End Sub
End Class
- C# -
public class Program
{
    [STAThread]
    static void Main()
    {
        RoutedEventSample rootElement = new RoutedEventSample();
        rootElement.Name = "RootElement";
        RoutedEventSample leafElement = new RoutedEventSample();
        leafElement.Name = "LeafElement";
        RoutedEventSample child1Element = new RoutedEventSample();
        child1Element.Name = "Child1Element";
        RoutedEventSample child2Element = new RoutedEventSample();
        child2Element.Name = "Child2Element";
 
        rootElement.Content = leafElement;
        leafElement.Content = child1Element;
        child1Element.Content = child2Element;
 
        rootElement.AddHandler(RoutedEventSample.SampleEvent, new RoutedEventHandler(RoutedEventSample_Sample));
        leafElement.AddHandler(RoutedEventSample.SampleEvent, new RoutedEventHandler(RoutedEventSample_Sample));
        child1Element.AddHandler(RoutedEventSample.SampleEvent, new RoutedEventHandler(RoutedEventSample_Sample));
        child2Element.AddHandler(RoutedEventSample.SampleEvent, new RoutedEventHandler(RoutedEventSample_Sample));
 
        child2Element.Fire();
 
        Console.ReadLine();
    }
 
    private static void RoutedEventSample_Sample(object sender,RoutedEventArgs e)
    {
        ContentControl element = (ContentControl)sender;
        Console.WriteLine("イベント名 = {0}", e.RoutedEvent.Name);
        Console.WriteLine("イベントをハンドルしたオブジェクト = {0}", element.Name);
        Console.WriteLine();
    }
}

上記出力例ではこのような要素関係が出来ています。

RootElement(ルート要素)
    ∟ Leaf1Element(子要素1)
           ∟ Child1Element(子要素2)
                  ∟ Child2Element(子要素3) ← イベントソース(イベント発生元)

上記の出力結果はこのようになります。

イベント名 = SampleEvent
イベントをハンドルしたオブジェクト = Child2Element

イベント名 = SampleEvent
イベントをハンドルしたオブジェクト = Child1Element

イベント名 = SampleEvent
イベントをハンドルしたオブジェクト = LeafElement

イベント名 = SampleEvent
イベントをハンドルしたオブジェクト = RootElement

このように「Bubble」方法のルーティングイベントは、イベントソース(Child2Element)からルート要素(RootElement)に向かってイベントが要素の経路を辿っているのが解ります。
ルーティングイベントの実装では一番「Bubble」方法のルーティングイベントが実装されています。「Tunnel」を使用し要素経路の検査が必要がないようなイベントでは「Bubble」方法の実装のイベントもあります。

ルーティング方法(Direct - 直接)
次はルーティング方法の「Direct(直接)」の実装例です。

- VB(Direct) -
Public Class RoutedEventSample
    Inherits ContentControl
    Public Shared ReadOnly SampleEvent As RoutedEvent
 
    Shared Sub New()
        SampleEvent = _
        EventManager.RegisterRoutedEvent("SampleEvent", RoutingStrategy.Direct, GetType(RoutedEventArgs), GetType(RoutedEventSample))
    End Sub
 
    Public Custom Event Sample As RoutedEventHandler
        AddHandler(ByVal value As RoutedEventHandler)
            MyBase.AddHandler(SampleEvent, value)
        End AddHandler
        RemoveHandler(ByVal value As RoutedEventHandler)
            MyBase.RemoveHandler(SampleEvent, value)
        End RemoveHandler
        RaiseEvent(ByVal sender As Object, ByVal e As System.Windows.RoutedEventArgs)
        End RaiseEvent
    End Event
 
    Protected Overridable Sub OnSample(ByVal e As RoutedEventArgs)
        MyBase.RaiseEvent(e)
    End Sub
 
    Public Sub Fire()
        Dim e As New RoutedEventArgs(SampleEvent, Me)
        Me.OnSample(e)
    End Sub
End Class
- C#(Direct) -
public class RoutedEventSample : ContentControl
{
    public static readonly RoutedEvent SampleEvent;
 
    static RoutedEventSample()
    {
        SampleEvent =
            EventManager.RegisterRoutedEvent("Sample", RoutingStrategy.Direct, typeof(RoutedEventArgs), typeof(RoutedEventSample));
    }
 
    public event RoutedEventHandler Sample
    {
        add { base.AddHandler(SampleEvent, value); }
        remove { base.RemoveHandler(SampleEvent, value); }
    }
 
    protected virtual void OnSample(RoutedEventArgs e)
    {
        base.RaiseEvent(e);
    }
 
    public void Fire()
    {
        RoutedEventArgs e = new RoutedEventArgs(SampleEvent, this);
        this.OnSample(e);
    }
}

「Direct」の実装として下記の幾つかのルールがあります。
  • ルーティングイベント識別子フィールドのサフィックス(接尾辞)には "Event" を付ける。(イベント名 + Event)
  • EventManager.RegisterRoutedEventメソッドにて登録するイベント名はCLRラッパーイベント名に合わせる。(今回ではSample)
ではこのルーティング方法「Direct」を確認する為に、コンソール出力をしてみます。
今回は「Bubble」方法と同じコンソール出力プログラムを使用します。出力結果はこのようになります。

イベント名 = SampleEvent
イベントをハンドルしたオブジェクト = Child2Element

このように「Direct」方法のルーティングイベントは、イベントソース(Child2Element)のみがイベントの発生を通知されているのが解ります。
「Direct」方法のルーティングイベントは従来のイベントと同様な物になっています。

ルーティングイベントの処理
ルーティングイベントでは要素経路を辿る時に、イベントハンドラ内でイベント引数を用いて「処理済み」にする事が出来ます。
まずは「Bubble」方法を実装した例を用いて見てみましょう。

- VB -
Public Class Program
    Public Shared Sub Main()
        Dim rootElement As New RoutedEventSample()
        rootElement.Name = "RootElement"
        Dim leafElement As New RoutedEventSample()
        leafElement.Name = "LeafElement"
        Dim child1Element As New RoutedEventSample()
        child1Element.Name = "Child1Element"
        Dim child2Element As New RoutedEventSample()
        child2Element.Name = "Child2Element"
 
        rootElement.Content = leafElement
        leafElement.Content = child1Element
        child1Element.Content = child2Element
 
        rootElement.AddHandler(RoutedEventSample.SampleEvent, New RoutedEventHandler(AddressOf RoutedEventSample_Sample))
        leafElement.AddHandler(RoutedEventSample.SampleEvent, New RoutedEventHandler(AddressOf RoutedEventSample_Sample))
        child1Element.AddHandler(RoutedEventSample.SampleEvent, New RoutedEventHandler(AddressOf RoutedEventSample_Sample))
        child2Element.AddHandler(RoutedEventSample.SampleEvent, New RoutedEventHandler(AddressOf RoutedEventSample_Sample))
 
        child2Element.Fire()
 
        Console.ReadLine()
    End Sub
 
    Private Shared Sub RoutedEventSample_Sample(ByVal sender As Object, ByVal e As RoutedEventArgs)
        Dim element As ContentControl = DirectCast(sender, ContentControl)
        Console.WriteLine("イベント名= {0}", e.RoutedEvent.Name)
        Console.WriteLine("イベントをハンドルしたオブジェクト = {0}", element.Name)
        If element.Name = "LeafElement" Then
            e.Handled = True
            Console.WriteLine("LeafElementによって処理済みとされました。")
        End If
        Console.WriteLine()
    End Sub
End Class
- C# -
public class Program
{
    [STAThread]
    static void Main()
    {
        RoutedEventSample rootElement = new RoutedEventSample();
        rootElement.Name = "RootElement";
        RoutedEventSample leafElement = new RoutedEventSample();
        leafElement.Name = "LeafElement";
        RoutedEventSample child1Element = new RoutedEventSample();
        child1Element.Name = "Child1Element";
        RoutedEventSample child2Element = new RoutedEventSample();
        child2Element.Name = "Child2Element";
 
        rootElement.Content = leafElement;
        leafElement.Content = child1Element;
        child1Element.Content = child2Element;
 
        rootElement.AddHandler(RoutedEventSample.SampleEvent, new RoutedEventHandler(RoutedEventSample_Sample));
        leafElement.AddHandler(RoutedEventSample.SampleEvent, new RoutedEventHandler(RoutedEventSample_Sample));
        child1Element.AddHandler(RoutedEventSample.SampleEvent, new RoutedEventHandler(RoutedEventSample_Sample));
        child2Element.AddHandler(RoutedEventSample.SampleEvent, new RoutedEventHandler(RoutedEventSample_Sample));
 
        child2Element.Fire();
 
        Console.ReadLine();
    }
 
    private static void RoutedEventSample_Sample(object sender,RoutedEventArgs e)
    {
        ContentControl element = (ContentControl)sender;
        Console.WriteLine("イベント名 = {0}", e.RoutedEvent.Name);
        Console.WriteLine("イベントをハンドルしたオブジェクト = {0}", element.Name);
        if (element.Name == "LeafElement")
        {
            e.Handled = true;
            Console.WriteLine("LeafElementによって処理済みとされました。");
        }
        Console.WriteLine();
    }
}

この出力結果はこのようになります。

イベント名 = Sample
イベントをハンドルしたオブジェクト = Child2Element

イベント名 = Sample
イベントをハンドルしたオブジェクト = Child1Element

イベント名 = Sample
イベントをハンドルしたオブジェクト = LeafElement
LeafElementによって処理済とされました。

上記の出力結果からわかる通りに、「Bubble」方法のルーティングイベントではルート要素(RootElement)までイベントが伝達する所を、子要素のイベントリスナ(LeafElement)がイベントを処理済みとしてマークしている為に、それ以降の要素へのイベントの伝達が行われていません。
このように、イベントハンドラの第2引数のイベントアーギュメント、RoutedEventArgsクラスのHandledプロパティを使用しルーティングイベントを処理済みにする事が出来ます。
Handledプロパティを "True" に設定する事を「イベントを処理済みとしてマークする」と言います。基本的にイベントを処理済みとしてマークするのは後続の要素経路へのイベントの伝達を止める場合に使用します。

イベント処理を使用した「Tunnel」と「Bubble」イベントの実装
ルーティングイベントの「Tunnel」と「Bubble」は対になって実装されます。初めに「Tunnel」方法でイベントがルート要素からイベントソースまで伝達し検査されその後に「Bubble」方式のイベントがイベントソースからルート要素へ伝達していきます。
また、「Tunnel」と「Bubble」で使用されるイベント情報(RoutedEventArgsクラス)は共有されるのが一般的です。それは「Tunnel」は「Bubble」の検査用として使用される為、「Tunnel」方式のイベントの伝達中に、検査結果が正しくない場合、「Tunnel」の後続に発生する「Bubble」は伝達しないようにする為です。
それではイベント処理を使用した「Tunnel」と「Bubble」の実装例です。

- VB -
Public Class RoutedEventSample
    Inherits ContentControl
    Public Shared ReadOnly PreviewSampleEvent As RoutedEvent
    Public Shared ReadOnly SampleEvent As RoutedEvent
 
    Shared Sub New()
        PreviewSampleEvent = _
        EventManager.RegisterRoutedEvent("PreviewSample", RoutingStrategy.Tunnel, GetType(RoutedEventArgs), GetType(RoutedEventSample))
        SampleEvent = _
        EventManager.RegisterRoutedEvent("SampleEvent", RoutingStrategy.Bubble, GetType(RoutedEventArgs), GetType(RoutedEventSample))
    End Sub
 
    Public Custom Event PreviewSample As RoutedEventHandler
        AddHandler(ByVal value As RoutedEventHandler)
            MyBase.AddHandler(PreviewSampleEvent, value)
        End AddHandler
        RemoveHandler(ByVal value As RoutedEventHandler)
            MyBase.RemoveHandler(PreviewSampleEvent, value)
        End RemoveHandler
        RaiseEvent(ByVal sender As Object, ByVal e As System.Windows.RoutedEventArgs)
        End RaiseEvent
    End Event
 
    Public Custom Event Sample As RoutedEventHandler
        AddHandler(ByVal value As RoutedEventHandler)
            MyBase.AddHandler(SampleEvent, value)
        End AddHandler
        RemoveHandler(ByVal value As RoutedEventHandler)
            MyBase.RemoveHandler(SampleEvent, value)
        End RemoveHandler
        RaiseEvent(ByVal sender As Object, ByVal e As System.Windows.RoutedEventArgs)
        End RaiseEvent
    End Event
 
    Protected Overridable Sub OnPreviewSample(ByVal e As RoutedEventArgs)
        MyBase.RaiseEvent(e)
        e.RoutedEvent = SampleEvent
        OnSample(e)
    End Sub
 
    Protected Overridable Sub OnSample(ByVal e As RoutedEventArgs)
        MyBase.RaiseEvent(e)
    End Sub
 
    Public Sub Fire()
        Dim e As New RoutedEventArgs(PreviewSampleEvent, Me)
        Me.OnPreviewSample(e)
    End Sub
End Class
- C# -
public class RoutedEventSample : ContentControl
{
    public static readonly RoutedEvent PreviewSampleEvent;
    public static readonly RoutedEvent SampleEvent;
 
    static RoutedEventSample()
    {
        SampleEvent =
            EventManager.RegisterRoutedEvent("Sample", RoutingStrategy.Bubble, typeof(RoutedEventArgs), typeof(RoutedEventSample));
        PreviewSampleEvent =
            EventManager.RegisterRoutedEvent("PreviewSample", RoutingStrategy.Tunnel, typeof(RoutedEventArgs), typeof(RoutedEventSample));
    }
 
    public event RoutedEventHandler PreviewSample
    {
        add { base.AddHandler(PreviewSampleEvent, value); }
        remove { base.RemoveHandler(PreviewSampleEvent, value); }
    }
 
    public event RoutedEventHandler Sample
    {
        add { base.AddHandler(SampleEvent, value); }
        remove { base.RemoveHandler(SampleEvent, value); }
    }
 
    protected virtual void OnPreviewSample(RoutedEventArgs e)
    {
        base.RaiseEvent(e);
        e.RoutedEvent = SampleEvent;
        OnSample(e);
    }
 
    protected virtual void OnSample(RoutedEventArgs e)
    {
        base.RaiseEvent(e);
    }
 
    public void Fire()
    {
        RoutedEventArgs e = new RoutedEventArgs(SampleEvent, this);
        this.OnPreviewSample(e);
    }
}

ではこのイベント処理を用いた「Tunnel」「Bubble」の実装を確認する為に、コンソール出力をしてみます。

- VB -
Public Class Program
    Public Shared Sub Main()
        Dim rootElement As New RoutedEventSample()
        rootElement.Name = "RootElement"
        Dim leafElement As New RoutedEventSample()
        leafElement.Name = "LeafElement"
        Dim child1Element As New RoutedEventSample()
        child1Element.Name = "Child1Element"
        Dim child2Element As New RoutedEventSample()
        child2Element.Name = "Child2Element"
 
        rootElement.Content = leafElement
        leafElement.Content = child1Element
        child1Element.Content = child2Element
 
        rootElement.AddHandler(RoutedEventSample.PreviewSampleEvent, New RoutedEventHandler(AddressOf RoutedEventSample_PreviewSample))
        leafElement.AddHandler(RoutedEventSample.PreviewSampleEvent, New RoutedEventHandler(AddressOf RoutedEventSample_PreviewSample))
        child1Element.AddHandler(RoutedEventSample.PreviewSampleEvent, New RoutedEventHandler(AddressOf RoutedEventSample_PreviewSample))
        child2Element.AddHandler(RoutedEventSample.PreviewSampleEvent, New RoutedEventHandler(AddressOf RoutedEventSample_PreviewSample))
 
        rootElement.AddHandler(RoutedEventSample.SampleEvent, New RoutedEventHandler(AddressOf RoutedEventSample_Sample))
        leafElement.AddHandler(RoutedEventSample.SampleEvent, New RoutedEventHandler(AddressOf RoutedEventSample_Sample))
        child1Element.AddHandler(RoutedEventSample.SampleEvent, New RoutedEventHandler(AddressOf RoutedEventSample_Sample))
        child2Element.AddHandler(RoutedEventSample.SampleEvent, New RoutedEventHandler(AddressOf RoutedEventSample_Sample))
 
        child2Element.Fire()
 
        Console.ReadLine()
    End Sub
 
    Private Shared Sub RoutedEventSample_PreviewSample(ByVal sender As Object, ByVal e As RoutedEventArgs)
        Dim element As ContentControl = DirectCast(sender, ContentControl)
        Console.WriteLine("[ルーティング方式] {0}", e.RoutedEvent.RoutingStrategy.ToString())
        Console.WriteLine("イベント名 = {0}", e.RoutedEvent.Name)
        Console.WriteLine("イベントをハンドルしたオブジェクト = {0}", element.Name)
        If element.Name = "Child2Element" Then
            e.Handled = True
            Console.WriteLine("Child2Elementがイベントを処理済みにしました。")
        End If
        Console.WriteLine()
    End Sub
 
    Private Shared Sub RoutedEventSample_Sample(ByVal sender As Object, ByVal e As RoutedEventArgs)
        Dim element As ContentControl = DirectCast(sender, ContentControl)
        Console.WriteLine("[ルーティング方式] {0}", e.RoutedEvent.RoutingStrategy.ToString())
        Console.WriteLine("イベント名= {0}", e.RoutedEvent.Name)
        Console.WriteLine("イベントをハンドルしたオブジェクト = {0}", element.Name)
        Console.WriteLine()
    End Sub
End Class
- C# -
public class Program
{
    [STAThread]
    static void Main()
    {
        RoutedEventSample rootElement = new RoutedEventSample();
        rootElement.Name = "RootElement";
        RoutedEventSample leafElement = new RoutedEventSample();
        leafElement.Name = "LeafElement";
        RoutedEventSample child1Element = new RoutedEventSample();
        child1Element.Name = "Child1Element";
        RoutedEventSample child2Element = new RoutedEventSample();
        child2Element.Name = "Child2Element";
 
        rootElement.Content = leafElement;
        leafElement.Content = child1Element;
        child1Element.Content = child2Element;
 
        rootElement.AddHandler(RoutedEventSample.PreviewSampleEvent, new RoutedEventHandler(RoutedEventSample_PreviewSample));
        leafElement.AddHandler(RoutedEventSample.PreviewSampleEvent, new RoutedEventHandler(RoutedEventSample_PreviewSample));
        child1Element.AddHandler(RoutedEventSample.PreviewSampleEvent, new RoutedEventHandler(RoutedEventSample_PreviewSample));
        child2Element.AddHandler(RoutedEventSample.PreviewSampleEvent, new RoutedEventHandler(RoutedEventSample_PreviewSample));
 
        rootElement.AddHandler(RoutedEventSample.SampleEvent, new RoutedEventHandler(RoutedEventSample_Sample));
        leafElement.AddHandler(RoutedEventSample.SampleEvent, new RoutedEventHandler(RoutedEventSample_Sample));
        child1Element.AddHandler(RoutedEventSample.SampleEvent, new RoutedEventHandler(RoutedEventSample_Sample));
        child2Element.AddHandler(RoutedEventSample.SampleEvent, new RoutedEventHandler(RoutedEventSample_Sample));
 
        child2Element.Fire();
 
        Console.ReadLine();
    }
 
    private static void RoutedEventSample_PreviewSample(object sender, RoutedEventArgs e)
    {
        ContentControl element = (ContentControl)sender;
        Console.WriteLine("[ルーティング方法] {0}", e.RoutedEvent.RoutingStrategy.ToString());
        Console.WriteLine("イベント名 = {0}", e.RoutedEvent.Name);
        Console.WriteLine("イベントをハンドルしたオブジェクト = {0}", element.Name);
        if (element.Name == "Child2Element")
        {
            e.Handled = true;
            Console.WriteLine("Child2Elementによって処理済みとされました。");
        }
        Console.WriteLine();
    }
 
    private static void RoutedEventSample_Sample(object sender, RoutedEventArgs e)
    {
        ContentControl element = (ContentControl)sender;
        Console.WriteLine("[ルーティング方法] {0}", e.RoutedEvent.RoutingStrategy.ToString());
        Console.WriteLine("イベント名 = {0}", e.RoutedEvent.Name);
        Console.WriteLine("イベントをハンドルしたオブジェクト = {0}", element.Name);
        Console.WriteLine();
    }
}

この出力結果はこのようになります。

[ルーティング方法] Tunnel
イベント名 = PreviewSample
イベントをハンドルしたオブジェクト = RootElement

[ルーティング方法] Tunnel
イベント名 = PreviewSample
イベントをハンドルしたオブジェクト = LeafElement

[ルーティング方法] Tunnel
イベント名 = PreviewSample
イベントをハンドルしたオブジェクト = Child1Element

[ルーティング方法] Tunnel
イベント名 = PreviewSample
イベントをハンドルしたオブジェクト = Child2Element
Child2Elementによって処理済みとされました。

上記例では「Tunnel」方法でイベントを伝達し、途中でイベント処理をマークする事によって「Bubble」方法のイベントの発生を止めている例です。
このようにイベント処理を用いて、関連のあるイベントではイベント情報(RoutedEventArgsクラス)を共有する事によってルーティングイベントを制御します。


いままでの取り上げた例で基本的なルーティングイベントの理解・実装までを出来るようになるかと思います。
ルーティングイベントはWPFで提供されている基本コントロールでは沢山実装されております。
使用するイベントがどのようなルーティング方法で、どのような動作をする事を理解する事はコントロールを使用する上でとても重要な事になります。
今回は少し長くなりましたが、「ルーティングイベント」についてでした。

to be contine・・・
コメントの入力
タイトル
名前
Url
コメント