I'm trying to construct context menus for a WPF datagrid which are header-specific (in other words, each column header could have it's own context menu different from the other headers). In addition, the data backing the context menu is bound data. My problem is that I can't seem to connect my menu to my data context. I've tried a few suggestions from here and elsewhere but so far no luck.
I know that the context menu isn't within the visual tree of the rest of the document so I've tried using it's PlacementTarget (as you can see below) but when I click on a column header, my PlacementTarget is only the TextBlock of the header. How do I get from there to the DataContext of the grid?
This is an example of what I've tried:
<Window x:Class="ContextMenuExample.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:ContextMenuExample"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<local:ExampleViewModel />
</Window.DataContext>
<Grid>
<DataGrid ItemsSource="{Binding MyGridData}" IsReadOnly="True" AutoGenerateColumns="False">
<!-- THESE WORK, BUT THEY ARE GRID-GLOBAL. I WANT HEADER-SPECIFIC CONTEXT MENUS -->
<DataGrid.ContextMenu>
<ContextMenu>
<CheckBox Name="GridCheckbox1" Content="Grid Menu - Item1" IsChecked="{Binding Column1Checked, Mode=TwoWay}"/>
<CheckBox Name="GridCheckbox2" Content="Grid Menu - Item2" IsChecked="{Binding Column2Checked, Mode=TwoWay}"/>
<CheckBox Name="GridCheckbox3" Content="Grid Menu - Item3" IsChecked="{Binding Column3Checked, Mode=TwoWay}"/>
</ContextMenu>
</DataGrid.ContextMenu>
<DataGrid.Columns>
<DataGridTextColumn Header="Column 1 Data" Binding="{Binding Column1Data}" Width="Auto">
<!-- ATTEMPT 1 - JUST SEE IF I CAN BIND DIRECTLY. SHOULDN'T WORK. -->
<!-- System.Windows.Data Error: 40 : BindingExpression path error: 'Column1Checked' property not found on 'object' ''String' (HashCode=-1586790989)'. BindingExpression:Path=Column1Checked; DataItem='String' (HashCode=-1586790989); target element is 'CheckBox' (Name='Header1Checkbox'); target property is 'IsChecked' (type 'Nullable`1') -->
<DataGridTextColumn.HeaderTemplate>
<DataTemplate>
<TextBlock Name="Header1TextBlock" Text="{TemplateBinding Content}" >
<TextBlock.ContextMenu>
<ContextMenu>
<CheckBox Name="Header1Checkbox" Content="Header Menu 1" IsChecked="{Binding Column1Checked, Mode=TwoWay}"/>
</ContextMenu>
</TextBlock.ContextMenu>
</TextBlock>
</DataTemplate>
</DataGridTextColumn.HeaderTemplate>
</DataGridTextColumn>
<DataGridTextColumn Header="Column 2 Data" Binding="{Binding Column1Data}" Width="Auto">
<!-- ATTEMPT 2 - BIND TO THE PLACEMENT TARGET. THIS MAKES THE TextBlock MY DATA CONTEXT. CLOSER (?) BUT STILL WRONG -->
<!-- System.Windows.Data Error: 40 : BindingExpression path error: 'Column2Checked' property not found on 'object' ''TextBlock' (Name='Header2TextBlock')'. BindingExpression:Path=Column2Checked; DataItem='TextBlock' (Name='Header2TextBlock'); target element is 'CheckBox' (Name='Header2Checkbox'); target property is 'IsChecked' (type 'Nullable`1') -->
<DataGridTextColumn.HeaderTemplate>
<DataTemplate>
<TextBlock Name="Header2TextBlock" Text="{TemplateBinding Content}" >
<TextBlock.ContextMenu>
<ContextMenu DataContext="{Binding PlacementTarget, RelativeSource={RelativeSource Self}}">
<CheckBox Name="Header2Checkbox" Content="Header Menu 2" IsChecked="{Binding Column2Checked, Mode=TwoWay}"/>
</ContextMenu>
</TextBlock.ContextMenu>
</TextBlock>
</DataTemplate>
</DataGridTextColumn.HeaderTemplate>
</DataGridTextColumn>
<DataGridTextColumn Header="Column 3 Data" Binding="{Binding Column1Data}" Width="Auto">
<!-- ATTEMPT 3 - BIND TO THE PLACEMENT TARGETS' CONTEXT. I'M NOT EVEN SURE WHAT THIS IS, BUT IT DOESN'T WORK -->
<!-- System.Windows.Data Error: 40 : BindingExpression path error: 'Column3Checked' property not found on 'object' ''String' (HashCode=975011251)'. BindingExpression:Path=Column3Checked; DataItem='String' (HashCode=975011251); target element is 'CheckBox' (Name='Header3Checkbox'); target property is 'IsChecked' (type 'Nullable`1') -->
<DataGridTextColumn.HeaderTemplate>
<DataTemplate>
<TextBlock Name="Header3TextBlock" Text="{TemplateBinding Content}" >
<TextBlock.ContextMenu>
<ContextMenu DataContext="{Binding PlacementTarget.DataContext, RelativeSource={RelativeSource Self}}">
<CheckBox Name="Header3Checkbox" Content="Header Menu 3" IsChecked="{Binding Column3Checked, Mode=TwoWay}"/>
</ContextMenu>
</TextBlock.ContextMenu>
</TextBlock>
</DataTemplate>
</DataGridTextColumn.HeaderTemplate>
</DataGridTextColumn>
</DataGrid.Columns>
</DataGrid>
</Grid>
class GridData
{
public int Column1Data { get; set; }
public int Column2Data { get; set; }
public int Column3Data { get; set; }
}
class ExampleViewModel : INotifyPropertyChanged
{
private bool column1Checked;
private bool column2Checked;
private bool column3Checked;
public event PropertyChangedEventHandler PropertyChanged;
public List<GridData> MyGridData
{
get
{
return new List<GridData>
{
new GridData() { Column1Data = 1, Column2Data = 2, Column3Data = 3},
new GridData() { Column1Data = 4, Column2Data = 5, Column3Data = 6},
new GridData() { Column1Data = 7, Column2Data = 8, Column3Data = 9}
};
}
}
public bool Column1Checked
{
get { return column1Checked; }
set {
if (column1Checked != value)
{
column1Checked = value;
PropertyChanged(this, new PropertyChangedEventArgs(nameof(Column1Checked)));
}
}
}
public bool Column2Checked
{
get { return column2Checked; }
set {
if (column2Checked != value)
{
column2Checked = value;
PropertyChanged(this, new PropertyChangedEventArgs(nameof(Column2Checked)));
}
}
}
public bool Column3Checked
{
get { return column3Checked; }
set {
if (column3Checked != value)
{
column3Checked = value;
PropertyChanged(this, new PropertyChangedEventArgs(nameof(Column3Checked)));
}
}
}
}
So my question is... How can I bind my context menus to my ViewModel?
Thanks for any help you can offer.