背景
Gridに配置したButtonなどのコントロールを含む全てのGrid内の領域で同一のイベントハンドラーが実行されるようにしたい時どうすればいいのか分からず、結構調べたのでまとめておきます。
間違いなどあればご指摘お願いします。
修正前のコード
Grid内どこでもクリックすると、「Grid_MouseDown」と書かれたメッセージボックスを表示させたいといったサンプル。
<Grid x:Name="Grid" MouseDown="Grid_MouseDown" IsHitTestVisible="True" Background="Transparent">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Button x:Name="Button1" Click="Button_Click"
Content="Click me!" Grid.Row="0" Grid.Column="0"/>
<Button x:Name="Button2" Click="Button_Click"
Content="Click me!" Grid.Row="1" Grid.Column="0"/>
</Grid>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void Grid_MouseDown(object sender, MouseButtonEventArgs e)
{
if (sender is Grid item)
MessageBox.Show(item.Name + "_MouseDown");
}
private void Button_Click(object sender, RoutedEventArgs e)
{
if (sender is Control item)
MessageBox.Show(item.Name + "_Click");
}
}
上記のサンプルでは、Buttonがないところでクリックした場合は、「Grid_MouseDown」のメッセージボックスが表示されます。
しかし、Buttonをクリックした時は、「Button1_Click」または「Button2_Click」のメッセージボックスしか表示されず、「Grid_MouseDown」のメッセージボックスが表示されません。
修正後のコード
__GridにButtonBase.ClickイベントにGrid_MouseDownイベントハンドラーの登録を追加する__と、Buttonをクリック時でもGridに登録したイベントハンドラーが実行されるようになります。
<!--修正前-->
<!--<Grid x:Name="Grid" MouseDown="Grid_MouseDown" IsHitTestVisible="True" Background="Transparent">-->
<!--修正後-->
<Grid x:Name="Grid" MouseDown="Grid_MouseDown" ButtonBase.Click="Grid_MouseDown"
IsHitTestVisible="True" Background="Transparent">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Button x:Name="Button1" Click="Button_Click"
Content="Click me!" Grid.Row="0" Grid.Column="0"/>
<Button x:Name="Button2" Click="Button_Click"
Content="Click me!" Grid.Row="1" Grid.Column="0"/>
</Grid>
GridのMouseDownとButtonBase.Clickのイベントハンドラーは同じGrid_MouseDownを使いますのでxaml.csのコードは変更不要です。
Buttonをクリックした時は、「Button1_Click」または「Button2_Click」のメッセージボックスしか表示された後、「Grid_MouseDown」のメッセージボックスが表示されるようになりました。
イベントについて
イベントはTunnelとBubbleがあります。
PreViewMouseDownなどPreViewがTunnel、ClickやMouseDownなどはBubbleになっています。
PreViewMouseDownとMouseDownイベントが実行される順番は
- Grid.PreViewMouseDown
- Button.PreViewMouseDown
- Button.MouseDown
- Grid.MouseDown
となります。
つまり、修正前のコードでは先に3. Button.MouseDownが実行されます。これは意図通りの挙動だといえます。
何故、4. Grid.MouseDownが実行されないかというと、処理済のRoutedEventは実行されなくなるためです。
そこで、修正後のコードのようにGridにButtonBase.Clickにイベントハンドラーを登録することで、添付イベントとしてButtonのクリック時にもGridで定義したイベントハンドラーが実行されるようになります。
GridのButtonBase.ClickのEventHandlerをコードビハインドに置きたくない場合
ButtonBase.Clickは本来Gridには定義されていないRotedEventなので、添付イベントとして処理する方法で実現します。
UIElement.AddHandlerを使ってイベントハンドラーを登録することができます。
今回はコードビハインドにイベントハンドラーを置きたくないので、イベントハンドラーをBehaiviorに移動してButtonBase.ClickのRoutedEventHandlerとして登録しましょう。
<Grid x:Name="Grid" IsHitTestVisible="True" Background="Transparent">
<!--<Grid x:Name="Grid" MouseDown="Grid_MouseDown" ButtonBase.Click="Grid_MouseDown" IsHitTestVisible="True" Background="Transparent">-->
<i:Interaction.Behaviors>
<b:GridBehaivior />
</i:Interaction.Behaviors>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Button x:Name="Button1" Click="Button_Click"
Content="Click me!" Grid.Row="0" Grid.Column="0"/>
<Button x:Name="Button2" Click="Button_Click"
Content="Click me!" Grid.Row="1" Grid.Column="0"/>
</Grid>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
/* Behaiviorに移動
private void Grid_MouseDown(object sender, MouseButtonEventArgs e)
{
if (sender is Grid item)
MessageBox.Show(item.Name + "_MouseDown");
}
*/
private void Button_Click(object sender, RoutedEventArgs e)
{
if (sender is Control item)
MessageBox.Show(item.Name + "_Click");
}
}
public class GridBehaivior : Behavior<Grid>
{
private RoutedEventHandler routedEventHandler;
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.MouseDown += AssociatedObject_MouseDown;
// ButtonBase.ClickイベントのRoutedEventHandlerを登録する
routedEventHandler = new RoutedEventHandler(AssociatedObject_MouseDown);
AssociatedObject.AddHandler(ButtonBase.ClickEvent, routedEventHandler);
}
protected override void OnDetaching()
{
base.OnDetaching();
AssociatedObject.MouseDown -= AssociatedObject_MouseDown;
AssociatedObject.RemoveHandler(UIElement.MouseDownEvent, routedEventHandler);
}
private void AssociatedObject_MouseDown(object sender, RoutedEventArgs e)
{
if (sender is Grid item)
MessageBox.Show(item.Name + "_MouseDown");
}
}
Button以外のUIElementがある場合でもEventHandlerが実行したい場合
UIElement.MouseUpのイベントハンドラーとして登録します。
MouseDownのイベントハンドラーとして登録すると、Button.Clickより先にButton.MouseDownが実行されるため処理済のRoutedEvent (Button.Click) は実行されなくなるためです。
マウスイベントは以下の順で実行されます。
MouseDown→MouseHover→Click→MouseClick→MouseUp→MouseCaptureChanged
(車輪の再発明C# マウスイベントより)
そのためButton.Clickより後に来るMouseUpのイベントハンドラーとして登録し、UIElement.AddHandler メソッドの handledEventsToo をtrueにすることで処理済のRoutedEventが実行されるようにイベントハンドラーを登録してあげます。
AddHandler(RoutedEvent, Delegate, Boolean)
指定したルーティング イベントのルーティング イベント ハンドラーを追加します。このハンドラーは、現在の要素のハンドラー コレクションに追加されます。 イベント ルート上の別の要素により既にハンドル済みとしてマークされているルーティング イベントに対し、指定したハンドラーが呼び出されるようにするには、handledEventsToo を true に指定します。
public class GridBehaivior:Behavior<Grid>
{
private RoutedEventHandler routedEventHandler;
protected override void OnAttached()
{
base.OnAttached();
//AssociatedObject.MouseDown += AssociatedObject_MouseDown;
//AssociatedObject.AddHandler(ButtonBase.ClickEvent, new RoutedEventHandler(AssociatedObject_MouseDown));
// UIElement.MouseUpEventイベントのRoutedEventHandlerを登録する
routedEventHandler = new RoutedEventHandler(AssociatedObject_MouseDown);
AssociatedObject.AddHandler(UIElement.MouseUpEvent, routedEventHandler, true);
}
protected override void OnDetaching()
{
base.OnDetaching();
//AssociatedObject.MouseDown -= AssociatedObject_MouseDown;
AssociatedObject.RemoveHandler(UIElement.MouseDownEvent, routedEventHandler);
}
private void AssociatedObject_MouseDown(object sender, RoutedEventArgs e)
{
if (sender is Grid item)
MessageBox.Show(item.Name + "_MouseDown" + Environment.NewLine +
"OriginalSource: " + e.OriginalSource);
}
}
参考
http://csharphelper.com/blog/2015/03/understand-event-bubbling-and-tunneling-in-wpf-and-c/
https://blog.okazuki.jp/entry/2014/08/22/211021
http://codingseason.blogspot.com/2012/09/events-in-wpf.html