Programatically attach an event handler to a custom child element routed event - wpf

I have a Window that has a Button and a Popup. When the button is clicked, the event handler in code behind opens the Popup. On the Popup I have Button when clicked, the event handler in code behind closes the Popup. Simple. Crude.
I also have a UserControl that has a custom routed event and a Button that raises that event.
That UserControl has been placed on the Popup.
I have added an event handler in XAML on the Popup element for the UserControl custom event. In code behind I show a message box.
This all works fine and dandy.
This is an extremely boiled down example. My ultimate question is, how do I programmatically attach an event handler to the custom routed event at the Popup level?
The UserControl XAML:
<Grid>
<Button
Width="100"
Height="100"
Click="Button_Click"
Content="Event!" />
</Grid>
The UserControl code behind:
public static readonly RoutedEvent CustomEventEvent = EventManager.RegisterRoutedEvent(
nameof(CustomEvent), RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(UserControl1));
public event RoutedEventHandler CustomEvent
{
add => this.AddHandler(CustomEventEvent, value);
remove => this.RemoveHandler(CustomEventEvent, value);
}
private void Button_Click(object sender, RoutedEventArgs e)
{
RaiseEvent(new RoutedEventArgs(CustomEventEvent));
}
Main window XAML:
<Grid>
<Button
Width="200"
Height="100"
Click="Button_Click"
Content="Popup" />
<Popup
x:Name="MyPopup"
local:UserControl1.CustomEvent="MyPopup_CustomEvent"
AllowsTransparency="True"
Loaded="MyPopup_Loaded"
Placement="Right">
<Border
Background="Azure"
BorderBrush="Gray"
BorderThickness="2"
CornerRadius="3">
<Grid Width="500" Height="300">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Button
Grid.Row="0"
Width="100"
Height="100"
Click="Button_Click_1"
Content="Close" />
<local:UserControl1
Grid.Row="1"
Width="100"
Height="100" />
</Grid>
</Border>
</Popup>
</Grid>
The main window code behind (minus the boring popup open/close button clicks):
private void MyPopup_CustomEvent(object sender, RoutedEventArgs e)
{
MessageBox.Show("YAY");
}
private void MyPopup_Loaded(object sender, RoutedEventArgs e)
{
if (sender is Popup popup)
{
// Here is where the wheels fall off.
// How do I find/attach to the routed event after it has bubbled up to the popup?
// popup.CustomEvent += LocalMyPopup_CustomEvent;
}
}
private void LocalMyPopup_CustomEvent(object sender, RoutedEventArgs e)
{
}
Ultimately, there will be several of the UserControls in many popups and I'd rather not have to attach event handlers to each and every one of them. Especially considering I can get them all at the Popup level using MyPopup_CustomEvent. I just want to replicate that behavior.

Turns out the solution is as trivial as it is (in hindsight) obvious. The AddHandler has to be called directly on the Popup and passing in the static Routed Event reference.
private void MyPopup_Loaded(object sender, RoutedEventArgs e)
{
if (sender is Popup popup)
{
popup.AddHandler(UserControl1.CustomEventEvent, new RoutedEventHandler(LocalMyPopup_CustomEvent), true);
// popup.CustomEvent += LocalMyPopup_CustomEvent;
}
}

Related

Return keyboard focus

I have an application where the main window contains a user control, and inside that user control are items stored in an ItemsControl. Each item can be removed by clicking an 'x' button.
The problem I am facing is that although the Keyboard focus is initially set to the user control, when you remove an item, focus is then transferred to the main window, instead of back to the user control?
Is there a way I can fix this without having to add code behind to manually store/retrieve/set focus after the click?
I have lots of these buttons within my application and I'm trying to avoid having to add code all over the place to manage returning the Focus.
I have created a very simple example to show the issue :
<UserControl x:Class="WpfApp28.MyControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid Width="300">
<StackPanel>
<ItemsControl ItemsSource="{Binding}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<TextBlock Text="{Binding}" />
<Button Content="x"
Width="20"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Click="Button_Click" />
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Grid>
</UserControl>
public partial class MyControl : UserControl
{
public MyControl()
{
InitializeComponent();
Focusable = true;
Loaded += MyControl_Loaded;
}
private void MyControl_Loaded(object sender, RoutedEventArgs e)
{
Keyboard.Focus(this);
}
private void Button_Click(object sender, RoutedEventArgs e)
{
if (sender is FrameworkElement fe && fe.DataContext is string item)
{
(DataContext as ObservableCollection<string>).Remove(item);
}
}
}
<Window x:Class="WpfApp28.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApp28"
Title="MainWindow" Height="450" Width="800">
<local:MyControl DataContext="{Binding Items}" />
public partial class MainWindow : Window
{
public ObservableCollection<string> Items { get; } = new ObservableCollection<string>();
public MainWindow()
{
Items.Add("hello");
Items.Add("there");
Items.Add("world");
DataContext = this;
InitializeComponent();
DispatcherTimer t = new DispatcherTimer();
t.Interval = TimeSpan.FromMilliseconds(250);
t.Tick += T_Tick;
t.Start();
}
private void T_Tick(object? sender, EventArgs e)
{
Title = Keyboard.FocusedElement?.GetType().ToString() ?? "NULL";
}
}
The reason that the keyboard focus moves to the hosting Window is obvious once you understand how WPF handles focus. It's important to know that WPF uses scopes in which the focus traverses the elements.
There can be multiple focus scopes allowing multiple elements to remain focused simultaneously.
By default, the hosting Window defines a focus scope. Since it is the only focus scope, it is global (the scope of the complete visual tree).
What happens in your code in short:
The Button receives the focus via mouse click
The click handler removes the clicked item and therefore the clicked Button from the visual tree
WPF moves focus back to the focus scope root, which is the MainWindow in your case
You have multiple options to prevent the focus from being moved back to the focus root. Some involve code-behind.
The following examples show how to move the focus back to the parent UserControl. But it could be any element as well:
You can configure the Button (the element that "steals" the current focus) to be not focusable. This only works if the UserControl is already focused:
<DataTemplate>
<Button Content="x"
Focusable="False" />
</DataTemplate>
You can introduce a new focus scope. Since you want the UserControl itself to be focused, you must choose the root element of the UserControl. You can achieve this by using the FocusManager helper class:
<UserControl>
<Grid x:Name="RootPanel"
FocusManager.IsFocusScope="True"
Width="300">
</Grid>
</UserControl>
You can of course register a Button.Click handler or preferably a routed command to move the focus back to the UserControl explicitly. A routed command can be more convenient in most cases. It allows to send a command parameter that makes the code-behind simpler.
Note, since Button.Click is a routed event, you can simply register a Button.Click event handler on the UserControl. This example uses the existing click handler that is used to remove the item from the ItemsControl:
UserControl.xaml
<DataTemplate>
<Button Content="x"
Click="OnButtonClick" />
</DataTemplate>
UserControl.xaml.cs
private void OnButtonClick(object sender, RoutedEventArgs)
{
/* Delete the item */
Keyboard.Focus(this);
}
Final suggested solution
To improve your code and handling of the UserControl you must definitely implement an ItemsSource dependency property and use a routed command to delete the items.
The following example uses the predefined ApplicationCommands.Delete routed command. You will notice how simple the code has become:
MyControl.xaml.cs
public partial class MyControl : UserControl
{
public IList ItemsSource
{
get => (IList)GetValue(ItemsSourceProperty);
set => SetValue(ItemsSourceProperty, value);
}
public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register(
"ItemsSource",
typeof(IList),
typeof(UserControl4), new PropertyMetadata(default));
public MyControl()
{
InitializeComponent();
this.Focusable = true;
}
public override void OnApplyTemplate()
=> Keyboard.Focus(this);
private void DeleteItemCommand_Executed(object sender, ExecutedRoutedEventArgs e)
=> this.ItemsSource.Remove(e.Parameter);
private void DeleteItemCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e)
=> e.CanExecute = this.ItemsSource.Contains(e.Parameter);
}
MyControl.xaml
<UserControl>
<UserControl.CommandBindings>
<CommandBinding Command="{x:Static ApplicationCommands.Delete}"
Executed="DeleteItemCommand_Executed"
CanExecute="DeleteItemCommand_CanExecute" />
</UserControl.CommandBindings>
<Grid x:Name="RootPanel"
FocusManager.IsFocusScope="True">
<StackPanel>
<ItemsControl ItemsSource="{Binding RelativeSource={RelativeSource AncestorType=local:UserControl4}, Path=ItemsSource}"
>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<TextBlock Text="{Binding}" />
<Button Content="x"
Command="{x:Static ApplicationCommands.Delete}"
CommandParameter="{Binding}"
Width="20"
HorizontalAlignment="Right"
VerticalAlignment="Center" />
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Grid>
</UserControl>
MainWindow.xaml
<Window>
<MyControl ItemsSource="{Binding Items}" />
</Window>
Remarks
You should consider to use a ListBox instead of the pure ItemsControl.
ListBox is an extended ItemsControl. It will significantly improve performance and provides a ScrollViewer by default.

Access keys not working inside popup (WPF)

I have a simple popup in my WPF application.
There is a button with access-key inside this popup.
The problem I have is that this button doesn't respond to Alt+access-key combination.
Moreover pressing Alt doesn't make access key visible like it happens in ordinary window.
Is there any way to make controls inside popup respond to Alt+access-key combination?
P.S. I have no problem with navigation using Tab through this popup.
Sapmle code that I'm using
<Grid>
<Button Click="ButtonBase_OnClick" Content="_Open File"></Button>
<Popup x:Name="Popup" StaysOpen="False">
<Grid Background="White">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Button Content="_Open File"/>
<Button Grid.Row="1" Content="O_pen File"/>
<CheckBox Grid.Row="2" Content="_Go"></CheckBox>
</Grid>
</Popup>
</Grid>
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
Popup.IsOpen = true;
}
I have also tried adding this as the first answer suggests
private void Popup_OnOpened(object sender, EventArgs e)
{
var popup = sender as Popup;
popup.Child.Focusable = true;
Keyboard.Focus(popup.Child);
}
I have also tried the idea from the first comment
private void Popup_OnOpened(object sender, EventArgs e)
{
var popup = sender as Popup;
FocusManager.SetIsFocusScope(popup, true);
}
or instead of standart focus scope the one from suggested link codeproject.com/Articles/38507/Using-the-WPF-FocusScope
EnhancedFocusScope.SetFocusOnActiveElementInScope(popup);
Setting focus scope helped a little bit, but I didn't manage to make it work exactly as I would like.
Setting focus scope to true did help to use alt+key combination for checkboxes and label+textbox, but not for buttons. Although I could use Alt+access key combinations, I couldn't actually see them, because underscores didn't appear when I pressed Alt
Popup is not part of the visual tree. This means it has its own isolated focus scope. When a Popup is opened, the Popup.Child is hosted in a dedicated Window with its own detached visual tree. The Popup.Child therefore needs to explicitly receive keyboard focus before the access keys are available in the detached focus scope.
You can handle the Popup.Opened event, either in code-behind or using an attached behavior.
It's essential that the Popup.Child is focusable in order to receive keyboard focus.
Some classes like Panel and its subclasses have UIElement.Focusable set to false by default.
<StackPanel>
<ToggleButton x:Name="ToggleButton" Content="Show Popup" />
<Popup x:Name="Popup"
AllowsTransparency="True"
PlacementTarget="{Binding ElementName=ToggleButton}"
IsOpen="{Binding ElementName=ToggleButton, Path=IsChecked}"
Opened="Popup_OnOpened">
<StackPanel>
<Button Grid.Row="1" Content="O_pen File" />
<CheckBox Grid.Row="2" Content="_Go" />
</StackPanel>
</Popup>
</StackPanel>
private void Popup_OnOpened(object sender, EventArgs e)
{
var popup = sender as Popup;
popup.Child.Focusable = true;
Keyboard.Focus(popup.Child);
}
Once a button or a checkbox inside the popup is focused, the alt-shortcuts works.
<Button Click="ButtonBase_OnClick" Content="_Open File" />
<Popup x:Name="Popup">
<StackPanel Background="White">
<CheckBox x:Name="FirstCeckbox" Content="_Foo" />
<CheckBox Content="_Bar" />
</StackPanel>
</Popup>
Code behind:
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
Popup.IsOpen = !Popup.IsOpen;
if (Popup.IsOpen)
FirstCeckbox.Focus(); // Make sure to focus a Button or a Checkbox, not the Stackpanel or Grid etc.
}

Same click event for both Button and Grid

I read about Routed events today and tried to provide same click event handler for both a normal button and a custom grid button. Stackpanel is handling the routed button click event, and to invoke same handler I am firing a click event from grid's mousedown event. Code is without error but not working as expected.
Button click brings the messagebox but clicking the grid with mouse does nothing.
<Window x:Class="RoutedEventPr.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="550" Width="525">
<StackPanel Background="Transparent" Button.Click="Button_Click_1">
<Grid Width="200" Height="100" Background="Aqua" MouseDown="Grid_MouseDown_1">
<Ellipse StrokeThickness="4" Width="200" Height="100" Fill="Beige"/>
<TextBlock Text="Press" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Grid>
<Button x:Name="SureBtn" Content="Pause !" Width="200" Margin="0 10 0 0"/>
<Image HorizontalAlignment="Center" VerticalAlignment="Center" Width="200" Height="200" Source="I://fancybtn.jpg"/>
<Label Content="Start the game"/>
</StackPanel>
private void Grid_MouseDown_1(object sender, MouseButtonEventArgs e)
{
RaiseEvent(new RoutedEventArgs(Button.ClickEvent, this));
}
private void Button_Click_1(object sender, RoutedEventArgs e)
{
MessageBox.Show("Are you sure !");
}
Button.Click event is routed event with bubbling strategy set to Bubble.
That means it will bubble up to visual parent till root until it was handled. In your case, you raise an event from Window, so it will bubble up from Window to its parent which is null.
You have to raise the event from child control of StackPanel so that it can bubble up to StackPanel.
XAML
<Grid x:Name="grid" Width="200" Height="100" Background="Aqua"
MouseDown="Grid_MouseDown_1">
<Ellipse StrokeThickness="4" Width="200" Height="100" Fill="Beige"/>
<TextBlock Text="Press" HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Grid>
Code behind
private void Grid_MouseDown_1(object sender, MouseButtonEventArgs e)
{
grid.RaiseEvent(new RoutedEventArgs(Button.ClickEvent, this));
}
I did below changes and it works now.
private void Grid_MouseDown_1(object sender, MouseButtonEventArgs e)
{
SureBtn.RaiseEvent(new RoutedEventArgs(Button.ClickEvent, this));
}

Any way for a ToolTip to trigger a MouseEnter event?

I have a control with a tooltip. What i want is that when I hover over the control and the tooltip opens: if I then enter the tooltip with the mouse - this will trigger a mouseEnter event in order to trigger some other action. The closest I am to finding a solution to this is adding a ToolTipClosing event on the control with the trigger ... but this will fire as soon as I leave the control - even if my mouse doesn't enter the tooltip.
(Triggering a MouseEnter event on the tooltip itself doesn't seem to get fired at all)
Here's an example: (where I want to change the background of the border if I enter the tooltip)
XAML
<Border Height="300" Name="dummyBorder"
Width="200"
Background="Red" />
<Label ToolTipService.InitialShowDelay="3000"
Content="Hover over here"
ToolTipService.ShowDuration="4000"
ToolTipService.Placement="Right"
ToolTipClosing="Label_ToolTipClosing"
Width="100"
HorizontalAlignment="Center"
Margin="10">
<Label.ToolTip>
<ToolTip Name="tt" MouseEnter="ttBorder_MouseEnter">
<Border Background="Brown"
Name="ttBorder"
MouseEnter="ttBorder_MouseEnter"
Width="100"
Height="50">
<TextBlock Text="This is a tool tip." />
</Border>
</ToolTip>
</Label.ToolTip>
</Label>
CodeBehind: (neither of these work)
private void Label_ToolTipClosing(object sender, ToolTipEventArgs e)
{
if (tt.IsMouseDirectlyOver)
{
dummyBorder.Background = Brushes.Aqua;
}
}
private void ttBorder_MouseEnter(object sender, MouseEventArgs e)
{
dummyBorder.Background = Brushes.Aqua;
}
I specifically want to use a tooltip and not a popup. Is this possible?
Any help will be greatly appreciated!
you will want to try something like this:
<Window.CommandBindings>
<CommandBinding Command="ChangeColour"
CanExecute="ChangeCanExecute"
Executed="ChangeExecuted" />
</Window.CommandBindings>
inside your tooltip tag:
<MouseBinding Gesture="LeftClick" Command="{Binding ChangeColour}"/>
then in your codebehind:
private void ChangeCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute = true;
e.Handled = true;
}
private void ChangeExecuted(object sender, ExecutedRoutedEventArgs e)
{
dummyBorder.Background = Brushes.Aqua;
e.Handled = true;
}
Well (after 9 months and no answer) I guess that there is no way then. (Unless proven otherwise)

How to close popup in silverlight?

I have ListBox. when i click on ListBox item I have to show item information in popup But it does not close after clicking out side. I am creating popup in itemsselected event. how to handle popup close?
One approach is to create a canvas with a transparent background that you make visible at the same time as opening the Popup and attaching to is Mouse down event to closed the popup. Like this:-
Xaml:-
<Grid x:Name="LayoutRoot" Background="White" >
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Popup x:Name="MyPopup" Closed="MyPopup_Closed" HorizontalOffset="100" VerticalOffset="100" Opened="Popup_Opened">
<ListBox x:Name="PopupChild" MaxHeight="300" LostFocus="PopupChild_LostFocus">
<sys:String>Hello World</sys:String>
</ListBox>
</Popup>
<Button Content="Open Popup" Grid.Row="1" Click="Button_Click" />
<Canvas x:Name="PopupOpen" Visibility="Collapsed" Background="Transparent" Grid.RowSpan="2" MouseLeftButtonDown="PopupOpen_MouseLeftButtonDown" />
</Grid>
Code:-
private void Button_Click(object sender, RoutedEventArgs e)
{
MyPopup.IsOpen = true;
}
private void Popup_Opened(object sender, EventArgs e)
{
PopupOpen.Visibility = Visibility.Visible;
}
private void PopupChild_LostFocus(object sender, RoutedEventArgs e)
{
MyPopup.IsOpen = false;
}
private void PopupOpen_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
MyPopup.IsOpen = false;
}
private void MyPopup_Closed(object sender, EventArgs e)
{
PopupOpen.Visibility = Visibility.Collapsed;
}
Note that its important that if your popup contains a control that can receive the focus that you also handle LostFocus.
This is similar to a question that I had. Take a look at How to dismiss a popup in Silverlight when clicking outside of the control?. I posted in my solution an extension method that's been very helpful in making popups close when clicking outside of them.
I'm not quite sure what you mean by "clicking out side" because popups act in a modal way.
You should set up your popup window as a ChildWindow. Then you can handle the Closed event.
Here's a very simple sample that shows a selected string from a listbox in a main window.
First the main window:
<UserControl x:Class="PopupTest.MainPage"
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"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="400">
<Grid x:Name="LayoutRoot" Background="White">
<StackPanel Orientation="Vertical">
<ListBox x:Name="SomeList" Width="100" Height="100" />
<TextBlock x:Name="DialogResult" Width="100" />
</StackPanel>
</Grid>
In the codebehind, the popup is triggered when the list selection changes. Simply set up a Closed handler. In this example, I simply put the chosen list item into a textblock, then upon closing the popup, I just put the dialog result in a textblock on the main window (to show if the user pushed ok or cancel).
public MainPage()
{
InitializeComponent();
SomeList.SelectionChanged += new SelectionChangedEventHandler(SomeList_SelectionChanged);
SomeList.Items.Add("one");
SomeList.Items.Add("two");
SomeList.Items.Add("three");
}
void SomeList_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
var popup = new SomePopup();
popup.Closed += new EventHandler(popup_Closed);
popup.ChosenItem.Text = (string)SomeList.SelectedItem;
DialogResult.Text = "";
popup.Show();
}
void popup_Closed(object sender, EventArgs e)
{
var popup = sender as SomePopup;
if (popup.DialogResult == true)
DialogResult.Text = "Ok";
else
DialogResult.Text = "Cancel";
}
The popup closes when the user pushes Ok or Cancel, because the DialogResult value is set in the popup's code-behind:
private void OKButton_Click(object sender, RoutedEventArgs e)
{
this.DialogResult = true;
}
private void CancelButton_Click(object sender, RoutedEventArgs e)
{
this.DialogResult = false;
}

Resources