Reputation: 279
I'm trying to add a ContextMenu
to ListBoxItem
, but the Click
event is not fired.
I tried ContextMenu.MenuItem.Click
event.
I also tried binding Command
, but a binding error appears in the output window like:
"Cannot find source for binding with reference 'RelativeSource FindAncestor, AncestorType='System.Windows.Window', AncestorLevel='1''. BindingExpression:Path=NavigateCommand;"
Here is complete sample code.
XAML
<ListBox>
<ListBoxItem>1</ListBoxItem>
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}"
BasedOn="{StaticResource {x:Type ListBoxItem}}">
<Setter Property="ContextMenu">
<Setter.Value>
<ContextMenu MenuItem.Click="ContextMenu_Click">
<MenuItem Header="Navigate"
Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=Window, AncestorLevel=1}, Path=NavigateCommand}"
Click="NavigateItemClick" />
</ContextMenu>
</Setter.Value>
</Setter>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
CODE BEHIND
public MainWindow()
{
InitializeComponent();
NavigateCommand = new DelegateCommand(Navigate);
}
public DelegateCommand NavigateCommand { get; set; }
private void Navigate()
{
MessageBox.Show("Command Worked");
}
private void NavigateItemClick(object sender, RoutedEventArgs e)
{
MessageBox.Show("Item Click Worked");
}
private void ContextMenu_Click(object sender, RoutedEventArgs e)
{
MessageBox.Show("Any Item click Worked");
}
Is there any way to invoke the Click
event handler or bind the Command
?
Upvotes: 0
Views: 147
Reputation: 29028
I don't think it is necessary to write "hackish" code. I think you should refine your code instead.
A context menu should only operate on its current UI context, hence the name context menu. When you right click your code editor, you won't find e.g. an "Open File" menu item - this would be out of context.
The context of the menu is the actual item. Therefore, the command should be defined in the data model of the item as the context menu should only define commands that target the items.
Note that the ContextMenu
is always implicitly hosted inside a Popup
(as Child
of Popup
). The child elements of Popup
can never part of the visual tree (since the content is rendered inside a dedicated Window
instance and Window
can't be a child of another element, Window
is always the root of its own visual tree), therefore tree traversal using Binding.RelativeSource
can't work.
You can reference ContextMenu.PlacementTarget
(instead of DataContext
) to get the actual ListBoxItem
(the placement target). The DataContext
of the ListBoxItem
is the data model (WindowItem class in the example below).
For example to reference the e.g. OpenWindowCommand
of the underlying data model of the current ListBoxItem
from inside the ContextMenu
use:
"{Binding RelativeSource={RelativeSource AncestorType=ContextMenu}, Path=PlacementTarget.DataContext.OpenWindowCommand}"
WindowItem.cs
Create a data model for the items in the list. This data model exposes all the commands to operate on the item itself.
class WindowItem
{
public WindowItem(string windowName) => this.Name = windowName;
public string Name { get; }
public ICommand OpenWindowCommand => new RelayCommand(ExecuteOpenWindow);
private void ExecuteOpenWindow(object commandParameter)
{
// TODO::Open the window associated with this instance
MessageBox.Show($"Window {this.Name} is open");
}
public override string ToString() => this.Name;
}
MainWindow.xaml.cs
Initialize the ListBox from your MainWindow using data binding:
partial class MainWindow : Window
{
public ObservableCollection<WindowItem> WindowItems { get; }
public MainWindow()
{
InitializeComponent();
this.DataContext = this;
var windowItems = new
this.WindowItems = new ObservableCollection<WindowItem>
{
new WindowItem { Name = "Window 1" },
new WindowItem { Name = "Window 2" }
}
}
}
MainWindow.xaml
<Window>
<Window.Resources>
<!-- Command version -->
<ContextMenu x:Key="ListBoxItemContextMenu">
<MenuItem Header="Open"
Command="{Binding RelativeSource={RelativeSource AncestorType=ContextMenu}, Path=PlacementTarget.DataContext.OpenWindowCommand}" />
</ContextMenu>
<!-- Optionally define a Click handler in code-behind (MainWindow.xaml.cs) -->
<ContextMenu x:Key="ListBoxItemContextMenu">
<MenuItem Header="Open"
Click="OpenWindow_OnMenuItemClick" />
</ContextMenu>
</Window.Resources>
<ListBox ItemsSource="{Binding WindowItems}">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="ContextMenu" Value="{StaticResource ListBoxItemContextMenu}">
</Setter>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
</Window>
Upvotes: 1
Reputation: 22119
The ContextMenu
is not part of the same visual tree as your ListBox
, as it is displayed in a separate window. Therefore using a RelativeSource
or ElementName
binding does not work directly. However, you can work around this issue by binding the Window
(where you define the NavigateCommand
in code-behind) with a RelativeSource
binding to the Tag
property of ListBoxItem
. This works, since they are part of the same visual tree. The Tag
property is a general purpose property that you can assign anything to.
Gets or sets an arbitrary object value that can be used to store custom information about this element.
Then use the PlacementTarget
property of the ContextMenu
as indirection to access the Tag
of the ListBoxItem
that it was opened for.
<Style TargetType="{x:Type ListBoxItem}"
BasedOn="{StaticResource {x:Type ListBoxItem}}">
<Setter Property="Tag" Value="{Binding RelativeSource={RelativeSource AncestorType={x:Type Window}}}"/>
<Setter Property="ContextMenu">
<Setter.Value>
<ContextMenu>
<MenuItem Header="Navigate"
Command="{Binding PlacementTarget.Tag.NavigateCommand, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}"/>
</ContextMenu>
</Setter.Value>
</Setter>
</Style>
In essence, you bind the Window
data context to the Tag
of ListBoxItem
which is the PlacementTarget
of the ContextMenu
, that can then bind the NavigateCommand
through the Tag
property.
Adding a Click
event handler is also possible, but in case the ContextMenu
is defined in a Style
, you have to add it differently, otherwise it will not work and you get this strange exception.
Unable to cast object of type
System.Windows.Controls.MenuItem
to typeSystem.Windows.Controls.Button
.
Add a MenuItem
style with an event setter for Click
, where you assign the event handler.
<Style TargetType="{x:Type ListBoxItem}"
BasedOn="{StaticResource {x:Type ListBoxItem}}">
<Setter Property="ContextMenu">
<Setter.Value>
<ContextMenu>
<MenuItem Header="Navigate">
<MenuItem.Style>
<Style TargetType="MenuItem">
<EventSetter Event="Click" Handler="MenuItem_OnClick"/>
</Style>
</MenuItem.Style>
</MenuItem>
</ContextMenu>
</Setter.Value>
</Setter>
</Style>
Upvotes: 2