I try to learn a little bit about how a listboxview combined with context menu works, so I've made the following XAML code:
<UserControl>
...
<ListView
x:Name="level1Lister"
Grid.Row="1"
behaviours:AutoScrollListViewBehaviour.ScrollOnNewItem="True"
ItemsSource="{Binding LogBuffer, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="Padding" Value="1" />
<Setter Property="Margin" Value="2,0" />
<Setter Property="BorderThickness" Value="0" />
</Style>
</ListView.ItemContainerStyle>
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical" />
</ItemsPanelTemplate>
</ListView.ItemsPanel>
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel>
<StackPanel.ContextMenu>
<ContextMenu>
<MenuItem Command="{Binding Path=DataContext.ValidateAllCommand, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ContextMenu}}, FallbackValue=9999999999}" Header="Copy" />
</ContextMenu>
</StackPanel.ContextMenu>
<TextBlock Foreground="{Binding Color, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Text="{Binding Message, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</UserControl>
My main problem is that I'm not able to access my "ValidateAllCommand" function, for some reason... I think it has to do with the "RelativeSource={RelativeSource FindAncestor ... " part but I can't figure out how.
CodePudding user response:
The "problem" with the ContextMenu
is that it is not part of the visual tree. The reason is that the MenuItem
uses a Popup
to host the content. While the Popup
itself is part of the visual tree, its Child content is disconnected as it is rendered in a new Window
instance. We know there can can only be a single Window
in the tree and this Window
must be the root.
Since Binding.RelativeSource
traverses the visual tree to find the binding source, the Bindig
does not resolve.
The Popup.Child
content inherits the DataContext
of the Popup
. In case of the ContextMenu
it means that the MenuItem
inherits the DataContext
from the ContextMenu
or from the parent of the ContextMenu
, to be more precise. In your scenario the DataContext
is the data model of the ListBoxItem
i.e. the DataContext
of the DataTemplate
.
This means, one solution could be to implement the command in the item model.
The second solution is to make use of routed commands. This solution might be more reasonable in your scenario. The routed command/event can cross the boundary between the two trees.
In the following example, I use the ApplicationCommands.Copy
command, one of the predefined routed commands. The MenuItem.Parameter
is bound to the DataContext
, which is the item data model (inherited from the ContextMenu
, as mentioned before). This way the command handler can know the data source.
The event handler can be attached to any parent element using the UIElement.CommandBindings
property. In the example the handler is attached to the ListBox
element:
MainWindow.xaml
<ListBox>
<ListBox.CommandBindings>
<CommandBinding Command="{x:Static ApplicationCommands.Copy}"
Executed="CopyCommand_Executed" />
</ListBox.CommandBindings>
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type local:DataItem}">
<StackPanel>
<StackPanel.ContextMenu>
<ContextMenu>
<!--
Here we are in a detached visual tree.
The DataContext is inherited from the ContextMenu element.
The framework allows routed events to cross the boundaries between the trees.
-->
<MenuItem Command="{x:Static ApplicationCommands.Copy}"
CommandParameter="{Binding}"
Header="Copy" />
</ContextMenu>
</StackPanel.ContextMenu>
<TextBlock />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
MainWindow.xaml.cs
private void CopyCommand_Executed(object sender, ExecutedRoutedEventArgs e)
{
var listBoxItemModel = e.Parameter as LogBufferItem;
// TODO::Handle Copy command. For example:
var commandTarget = this.DataContext as ICommandModel;
commandTarget.ValidateAllCommand.Execute(listBoxItemModel);
}
A third, but not recommended solution is to bind the DataContext
of the ContextMenu
parent to the context of interest:
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type local:DataItem}">
<StackPanel>
<!-- DataTemplate DataContext -->
<Grid DataContext="{Binding RelativeSource={RelativeSource AncestorType=ListBox}, Path=DataContext}">
<!-- ListBox DataContext -->
<Grid.ContextMenu>
<ContextMenu>
<!--
Here we are in a detached visual tree.
The DataContext is inherited from the ContextMenu/Grid element.
-->
<MenuItem Command="{Binding ValidateAllCommand}"
Header="Copy" />
</ContextMenu>
</Grid.ContextMenu>
</Grid>
<!-- DataTemplate DataContext -->
<TextBlock />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox