How do I bind the MenuItem of a ContextMenu of a D

2019-08-12 13:48发布

The application I'm working on uses a DataGrid to present entries to the user and these entries are grouped. The grouping is not tied to a single property of each entry, a single entry can be in multiple groups. The user is able to create groups at will and add entries to those groups.

We want the user to be able to edit the entries and the groups directly from this view. To remove a group, we'd like the user to be able to right click on the group and select "Delete group" from the context menu.

I've been able to give the GroupItem's Expander a context menu, but have no idea how to bind the Command or CommandParameter to the ViewModel.

How do I achieve the results I seek? I appreciate that this might require "moving" the context menu to a different part of the control, but we want a different context menu for the entries to the group headers. This we have achieved in our live code, but is not represented in the example below.

Here is a simplified example to represent what we are trying to achieve.

XAML:

<Window x:Class="DataGridHeaderTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">

    <Window.Resources>
        <CollectionViewSource x:Key="GroupedEntriesSource" Source="{Binding Entries}">
            <CollectionViewSource.GroupDescriptions>
                <PropertyGroupDescription PropertyName="Key"/>
            </CollectionViewSource.GroupDescriptions>
        </CollectionViewSource>

        <Style x:Key="GroupContainerStyle" TargetType="{x:Type GroupItem}">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type GroupItem}">
                        <Expander IsExpanded="True" Background="#414040">

                            <Expander.ContextMenu>
                                <ContextMenu>

                                    <!-- How do I bind Command and CommandParameter? -->
                                    <MenuItem Header="Delete group" Command="{Binding DeleteGroupCommand}" CommandParameter="{Binding}" />

                                </ContextMenu>
                            </Expander.ContextMenu>

                            <Expander.Header>
                                <Grid>
                                    <TextBlock Text="{Binding Path=Items[0].Key.Name}"/>
                                </Grid>
                            </Expander.Header>
                            <ItemsPresenter />
                        </Expander>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </Window.Resources>

    <Grid>
        <DataGrid ItemsSource="{Binding Source={StaticResource GroupedEntriesSource}}" AutoGenerateColumns="False">
            <DataGrid.GroupStyle>
                <GroupStyle ContainerStyle="{StaticResource GroupContainerStyle}">
                    <GroupStyle.Panel>
                        <ItemsPanelTemplate>
                            <DataGridRowsPresenter/>
                        </ItemsPanelTemplate>
                    </GroupStyle.Panel>
                </GroupStyle>
            </DataGrid.GroupStyle>

            <DataGrid.Columns>
                <DataGridTextColumn Header="Name" Binding="{Binding Value.Name, Mode=OneWay}"/>
                <DataGridTextColumn Header="Data" Binding="{Binding Value.Val, UpdateSourceTrigger=LostFocus}"/>
            </DataGrid.Columns>
        </DataGrid>
    </Grid>
</Window>

Code behind:

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Input;

namespace DataGridHeaderTest
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            CreateData();

            DeleteGroupCommand = new TestCommand(DeleteGroup);

            DataContext = this;
            InitializeComponent();
        }

        void CreateData()
        {
            Entries = new ObservableCollection<KeyValuePair<Group, Entry>>();

            Group group1 = new Group() { Name = "Group1" };
            Group group2 = new Group() { Name = "Group2" };
            Entry entry1 = new Entry() { Name = "Entry1", Val = "Val1" };
            Entry entry2 = new Entry() { Name = "Entry2", Val = "Val2" };
            Entry entry3 = new Entry() { Name = "Entry3", Val = "Val3" };

            Entries.Add(new KeyValuePair<Group, Entry>(group1, entry1));
            Entries.Add(new KeyValuePair<Group, Entry>(group1, entry3));
            Entries.Add(new KeyValuePair<Group, Entry>(group2, entry2));
            Entries.Add(new KeyValuePair<Group, Entry>(group2, entry3));
        }

        void DeleteGroup(object group)
        {
            // I want to run this when "Delete group" is selected from the context menu of the Group Expander.
            // I want the Group object associated with the Group Expander passed as the parameter
        }

        public ObservableCollection<KeyValuePair<Group, Entry>> Entries { get; set; }
        public ICommand DeleteGroupCommand { get; set; }

        public class Group
        {
            public string Name { get; set; }
        }

        public class Entry
        {
            public string Name { get; set; }
            public string Val { get; set; }
        }

        public class TestCommand : ICommand
        {
            public delegate void ICommandOnExecute(object parameter);

            private ICommandOnExecute _execute;

            public TestCommand(ICommandOnExecute onExecuteMethod)
            {
                _execute = onExecuteMethod;
            }

            public event EventHandler CanExecuteChanged
            {
                add { CommandManager.RequerySuggested += value; }
                remove { CommandManager.RequerySuggested -= value; }
            }

            public bool CanExecute(object parameter)
            {
                return true;
            }

            public void Execute(object parameter)
            {
                _execute.Invoke(parameter);
            }
        }
    }
}

3条回答
【Aperson】
2楼-- · 2019-08-12 14:35

lokusking's answer showed me how to bind the command correctly, but I still needed to bind the command parameter to the Group object.

I thought I could get access to the Group's Name property through the CollectionViewGroupInternal, but it would not have been a very good solution as the class is mostly inaccessible.

I have managed to make changes to lokusking's solution to bind the Tag of the Expander to the Tag of the ContextMenu rather than the DataContext of the Context menu. I can then bind the Command of the MenuItem to the Tag of the ContextMenu leaving the DataContext of the MenuItem intact for the CommandParameter.

Here is the relevant section of XAML in case it is of use to anybody:

<Style x:Key="GroupContainerStyle" TargetType="{x:Type GroupItem}">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type GroupItem}">                            
                <Expander IsExpanded="True" Background="#414040" Tag="{Binding ElementName=root, Path=DataContext}">
                    <Expander.ContextMenu>
                        <ContextMenu Tag="{Binding Path=PlacementTarget.Tag, RelativeSource={RelativeSource Self}}">
                            <MenuItem Header="Delete group"
                                        Command="{Binding Tag.DeleteGroupCommand, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=ContextMenu}}"
                                        CommandParameter="{Binding Items[0].Key}" />
                        </ContextMenu>
                    </Expander.ContextMenu>                            
                    <Expander.Header>
                        <Grid>
                            <TextBlock Text="{Binding Path=Items[0].Key.Name}"/>
                        </Grid>
                    </Expander.Header>
                    <ItemsPresenter />
                </Expander>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>
查看更多
迷人小祖宗
3楼-- · 2019-08-12 14:37

The problem here is that binding

Command="{Binding DeleteGroupCommand}"

does not get resolved. A simple solution is to make DeleteGroupCommand static and to refer to it as follows:

<MenuItem Header="Delete group" Command="{x:Static dataGridHeaderTest:MainWindow.DeleteGroupCommand}" CommandParameter="{Binding}" />

And this works fine. For further improvements you can check out related topic:WPF: Bind to command from ControlTemplate

查看更多
Emotional °昔
4楼-- · 2019-08-12 14:56

This should do the Trick:

XAML:

<Window x:Class="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"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525" x:Name="root">
    <Window.Resources>
            <CollectionViewSource x:Key="GroupedEntriesSource" Source="{Binding Entries}">
                <CollectionViewSource.GroupDescriptions>
                    <PropertyGroupDescription PropertyName="Key"/>
                </CollectionViewSource.GroupDescriptions>
            </CollectionViewSource>


            <Style x:Key="GroupContainerStyle" TargetType="{x:Type GroupItem}">
                <Setter Property="Template">
                    <Setter.Value>
                        <ControlTemplate TargetType="{x:Type GroupItem}">
                            <Expander IsExpanded="True" Background="#414040" Tag="{Binding ElementName=root, Path=DataContext}">
                                <Expander.ContextMenu>
                                    <ContextMenu DataContext="{Binding Path=PlacementTarget.Tag, RelativeSource={RelativeSource Self}}">
                                        <MenuItem Header="Delete group" Command="{Binding DeleteGroupCommand}" CommandParameter="{TemplateBinding DataContext}" />

                                    </ContextMenu>
                                </Expander.ContextMenu>

                                <Expander.Header>
                                    <Grid>
                                        <TextBlock Text="{Binding Path=Items[0].Key.Name}"/>
                                    </Grid>
                                </Expander.Header>
                                <ItemsPresenter />
                            </Expander>
                        </ControlTemplate>
                    </Setter.Value>
                </Setter>
            </Style>
        </Window.Resources>

        <Grid>
            <DataGrid ItemsSource="{Binding Source={StaticResource GroupedEntriesSource}}" AutoGenerateColumns="False">
                <DataGrid.GroupStyle>
                    <GroupStyle ContainerStyle="{StaticResource GroupContainerStyle}">
                        <GroupStyle.Panel>
                            <ItemsPanelTemplate>
                                <DataGridRowsPresenter/>
                            </ItemsPanelTemplate>
                        </GroupStyle.Panel>
                    </GroupStyle>
                </DataGrid.GroupStyle>

                <DataGrid.Columns>
                    <DataGridTextColumn Header="Name" Binding="{Binding Value.Name, Mode=OneWay}"/>
                    <DataGridTextColumn Header="Data" Binding="{Binding Value.Val, UpdateSourceTrigger=LostFocus}"/>
                </DataGrid.Columns>
            </DataGrid>
        </Grid>
</Window>

Code-Behind:

public partial class MainWindow : Window {
    public MainWindow() {
      InitializeComponent();
      CreateData();

      DeleteGroupCommand = new TestCommand(DeleteGroup);

      DataContext = this;

    }

    void CreateData() {
      Entries = new ObservableCollection<KeyValuePair<Group, Entry>>();

      Group group1 = new Group() { Name = "Group1" };
      Group group2 = new Group() { Name = "Group2" };
      Entry entry1 = new Entry() { Name = "Entry1", Val = "Val1" };
      Entry entry2 = new Entry() { Name = "Entry2", Val = "Val2" };
      Entry entry3 = new Entry() { Name = "Entry3", Val = "Val3" };

      Entries.Add(new KeyValuePair<Group, Entry>(group1, entry1));
      Entries.Add(new KeyValuePair<Group, Entry>(group1, entry3));
      Entries.Add(new KeyValuePair<Group, Entry>(group2, entry2));
      Entries.Add(new KeyValuePair<Group, Entry>(group2, entry3));
    }

    void DeleteGroup(object group) {
      // I want to run this when "Delete group" is selected from the context menu of the Group Expander.
      // I want the Group object associated with the Group Expander passed as the parameter
    }

    public ObservableCollection<KeyValuePair<Group, Entry>> Entries {
      get; set;
    }
    public ICommand DeleteGroupCommand { get; }

    public class Group {
      public string Name {
        get; set;
      }
    }

    public class Entry {
      public string Name {
        get; set;
      }
      public string Val {
        get; set;
      }
    }


  }

  public class TestCommand : ICommand {
    public delegate void ICommandOnExecute(object parameter);

    private ICommandOnExecute _execute;

    public TestCommand(ICommandOnExecute onExecuteMethod) {
      _execute = onExecuteMethod;
    }

    public event EventHandler CanExecuteChanged {
      add {
        CommandManager.RequerySuggested += value;
      }
      remove {
        CommandManager.RequerySuggested -= value;
      }
    }

    public bool CanExecute(object parameter) {
      return true;
    }

    public void Execute(object parameter) {
      _execute.Invoke(parameter);
    }
  }

You probably have to edit the Binding of the CommandParameter so it will fit your needs

EDIT:

Fixed the Xaml to enable proper Copy-Paste

查看更多
登录 后发表回答