Event bubbling and RoutedEventArgs Source Property - c#

I am trying to understand the RoutedEventArgs.Source property in a simple WPF application. Here is the XAML Code
<Window x:Class="BubbleDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<StackPanel x:Name="stackPanel1" Button.Click="OnOuterButtonClick">
<Button x:Name="button1" Content="Button 1" Margin="5" />
<Button x:Name="button2" Margin="5" Click="OnButton2">
<ListBox x:Name="listBox1">
<Button x:Name="innerButton1" Content="Inner Button 1" Margin="4" Padding="4" Click="OnInner1" />
<Button x:Name="innerButton2" Content="Inner Button 2" Margin="4" Padding="4" Click="OnInner2" />
</ListBox>
</Button>
<ListBox ItemsSource="{Binding}" />
</StackPanel>
</Window>
And here is the code behind
using System;
using System.Collections.ObjectModel;
using System.Windows;
namespace BubbleDemo
{
public partial class MainWindow : Window
{
private ObservableCollection<string> messages = new ObservableCollection<string>();
public MainWindow()
{
InitializeComponent();
this.DataContext = messages;
}
private void AddMessage(string message, object sender, RoutedEventArgs e)
{
messages.Add(String.Format("{0}, sender: {1}; source: {2}; original source: {3}",
message, (sender as FrameworkElement).Name,
(e.Source as FrameworkElement).Name,
(e.OriginalSource as FrameworkElement).Name));
}
private void OnOuterButtonClick(object sender, RoutedEventArgs e)
{
AddMessage("outer event", sender, e);
}
private void OnInner1(object sender, RoutedEventArgs e)
{
AddMessage("inner1", sender, e);
}
private void OnInner2(object sender, RoutedEventArgs e)
{
AddMessage("inner2", sender, e);
e.Handled = true;
}
private void OnButton2(object sender, RoutedEventArgs e)
{
AddMessage("button2", sender, e);
e.Source = sender;
}
}
}
When I click on InnerButton1 the click event is raised and then is executed the OnInner1 handler.
After is executed the OnButton2 Handler which sets the RoutedEventArgs.Source property with the sender parameter.
If you build and execute this code, you can see the output results.
When the event arrives on the OnOuterButtonClick handler, the output in the bottom ListBox should be:
inner1, sender: innerButton1; source: innerButton1; original source: innerButton1
button2, sender: button2; source: innerButton1; original source: innerButton1
outer event, sender: stackPanel1; source: button2; original source: innerButton1
but the output is this
inner1, sender: innerButton1; source: innerButton1; original source: innerButton1
button2, sender: button2; source: innerButton1; original source: innerButton1
outer event, sender: stackPanel1; source: innerButton1; original source: innerButton1
The RoutedEventArgs.Source property reassigned in the OnButton2 hander is changed but returns to reference innerButton1 within the OnOuterButtonClick handler.
Why this happens?
Thanks

This is a really good question, and i had to look into the Source of .net to figure out why it is like this:
The Source Property looks like this:
public object Source
{
get {return _source;}
set
{
if (UserInitiated && InvokingHandler)
throw new InvalidOperationException(SR.Get(SRID.RoutedEventCannotChangeWhileRouting));
...
}
}
This execption s thrown, whenever the User tries to SET the source, while the event is Bubbling or tunneling.
I'm assuming, that the part of the .net Framework, taking care for this behavior is also catching the Exception, so you don't get aware of the problem. In fact, when trying to set the Source Property, while the event is bubbling, the Debugger shows, that is not changed right after setting it.
Unfortunately the source code just shows that Microsoft does not allow to change the Source-Property while the Event is bubbling (or tunneling), but not why.
If you - for whatever Reason - need to get information about the Prior handler that processed an event, you could create your own Extension of RoutedEventArgs and add another property, containing this information.
finally you can extend the button class, and raise your own Event, that contains the appropriate RoutedEventArgsWithHandlerHistory Object :)

This is an interesting question and demanded to reflect the .net Routing engine. So what I found is each UIElement uses RaiseEvent() method to initiate the RoutedEvent. While doing so it first builds the EventRoute. While build EventRoute, it creates the list of Invoke handlers depending on the RoutingStrategy i.e for Bubble and Tunnel it goes up and down the VisualTree to which UIElement belongs and finds out how many Handlers are attached to the given RoutedEvent. As apparent, in your case for innerButton1 and innerButton1 there are three handlers.
Now UIElement got the EventRoute for its RoutedEvent, next it calls InvokeHandlers() on EventRoute. While calling the handlers in the loop, InvokeHandler reset the args.Source to the original value it has like below where it is doing it for the Bubble strategy.
for (int index = 0; index < this._routeItemList.Count; ++index)
{
if (index >= endIndex)
{
object bubbleSource = this.GetBubbleSource(index, out endIndex);
if (!reRaised)
args.Source = bubbleSource ?? source;
}
Hence before each handler call, the Source is reset to its original value therefore changing it inside any handler will not be passed to the next handler.

Related

How to get Button as Parent on MenuItemFlyoutItem_Click?

I'm building a WinRT Universal app and I have a button and a MenuFlyout attached to it - I'm trying to get the Name and Tag of the button.
XAML:
<MenuFlyout x:Key="FlyOutResource">
<MenuFlyoutItem Text="pin to start" Click="PinToStart_Click"/>
</MenuFlyout>
<Button x:Name="ButtonName" Tag="BUTTON TAG" FlyoutBase.AttachedFlyout="{StaticResource FlyOutResource}" Holding="Button_Holding"/>
C#:
private void Button_Holding(object sender, Windows.UI.Xaml.Input.HoldingRoutedEventArgs e)
{
FlyoutBase.ShowAttachedFlyout(sender as FrameworkElement);
}
private void PinToStart_Click(object sender, Windows.UI.Xaml.RoutedEventArgs e)
{
var menuFlyoutItem = sender as MenuFlyoutItem;
if (menuFlyoutItem != null)
{
????
}
}
How do I get the name of the button (of which the FlyOut is attached to)? DataContext doesn't work.
Kind regards,
Niels
I am in UWP but am hitting the same issue and do not see a way around it from within the PinToStart_Click Handler.
In your solution, you at least see which button opened the flyout within the Button_Holding handler. I'd recommend storing a reference to the sender in the Button_Holding handler and then access that reference within the PinToStart click handler.
var tempParent = Windows.UI.Xaml.Media.VisualTreeHelper.GetParent(child as FrameworkElement);
I hope this will work for you.
For future reference, I leave a solution
If you name your MenuFlyout:
<Button.Flyout>
<MenuFlyout x:Name="MenuFlyoutContainer">
<MenuFlyoutItem Tapped="OnMenuFlyoutItem"/>
</MenuFlyout>
</Button.Flyout>
Then on the tapped event you can search for it:
private void OnDeletePressed(object sender, TappedRoutedEventArgs e)
{
var item = (sender as MenuFlyoutItem);
var itemDataContext = item.DataContext;
FrameworkElement parent = (item.FindName("MenuFlyoutContainer") as MenuFlyout);
}
And voila you have your MenuFlyout instance.
Tested inside DataTemplates on UWP.

Silverlight MouseLeave not firing

Considering that piece of code :
XAML:
<Grid x:Name="LayoutRoot">
<Border x:Name="brd1" Height="100" Width="100" Background="Blue"
MouseLeftButtonUp="brd1_MouseLeftButtonUp"
MouseLeave="brd1_MouseLeave" />
</Grid>
C# :
private void brd1_MouseLeftButtonUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
brd1.Visibility = System.Windows.Visibility.Collapsed;
}
private void brd1_MouseLeave(object sender, MouseEventArgs e)
{
MessageBox.Show("Mouse Leave");
}
Why is the MouseLeave not firing when setting Visibility = Collapsed (ie : when I click on the border)?
Is there a way to always catch the MouseLeave event even if the control disappears (or one of its parent)? I cannot listen to the MouseButtonUp event, since my control can appear/disappear asynchronously at any time.
(note : my application is far more complex than that, this was just a simple example of what I need to do)

Attached events in WPF: an OK button always requires 2 clicks to close

I have the following XAML
<Window x:Class="SimpleAttahEvent.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow">
<Grid>
<StackPanel Margin="5" Name="stackButton" ButtonBase.Click="DoSomething">
<Button Name="cmd1" Tag="The first button"> Command 1</Button>
<Button Name="cmd2" Tag="The second button"> Command 2</Button>
<Button Name="cmd3" Tag="The third button"> Command 3</Button>
</StackPanel>
</Grid>
</Window>
...With the following code to handle the attached events.
namespace SimpleAttahEvent
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
stackButton.AddHandler(Button.ClickEvent, new RoutedEventHandler(DoSomething));
}
private void DoSomething(object sender, RoutedEventArgs e)
{
Console.WriteLine(Button.ClickEvent.RoutingStrategy);
Console.WriteLine(TextBox.PreviewKeyDownEvent.RoutingStrategy);
if (e.Source == cmd1)
{
MessageBox.Show("First button is clicked");
}
if (e.Source == cmd2)
{
MessageBox.Show("Second button is clicked");
}
if (e.Source == cmd3)
{
MessageBox.Show("Third button is clicked");
}
}
}
}
These produce a dialog box with 3 buttons stacked vertically. When I click one of the button, a messagebox comes up with an OK button. However, the OK button on the dialogue box won't close unless I clicked it twice. Did I do this implicitly from the code given above?
Edit - Additional Info:
When I do this instead
private void DoSomething(object sender, RoutedEventArgs e)
{
object tag = ((FrameworkElement)sender).Tag;
MessageBox.Show((string)tag);
}
..it still require 2 clicks to close the message box.
Your problem is that you are doubling your handler. You do not have to click twice on the same OK; you are clicking on OK, which closes the first message. Then, the event is handled again and you get another exact same message that you have to click OK on. If you add + DateTime.Now to your messages you will see that this is indeed a second message
I missed this line on my first glance:
stackButton.AddHandler(Button.ClickEvent, new RoutedEventHandler(DoSomething));
Which is the same as the ButtonBase.Click from this line
<StackPanel Margin="5" Name="stackButton" ButtonBase.Click="DoSomething">
Choose one way to attach to event handlers and stick to it. Mixing them up is just going to cause confusion.

How to Programmatically change a Tab Control's Index & State (Perhaps using commands?)

I have a WPF TabControl which contains a number of TabItems with child UserControls, like this.
XAML:
<TabControl x:Name="tabsMain" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
Grid.Row="0" Grid.RowSpan="3" Background="lightgray">
<TabItem Width="100" Header="Profile" FontSize="16">
<InfoControl:InfoTab x:Name="myInfo" />
</TabItem>
<TabItem Width="120" x:Name="Summary" Header="Summary" FontSize="16">
<SummaryControl:SummaryTab/>
</TabItem>
</TabControl>
Within one of the UserControls, lets say, InfoTab, I have a Button. When this Button is clicked I would like to change the index of the TabControl to the SummaryTab and select a radio button on the SummaryTab page.
My problem is that the InfoTab user control does not have access to the MainUserControl which contains the TabControl shown above. I figured out a kludge which changes the SelectedIndex of the TabControl, but this is a very ugly solution and I'd prefer to do something more clean. Also I cannot currently change the RadioButton on my SummaryTab.
My Current C# hack:
Private void btnSummaryDetails_Click(object sender, RoutedEventArgs e)
{
TabControl tabControl = UIHelper.FindChild<TabControl>(Application.Current.MainWindow, "tabsMain");
tabControl.SelectedIndex = 7;
}
Is it possible to use commands or dependency properties to select the SummaryTab and my desired RadioButton? I'm still new to the WPF world, and would love to learn more about this. Thanks in advance.
See my post here for the UIHelper definition I use in the C# above.
One thought comes to mind that will not require too many changes.
First, add an event to your InfoTab class:
public event EventHandler SummaryButtonClicked;
Then handle that in your main form by replacing the control declaration with:
<InfoControl:InfoTab x:Name="myInfo" SummaryButtonClicked="summaryButtonClicked" />
And give a name to your SummaryTab:
<SummaryControl:SummaryTab x:Name="summaryTab" />
Then add the event handler in your main form:
void MainWindow_SummaryButtonClicked(object sender, EventArgs e)
{
this.summaryTab.SelectRadioButton();
}
And add a method in your SummaryTab class to select your radio button.
public void SelectRadioButton()
{
// TODO: something like
myRadioButton.IsChecked = true;
}
You could probably use WPF routed events to solve your problem. Routed events use the WPF visual tree to send events up to parent controls (bubbling) or down to child controls (tunneling) without excessive coupling. I've tried to give a simple example below because I know that routed events can be a bit hairy to learn at first but it's well worth it...
In your main window, define a routed event and add a handler method:
public partial class MainWindow : Window {
public static RoutedEvent ClickedEvent = EventManager.RegisterRoutedEvent(
"Clicked",
RoutingStrategy.Bubble,
typeof(RoutedEventHandler),
typeof(MainWindow));
public MainWindow() {
InitializeComponent();
this.AddHandler(MainWindow.ClickedEvent, new RoutedEventHandler(OnClickedEvent));
}
public void OnClickedEvent(object sender, RoutedEventArgs e) {
// do your work here
}
}
In your button click handler, raise the event:
public partial class UserControl1 : UserControl {
public UserControl1() {
InitializeComponent();
}
private void button1_Click(object sender, RoutedEventArgs e) {
// raise the event (gets bubbled up to the parent of the control)
this.RaiseEvent(new RoutedEventArgs(MainWindow.ClickedEvent));
}
}
The next step would be to tunnel another event down the visual tree and let the other usercontrol listen for it.
I ended up adding a public method as Jeremy suggested in his post. A simple but effective solution. Thanks Jeremy!
Another key realization was that in order to switch the tabcontrol by index, I can get a reference to the main user control and set the SelectedItem to the TabItem itself, like this:
Private void btnSummaryDetails_Click(object sender, RoutedEventArgs e)
{
//HACK replace this with a command that toggles to a different tab instead of this tab reference
MainUserControl mainUserControl = UIHelper.FindChild<MainUserControl>(Application.Current.MainWindow, "root");
mainUserControl.tabsMain.SelectedItem = mainUserControl.Summary;
mainUserControl.SummaryUserControl.SelectRadioButton();
}
Then as suggested by Jeremy, my solution was something like:
public void SelectRadioButton()
{
// TODO: something like
myRadioButton.IsChecked = true;
}
My XAML structure was like
// my main user control:
<UserControl
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
...
x:Name="root"
>
<TabControl x:Name="tabsMain" ...>
<TabItem x:Name="Summary" ... />
</TabControl>
</UserControl>
I think Andrew Jackson's comments are absolutely valid - long term, I plan to investigate using routed command or routed events to traverse the visual tree, but for now I'm sticking with this "quick and dirty" solution as we're not shipping this product. Based on my investigation Routed Events might be a little overkill for this situation.

Does MouseLeftButtonDown sender object really not pass a referance to object that triggered it? c# Silverlight

I recently created a Silverlight 3 app in which I created some UI elements in the code behind and added them at run-time dynamically.
I was hoping to just use the built-in MouseButtonEventArgs or the sender object to get a reference to the instance that was clicked, however I noticed once I started that this was not the case. I was not able to access any properties of the object that triggered the event and program against it.
void myFunc(object sender, MouseButtonEventArgs e)
{
//Can't do this :(
sender.someProperty = someValueToUpdate;
//or this
MyClass foo = sender as MyClass;
foo.someProperty = someValueToUpdate;
}
I ended up just writing a CustomEventArgs object to pass an instance, but it surprised me that this wasn't a default behavior.
Can anyone shed some light as to WHY the sender object doesn't contain a reference to the object that triggered the event?
Also, here is what I did to get that instance.
myObject.myEvent += new CustomEvent(myFunc);
...
void myFunc(object sender, CustomEventArgs e)
{
e.MyProperty = someValueToUpdate;
}
...
public class MyClass
{
public MyProperty = 0;
public event CustomEvent myEvent;
protected virtual void MyEventMethod(CustomEventArgs e)
{
if (myEvent != null){myEvent(this, e);}
}
public MyClass ()
{
this.MouseLeftButtonDown += new MouseButtonEventHandler(this_MouseLeftButtonDown);
}
void rect_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
CustomEventArgs e2 = new CustomEventArgs(this);
MyEventMethod(e2);
}
}
public class CustomEventArgs : EventArgs
{
private readonly MyClass myProperty;
public CustomEventArgs(MyClass myProperty) { this.myProperty = myProperty; }
public MyClass MyProperty { get { return myProperty; } }
}
public delegate void CustomEvent(object sender, CustomEventArgs e);
The MouseEventArgs has a OriginalSource property. Its this property which holds a reference to the object that originally triggered it.
The sender argument quite rightly is set to the instance of the object against which you attached the event handler. Perhaps a simple experiment will make how this hangs together clearer. In Visual Studio create a Silverlight Application. Make the content of the MainPage.xaml look like this:-
<UserControl x:Class="SilverlightApplication1.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
>
<Grid x:Name="LayoutRoot" Background="White" MouseLeftButtonDown="MouseHandler">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<StackPanel x:Name="OuterPanel" MouseLeftButtonDown="MouseHandler" Margin="5">
<StackPanel x:Name="TopPanel" MouseLeftButtonDown="MouseHandler">
<TextBlock Text="First Top Item" />
<TextBlock Text="Second Top Item" />
</StackPanel>
<StackPanel x:Name="BottomPanel" MouseLeftButtonDown="MouseHandler">
<TextBlock Text="First Bottom Item" />
<TextBlock Text="Second Bottom Item" />
</StackPanel>
</StackPanel>
<ListBox x:Name="lstOutput" Grid.Column="1" Margin="5" />
</Grid>
</UserControl>
And in MainPage.xaml.cs add this code:-
private void MouseHandler(object sender, MouseButtonEventArgs e)
{
FrameworkElement s = sender as FrameworkElement;
TextBlock o = e.OriginalSource as TextBlock;
string text = (o != null) ? o.Text : "Not from a text block";
lstOutput.Items.Add(String.Format("Sender: {0}, Text block: {1}", s.Name, text));
}
Note how this same handler is attached to three different items in the XAML but not to the TextBlocks themselves. Clicking the "First Top Item" gets you this:-
Sender: TopPanel, Text block: First Top Item
Sender: OuterPanel, Text block: First Top Item
Sender: LayoutRoute, Text block: First Top Item
The handler fires 3 times once for each item it is attached to as can be seen by the sender being different for each one. However the OrignalSource it the TextBlock that was actually clicked on despite it not having any handler attached. Also note that the OriginalSource remains the same as it bubbles up the ancestor elements.
Click on the area below the Stack panels. You only get:-
Sender: LayoutRoot, Text block: Not from a text block
Of interest also is that clicking in the Listbox results in no items being added at all, you might expect to the same ase the above line. Clearly ListBox handles the mouse down and therefore sets the event args Handled property to True preventing further bubbling.
From the msdn documentation:
For a bubbling event, the sender
parameter identifies the object where
the event is handled, not necessarily
the object that actually received the
input condition that initiated the
event.
I.e. since it's a bubbling event, maybe you should try something like
void myFunc(object sender, MouseButtonEventArgs e)
{
var theUIElement = sender as TheUIElementOfWhichImInterested;
if (theUIElement != null)
{
// set properties on the element
}
}

Categories

Resources