How to open the context menu programmatically?

Heiko 1,216 Reputation points
2023-05-17T10:22:54.0233333+00:00

In a WPF app with .NetFramework 4.8 I have an ItemsControl. Each item has its context menu which I can open via right mouse button. Now I want to support the apps key (right of 'Alt Gr' key). In XAML I added a handler for this key. In handler I get the selected item, get its context menu and open it. But then 3 issues appear:

  1. the context menu opens and closes immediately.
  2. the context menu of the item should have a data binding to the items data context. But ContextMenu.Tag is null and MenuItem.Tag is null, although it is set in XAML.
  3. the context menu opens always near by the mouse, not at its parent item, the item of the ItemsControl.

How can I manage that?

<Window x:Class="ContextMenuAppsKey.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:ContextMenuAppsKey"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">

	<Window.Resources>
		<RoutedCommand x:Key="AppsCommand" />
	</Window.Resources>
	<Window.CommandBindings>
		<CommandBinding Command="{StaticResource AppsCommand}" Executed="Apps_Executed" />
	</Window.CommandBindings>
	<Window.InputBindings>
		<KeyBinding Key="Apps" Command="{StaticResource AppsCommand}" />
	</Window.InputBindings>

	<Grid>
		<ItemsControl Name="listBox" ItemsSource="{Binding Children}">
			<ItemsControl.ItemTemplate>
				<DataTemplate>
					<TextBlock Text="{Binding Name}" Background="{Binding Background}" FontSize="20">
						<TextBlock.ContextMenu>
							<ContextMenu Opened="ContextMenu_Opened" Tag="{Binding}">
								<MenuItem Header="Say hello!" Tag="{Binding}" Click="menuItem_Click" />
							</ContextMenu>
						</TextBlock.ContextMenu>
					</TextBlock>
				</DataTemplate>
			</ItemsControl.ItemTemplate>
		</ItemsControl>
	</Grid>
</Window>
namespace ContextMenuAppsKey
{
	public class Person
	{
		internal bool IsSelected { get; set; }
		public string Name { get; internal set; }

		public Brush Background => new SolidColorBrush(IsSelected ? Colors.PowderBlue : Colors.GhostWhite);
	}

	public partial class MainWindow : Window
	{
		private List<Person> People;

		public MainWindow()
		{
			InitializeComponent();

			People = new List<Person>() {
					new Person() { Name = "Susan" },
					new Person() { Name = "Tim", IsSelected = true },
					new Person() { Name = "Tom" },
				};

			listBox.ItemsSource = People;
		}

		private void ContextMenu_Opened(object sender, RoutedEventArgs e)
		{
		}

		private void Apps_Executed(object sender, ExecutedRoutedEventArgs e)
		{
			e.Handled = true;

			Person selected = People.FirstOrDefault(p => p.IsSelected);

			if (selected == null)
				return;

			ContextMenu menu = FindContextMenu(selected);

			menu.IsOpen = true;
		}

		private ContextMenu FindContextMenu(Person entry)
		{
			DependencyObject dep = listBox.ItemContainerGenerator.ContainerFromItem(entry);

			if (dep == null)
				return null;

			TextBlock textBlock = FindChild_Recursive(dep, d => d is TextBlock) as TextBlock;

			return textBlock?.ContextMenu;
		}

		private static DependencyObject FindChild_Recursive(DependencyObject parent, Func<DependencyObject, bool> match)
		{
			int count = VisualTreeHelper.GetChildrenCount(parent);
			DependencyObject dep;

			for (int i = 0; i < count; ++i)
			{
				dep = VisualTreeHelper.GetChild(parent, i);
				if (match(dep))
					return dep;
			}
			for (int i = 0; i < count; ++i)
			{
				dep = VisualTreeHelper.GetChild(parent, i);
				dep = FindChild_Recursive(dep, match);
				if (dep != null)
					return dep;
			}
			return null;
		}

		private void menuItem_Click(object sender, RoutedEventArgs e)
		{
			Person entry = (sender as FrameworkElement).Tag as Person;

			MessageBox.Show("Hello, my name is " + (entry?.Name ?? "<null>"));
		}
	}
}
Windows Presentation Foundation
Windows Presentation Foundation
A part of the .NET Framework that provides a unified programming model for building line-of-business desktop applications on Windows.
2,709 questions
C#
C#
An object-oriented and type-safe programming language that has its roots in the C family of languages and includes support for component-oriented programming.
10,603 questions
{count} votes

1 answer

Sort by: Most helpful
  1. Heiko 1,216 Reputation points
    2023-05-18T12:32:04.4066667+00:00

    I have found the problem: The key binding command is always raised when the key is pressed and every following KeyUp event closes every menu. So I have to handle the KeyUp event for the Apps key instead. In the handler I have to set the Tags in ContextMenu and MenuItem, and I have to set the placement.

    <Window x:Class="ContextMenuAppsKey.MainWindow"
    	KeyUp="Window_KeyUp">
    </Window>
    
    <ContextMenu Opened="ContextMenu_Opened" Closed="ContextMenu_Closed" Tag="{Binding}">
    	<MenuItem Header="Say hello!" Tag="{Binding}" Click="menuItem_Click" />
    </ContextMenu>
    
    private void Window_KeyUp(object sender, KeyEventArgs e)
    {
    	if (e.Key != Key.Apps)
    		return;
    
    	e.Handled = true;
    
    	Person selected = People.FirstOrDefault(p => p.IsSelected);
    
    	if (selected == null)
    		return;
    
    	TextBlock textBlock = FindUIElement(selected);
    
    	textBlock.ContextMenu.PlacementTarget = textBlock;
    	textBlock.ContextMenu.Placement = System.Windows.Controls.Primitives.PlacementMode.Bottom;
    	textBlock.ContextMenu.VerticalOffset = -10;
    	textBlock.ContextMenu.Tag = selected;
    	(textBlock.ContextMenu.Items[0] as MenuItem).Tag = selected;
    	textBlock.ContextMenu.IsOpen = true;
    }
    
    private TextBlock FindUIElement(Person entry)
    {
    	DependencyObject dep = listBox.ItemContainerGenerator.ContainerFromItem(entry);
    
    	if (dep == null)
    		return null;
    
    	return FindChild_Recursive(dep, d => d is TextBlock) as TextBlock;
    }
    
    private void ContextMenu_Closed(object sender, RoutedEventArgs e)
    {
    	ContextMenu menu = sender as ContextMenu;
    
    	menu.Placement = System.Windows.Controls.Primitives.PlacementMode.MousePoint;
    }
    
    0 comments No comments