简体   繁体   中英

Header-specific context menu binding in WPF

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.

Try this.

<DataGridTextColumn Header="Column 1 Data" Binding="{Binding Column1Data}" Width="Auto">
    <DataGridTextColumn.HeaderTemplate>
        <DataTemplate>
            <TextBlock Name="Header1TextBlock" Text="{TemplateBinding Content}">
                <TextBlock.ContextMenu>
                    <ContextMenu>
                        <CheckBox Name="Header1Checkbox" Content="Header Menu 1" IsChecked="{Binding DataContext.Column1Checked, RelativeSource={RelativeSource AncestorType=Window}, Mode=TwoWay}"/>
                    </ContextMenu>
                </TextBlock.ContextMenu>
            </TextBlock>
        </DataTemplate>
    </DataGridTextColumn.HeaderTemplate>
</DataGridTextColumn>

I am not too sure if it works because your ContextMenu is defined inside a Template.

I finally found what I needed here:
WPF ContextMenu woes: How do I set the DataContext of the ContextMenu?
Thanks to daub815

This is what worked for me:

                <DataGridTextColumn Header="Column 1 Data" Binding="{Binding Column1Data}" Width="Auto">
                <DataGridTextColumn.HeaderTemplate>
                    <DataTemplate>
                        <TextBlock Name="Header1TextBlock" Text="{TemplateBinding Content}" Tag="{Binding DataContext, ElementName=MyDataGrid}">
                            <TextBlock.ContextMenu>
                                <ContextMenu DataContext="{Binding Path=PlacementTarget.Tag, RelativeSource={RelativeSource Self}}">
                                    <CheckBox Name="Header1Checkbox" Content="Header Menu 1" IsChecked="{Binding Column1Checked, Mode=TwoWay}"/>
                                </ContextMenu>
                            </TextBlock.ContextMenu>
                        </TextBlock>
                    </DataTemplate>
                </DataGridTextColumn.HeaderTemplate>
            </DataGridTextColumn>

(Where MyDataGrid is the DataGrid object)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM