When are tunneling and bubbling events useful in WPF? - c#

I understand how bubbling and tunneling works. However, i'm confused about using them.
Here is why:
I want to handle a mouse click event. To bubble it, there is MouseDown and, to tunnel it, there is PreviewMouseDown. However, MouseDown doesn't necessarily mean the user clicked the control. May be the user pressed the button and moved away from it to cancel the click.
I wouldn't want to change anything if the button is not being clicked.
So my question is, how are the Bubbling/Tunneling strategies useful?

If the event is listed RoutedEventArgs, then it's routed event. Routed events support a RoutingStrategy of Bubble, Tunnel, or Direct. Let's take a look at the event handler of Button.Click:
private void Grid_Click(object sender, RoutedEventArgs e)
{
MessageBox.Show("Button Test clicked!");
}
There specified RoutedEventArgs, so it's routed event. Because the preview were not specified in the name, therefore this Bubble event. This can be demonstrated in the following way:
<Grid ButtonBase.Click="Grid_Click">
<Button Name="TestButton" Width="100" Height="30" Content="Test" />
</Grid>
When you click on the TestButton, the event is to rise above the Grid, and displays a message:
Button Test clicked!
Usefulness of Bubbling/Tunneling strategies
Tunneling
Many of the standard controls listen to events, such as KeyDown, MouseDown, etc. For example -DataGrid control. I want by pressing the enter key the function was called adding a record. But DataGrid already has KeyDown event, so the event is not raised. So you have to do your logic in the Tunnel event - PreviewKeyDown, it will work before the KeyDown event. The same applies to RichTextBoxControl.
Bubbling
Sometimes, you need a global handler for a specific event, so it worked for all controls in VisualTree. Naturally, the a direct event you can not do it. Hence on the stage comes Bubbling event.
Another reason is the ideology of the WPF. This Button can contain anything: Image, another Button, etc:
The user can click on the TextBlock/Image in the Button. How do we know that the click was in Button? That's right, with the help of Bubbling event.
For more information, please see:
Understanding Routed Events and Commands In WPF
Edit
I changed little bit a Click handler:
private void Grid_Click(object sender, RoutedEventArgs e)
{
String message = "#" + eventCounter.ToString() + ":\r\n" +
" Sender: " + sender.ToString() + ":\r\n" +
" Source: " + e.Source + ":\r\n" +
" Original Source: " + e.OriginalSource;
lstEvents.Items.Add(message);
}
Result of click on the Button:

Hi though you can get some good articles regading this on net but still I will try to answer this.
Suppose you give a button a very plain appearance consisting of a single Rectangle, and provide a simple piece of text as the content Even with such basic visuals, there are still two elements present: the text and the rectangle.The button should respond to a mouse click whether the mouse is over the text or the rectangle. In the standard .NET event handling model, this would mean registering a MouseLeftButtonUp event handler for both elements.
This problem would get much worse when taking advantage of WPF’s content
model. A Button is not restricted to having plain text as a caption—it can contain any
object as content. The xaml below is not especially ambitious, but even
this has six visible elements: the yellow outlined circle, the two dots for the eyes, the
curve for the mouth, the text, and the button background itself. Attaching event
handlers for every single element would be tedious and inefficient. To work MouseDown We would have to add 8 MouseDownEvents to this bit of code.
<Button PreviewMouseDown="PreviewMouseDownButton" MouseDown="MouseDownButton">
<Grid PreviewMouseDown="PreviewMouseDownGrid" MouseDown="MouseDownGrid">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Canvas PreviewMouseDown="PreviewMouseDownCanvas" MouseDown="MouseDownCanvas" Width="20" Height="18" VerticalAlignment="Center">
<Ellipse PreviewMouseDown="PreviewMouseDownEllipse" MouseDown="MouseDownEllipse" x:Name="myEllipse" Canvas.Left="1" Canvas.Top="1" Width="16" Height="16" Fill="Yellow" Stroke="Black" />
<Ellipse Canvas.Left="4.5" Canvas.Top="5" Width="2.5" MouseDown="MouseDownEllipse" Height="3" Fill="Black" />
<Ellipse Canvas.Left="11" Canvas.Top="5" Width="2.5" MouseDown="MouseDownEllipse" Height="3" Fill="Black" />
<Path Data="M 5,10 A 3,3 0 0 0 13,10" Stroke="Black" MouseDown="Path_MouseDown_1"/>
</Canvas>
<TextBlock Grid.Column="1" MouseDown="TextBlock_MouseDown_1">Click!</TextBlock>
</Grid>
</Button>
WPF uses RoutedEvents Bubble/Tunnel /Normal, which are rather more thorough than normal events. Instead
of just calling handlers attached to the element that raised the event, WPF walks the
tree of user interface elements, calling all handlers for the routed event attached to
any node from the originating element right up to the root of the user interface tree.

As you said you know the concept of bubbling/tunneling so not going into that. But this is what these events are intended for i.e they let you know if the Mouse button was Down or Up on the control or its children.Events are working fine.For your scenario you should use the Click event of your button which tell if the mouse was down and up on the button itself.
Thanks

Related

Weird event behavior in a Popup inside a DataGridColumnHeader

I'm not using any external libraries here, just plain WPF.
I have a DataGrid with a custom DataGridColumnHeader. This column header contains a ToggleButton to toggle a Popup. Inside the popup there is a TextBox. The problem I'm having is that doubleclicking inside the TextBox raises the MouseDoubleClick event on the DataGrid. Here's a simplified version containing numbered comments I will refer to afterwards
<Window x:Class="PopupsAreWeird.MainWindow"
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"
xmlns:local="clr-namespace:PopupsAreWeird" xmlns:b="http://schemas.microsoft.com/xaml/behaviors"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.Resources>
<Style TargetType="DataGridColumnHeader">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="DataGridColumnHeader">
<!-- 1) This eventhandler is never called -->
<Grid Control.MouseDoubleClick="Grid_MouseDoubleClick_1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ContentPresenter Grid.Column="0" />
<ToggleButton x:Name="openToggle" Grid.Column="1" Content="Open" />
<Popup IsOpen="{Binding ElementName=openToggle, Path=IsChecked}" StaysOpen="True">
<!-- 2) This eventhandler is always called, and the problem I am having is there regardless of whether I set e.Handled = true in this handler or not -->
<TextBox Width="200" MouseDoubleClick="TextBox_MouseDoubleClick" />
</Popup>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<StackPanel Orientation="Vertical">
<!-- 3) This eventhandler is always called, but never should be -->
<DataGrid MouseDoubleClick="DataGrid_MouseDoubleClick">
<DataGrid.Columns>
<DataGridTextColumn Header="Header 1" Width="200" />
</DataGrid.Columns>
</DataGrid>
</StackPanel>
</Window>
1) I do not understand why this handler is never called. This was meant to set e.Handled to true to stop bubbling upwards to the DataGrid. While Grid does not have a definition for MouseDoubleClick, it is my understanding that I can attach eventhandlers for any event to any element (like attaching ButtonBase.Click to a Panel element). Is this not true, or is there any special case here I am missing?
3) I want to avoid the double click event bubbling to here, but even if I set e.Handled to true in eventhandler 2, this eventhandler is called, and in that handler, e.Handled is false. I assume the reason is that the Popup is defined inside of DataGridColumnHeader, and for some weird reason 2 events are being raised, one for the tree of the popup, and one for the tree that contains the Popup element, but that seems a bit nonsensical.
I know Popup is a sort of weird thing in WPF, but this seems like I'm missing something obvious. Is there any way to achieve what I want, i.e. not have events (or at least the MouseDoubleClick event) bubble up to the DataGrid?
Thanks in advance,
David
1) You are right, You can attach a handler for a routed event to any UIElement. But indeed the Popup is a special case. The Popup is a special control that behaves like Window. It can popup everywhere on the screen, always rendered top-most and is not necessarily bound to the application itself. That's why its visual tree is detached from the application's visual tree. Popup.Child will be a separate isolated visual tree. Microsoft Docs: Popup and the Visual Tree.
Since routed events traverse the visual tree to be handled by any node, it makes sense that bubbling/tunneling routed events inside the Popup will stop/start at the root of this isolated tree. So routed events that are originated in the Popup cannot be handled outside the Popup.
3) Short version: Control.MouseDoubleClick (and the preview version) is a special event that behaves different. This event is raised on each UIElement on the route when the event traverses the visual tree. So setting Handled to true has no effect.
To solve your problem, you should either handle UIElement.PreviewMousLeftButtonDown and check if the MouseButtonEventArgs.ClickCount equals 2 to detect a double click and then set Handled = true
or check if the type of the sender or RoutedEventArgs.Source is not TextBox before handling it (explicit event filtering).
"Although this routed event seems to follow a bubbling route through
an element tree, it actually is a direct routed event that is raised
along the element tree by each UIElement. If you set the Handled
property to true in a MouseDoubleClick event handler, subsequent
MouseDoubleClick events along the route will occur with Handled set to
false. This is a higher-level event for control consumers who want to
be notified when the user double-clicks the control and to handle the
event in an application.
Control authors who want to handle mouse double clicks should use the
MouseLeftButtonDown event when ClickCount is equal to two. This will
cause the state of Handled to propagate appropriately in the case
where another element in the element tree handles the event.
The Control class defines the PreviewMouseDoubleClick and
MouseDoubleClick events, but not corresponding single-click events. To
see if the user has clicked the control once, handle the MouseDown
event (or one of its counterparts) and check whether the ClickCount
property value is 1."
Microsoft Docs: Control.MouseDoubleClick

Flyout's "Light Dismissal" Causing 2 Taps to be Required to Open Next Flyout

I have two buttons that show Flyouts when clicked. I am displaying them the same way as demonstrated in the XAML UI Basics sample:
private void ButtonTapped(object sender, TappedRoutedEventArgs e)
{
FrameworkElement element = sender as FrameworkElement;
if (element != null)
{
FlyoutBase.ShowAttachedFlyout(element);
}
}
My problem is that if Button 1's flyout is open, the next tap on the screen closes the flyout. This is fine, but if the next tap happens to be on Button 2, I want the button's tap event to be fired and open its flyout. Instead, the button doesn't register a tap at all and closes Button 1's flyout.
This results in needing to tap two times - one to dismiss the Button 1's flyout, and a second to show Button 2's flyout.
In other words:
Current Flow:
Tap Button 1
Button 1's Flyout is opened
Tap Button 2
Button 1's flyout is closed, (Button 2, nor Page registers the tap)
Tap Button 2
Now Button 2's flyout is opened
What I'm Looking For:
Tap Button 1
Button 1's Flyout is opened
Tap Button 2
Button 1's flyout is closed, Button 2's flyout opens.
How can I do this? I've tried intercepting the Tapped event for the page, but when the flyout is open, it seems to intercept the Tapped event so it can be used for the Flyout's light dismissal
Would overriding the style of the Flyout, or the FlyoutPresenterStyle help me here? Or perhaps opening the Flyout in a more MVVM-ish way which would allow me for finer control on how the Flyout is opened/closed?
I'm not sure how to get around this!
Here is something I found on Microsoft documents:
When dismissing with a tap, this gesture is typically absorbed and not passed on to the UI underneath. For example, if there’s a button visible behind an open flyout, the user’s first tap dismisses the flyout but does not activate this button. Pressing the button requires a second tap.
You can change this behaviour by designating the button as an input pass-through element for the flyout. The flyout will close as a result of the light dismiss actions described above and will also pass the tap event to its designated OverlayInputPassThroughElement. Consider adopting this behaviour to speed up user interactions on functionally similar items. If your app has a favourites collection and each item in the collection includes an attached flyout, it's reasonable to expect that users may want to interact with multiple flyouts in rapid succession.
[!NOTE] Be careful not to designate an overlay input pass-through element which results in a destructive action. Users have become habituated to discreet light dismiss actions which do not activate primary UI. Close, Delete or similarly destructive buttons should not activate on light dismiss to avoid the unexpected and disruptive behaviour.
In the following example, all three buttons inside FavoritesBar will be activated on the first tap.
<Page>
<Page.Resources>
<Flyout x:Name="TravelFlyout" x:Key="TravelFlyout"
OverlayInputPassThroughElement="{x:Bind FavoritesBar}">
<StackPanel>
<HyperlinkButton Content="Washington Trails Association"/>
<HyperlinkButton Content="Washington Cascades - Go Northwest! A Travel Guide"/>
</StackPanel>
</Flyout>
</Page.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel x:Name="FavoritesBar" Orientation="Horizontal">
<HyperlinkButton x:Name="PageLinkBtn">Bing</HyperlinkButton>
<Button x:Name="Folder1" Content="Travel" Flyout="{StaticResource TravelFlyout}"/>
<Button x:Name="Folder2" Content="Entertainment" Click="Folder2_Click"/>
</StackPanel>
<ScrollViewer Grid.Row="1">
<WebView x:Name="WebContent"/>
</ScrollViewer>
</Grid>
private void Folder2_Click(object sender, RoutedEventArgs e){
Flyout flyout = new Flyout();
flyout.OverlayInputPassThroughElement = FavoritesBar;
flyout.ShowAt(sender as FrameworkElement);}

Popup with "StaysOpen=false" steals LeftMouseButtonDown event

In my application, when a user attempts to click a slider which is on the main window, while a popup control is open, the popup control steals the mouse down event.
This results in the slider not responding to the mouse down event correctly.
(it seems to get focus and move to an incorrect location)
I found that the that the "OnPreviewMouseLeftButtonDown" in the slider does not fire when popup's "StaysOpen" property is false (and the popup is open),
and does fire when its true (or when the popup is closed).
I was wondering if someone has found a solution for this issue.
I encountered these type of issues in other controls in my application in various contexts, So I would prefer a more general solution rather than just solving this for the slider.
Sample code:
<Window x:Class="SampleApplication.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Name="Root"
Title="MainWindow" Height="350" Width="525">
<Grid Height="130" Width="300">
<Button Width="40" Height="40" Click="ButtonBase_OnClick" HorizontalAlignment="Left" VerticalAlignment="Top"></Button>
<Popup StaysOpen="False" IsOpen="{Binding ElementName=Root, Path=IsOpen}" Width="100" Height="100"
HorizontalAlignment="Center" VerticalAlignment="Center" Placement="Center">
<Grid Background="Black">
<TextBlock Text="hello"></TextBlock>
</Grid>
</Popup>
<Slider Width="200" IsMoveToPointEnabled="True" VerticalAlignment="Bottom"></Slider>
</Grid>
Thanks ahead,
Yotam
This happens because PreviewMouseDown (and it's derivates) (from the base class UIElement) has a default RoutingStrategy.Direct.
Direct - The routed event does not route through an element tree, but does support other routed event capabilities such as class handling, EventTrigger or EventSetter.
This is the source code of the event taken from ReferenceSource.
public static readonly RoutedEvent PreviewMouseLeftButtonDownEvent =
EventManager.RegisterRoutedEvent(
"PreviewMouseLeftButtonDown",
RoutingStrategy.Direct,
typeof(MouseButtonEventHandler),
_typeofThis);
And here is what happens in the Popup:
private void OnPreviewMouseButton(MouseButtonEventArgs e)
{
// We should only react to mouse buttons if we are in an auto close mode (where we have capture)
if (_cacheValid[(int)CacheBits.CaptureEngaged] && !StaysOpen)
{
Debug.Assert( Mouse.Captured == _popupRoot.Value, "_cacheValid[(int)CacheBits.CaptureEngaged] == true but Mouse.Captured != _popupRoot");
// If we got a mouse press/release and the mouse isn't on the popup (popup root), dismiss.
// When captured to subtree, source will be the captured element for events outside the popup.
if (_popupRoot.Value != null && e.OriginalSource == _popupRoot.Value)
{
// When we have capture we will get all mouse button up/down messages.
// We should close if the press was outside. The MouseButtonEventArgs don't tell whether we get this
// message because we have capture or if it was legit, so we have to do a hit test.
if (_popupRoot.Value.InputHitTest(e.GetPosition(_popupRoot.Value)) == null)
{
// The hit test didn't find any element; that means the click happened outside the popup.
SetCurrentValueInternal(IsOpenProperty, BooleanBoxes.FalseBox);
}
}
}
}
So it was designed to work this way, and you should likely not use OnPreviewMouseDown for whatever you are trying to accomplish here.
In my application, when a user attempts to click a slider which is on the main window, while a popup control is open, the popup control steals the mouse down event
While your description is not completely correct, that is the normal behaviour of any Popup control. The reason that this occurs is because the Popup control has focus and so it is listening out for the Click event even if it occurs outside the bounds of the Popup. Think about this logically now... if it didn't do this, how would it know when to close? You will find the same behaviour from the Popup control used in a ComboBox.
There is a workaround to achieve the behavior you require, Set 'IsHitTestVisible = True' for the Slider control you are use.
PS:
Set IsHitTestVisible = True, only when the Popup is Open - False otherwise.

MouseLeftButtonUp does not fire

I have a Button
<Button>
<Button.Template>
<ControlTemplate>
<StackPanel>
<Image Source="share.png"
MouseLeftButtonUp="Image_MouseLeftButtonUp"
MouseLeftButtonDown="Image_MouseLeftButtonDown" />
</StackPanel>
</ControlTemplate>
</Button.Template>
</Button>
But the problem is that, unlike MouseLeftButtonDown, the event MouseLeftButtonUp does not fire. Why? How can I solve it? Thanks.
Probably because the Button is handling the event already, so it doesn't filter down to the Image control.
Why are you handling the MoustUp and MouseDown events on the Image? Why not handle them on the Button?
EDIT
After a quick look at the documentation for this event, I see that the routing strategy for MouseLeftButtonUp is Direct, but the actual underlying event is the MouseUp event which has a Bubbling strategy. So, in effect, MouseLeftButtonUp should have a de-facto bubbling strategy, meaning the Image should have the opportunity to handle the event before the Button.
Sounds like something else might be going on. If you set the Image to nothing on the Image control, and the background is null (different from Color.Transparent), then WPF will treat the control as not "existing" as far as mouse events go.
EDIT 2
Hmm... maybe my original guess was correct afterall. According to this thread:
http://social.msdn.microsoft.com/forums/en/wpf/thread/e231919d-9fef-4aa5-9dcb-2c1eb68df25b/#306db880-ed50-45d2-819f-88d76f7148c1
Button is handling the MouseUp event. Try using PreviewMouseUp and then checking to see which button was clicked.
EDIT 3
Ok... I think I figured it out. I'm betting that the Button is "capturing" the Mouse on the MouseDown event. When this occurs, other controls will not receive MouseUp or MouseDown events (include the Preview versions) until the Mouse has been released.
See: http://books.google.com/books?id=nYl7J7z3KssC&pg=PA146#v=twopage&q&f=true
The Click event is handling the LeftMouseUp event as Cyborgx37 said.
You can use the PreviewMouseLeftButtonUp event instead of MouseLeftButtonUp, on Button as you can see below:
<Button PreviewMouseLeftButtonUp="Button_PreviewMouseLeftButtonUp">
<Button.Template>
<ControlTemplate>
<StackPanel>
<Image Source="..." />
</StackPanel>
</ControlTemplate>
</Button.Template>
</Button>
Your Button template only has an Image, I guess you don't need this Button, you can use as above, so you can capture the MouseLeftUp event.
<Image MouseLeftButtonUp="Image_MouseLeftButtonUp" Source="..." />
This way is so much easier, but it depends on what you need.
Btw, sorry for my crap english, I hope it could help you.
Try using the event PreviewMouseLeftButtonUp.

WPF - Capture an event from a control that is not on top

I have a WPF application that I am working on where there is a button that is obscured by a partially opaque rectangle overlay. The button is still visible, but it can not receive any events because they are all caught by the rectangle which is on top of it.
Is there any way to set a pass-through so that the event is received by the next visual item underneath? If not is there some other workaround that could be used in this situation?
Set IsHitTestVisible="false" on the opaque overlay.
You need to set IsHitTestVisible="False" for the control over your button.
This example shows that a button is covered by a border, but the border doesn't get any event since because of the IsHitTestVisible="False" condition of border:
<Grid Background="Yellow">
<Button Click="Button_Click" Width="100" Height="25"/>
<Border Background="Cyan" Opacity="0.4" Width="200" Height="200" IsHitTestVisible="False" />
</Grid>
C# code,
private void Button_Click(object sender, RoutedEventArgs e)
{
MessageBox.Show("iiiiiii");
}
You could add the your own event to the rectangle event (or in the click event of the rectangle it self) and check there if it's with in buttons area
Rectangle.Click += your_click_event;
private void your_click_event(object sender, RoutedEventArgs e)
{
//check if it's coordinates are within the underlining button.
//fire button click event
}
But it would be more convient to set
IsHitTestVisible="False"
Like mentioned in other posts. UIElement.IsHitTestVisible Property
I would recommend to look in to routed events in WPF. Routed events get routed based primarily on the visual tree. Routed events support a RoutingStrategy of Bubble, Tunnel, or Direct.
Understanding Routed Events and Commands In WPF

Categories

Resources