Home > Software design >  Bind a ContextMenu Command inside of a ItemSource to a command inside the ViewModel
Bind a ContextMenu Command inside of a ItemSource to a command inside the ViewModel

Time:08-13

I have a ItemsControl control, in which there is ContextMenu control.
The ItemsControl has its ItemsSource bound to a List<Person>.

What I want to do is to bind a DisplayNameCommand and a DisplaySurnameCommand to their corresponding context menu item, where both commands are inside of a MainWindowViewModel - not the bound Person object!!!.

The important thing is that ContextMenu needs to still have the ItemsSource data context, as I need to acces the bound object property to include it in the command parameter.

ItemsControl:

<ItemsControl ItemsSource="{Binding PeopleList}">
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="{Binding Name}"/>
                <TextBlock Text="{Binding Surname}"/>
                <Image>
                    <Image.ContextMenu>
                        <ContextMenu>
                            <MenuItem Header="Display Name" 
                                        Command="{Binding DisplaySurnameCommand}" 
                                        CommandParameter="{Binding Name}">
                            </MenuItem>
                            <MenuItem Header="Display Surname" 
                                        Command="{Binding DisplaySurnameCommand}" 
                                        CommandParameter="{Binding Surname}">
                            </MenuItem>
                        </ContextMenu>
                    </Image.ContextMenu>
                </Image>
            </StackPanel>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

MainWindowViewModel and Person class:

    public class MainWindowViewModel
    {
        public MainWindowViewModel()
        {
            DisplayNameCommand = new RelayCommand(DisplayName);
            DisplaySurnameCommand = new RelayCommand(DisplaySurname);

            PeopleList = new List<Person>();

            PeopleList.Add(new Person("Julie", "Adams"));
            PeopleList.Add(new Person("Mack", "McMack"));
            PeopleList.Add(new Person("Josh", "Broccoli"));
        }

        public List<Person> PeopleList { get; set; }

        public void DisplayName(object message)
        {
            MessageBox.Show("Name: "   (string)message);
        }

        public void DisplaySurname(object message)
        {
            MessageBox.Show("Surname: "  (string)message);
        }

        public RelayCommand DisplayNameCommand { get; }
        public RelayCommand DisplaySurnameCommand { get; }
    }

    public class Person
    {
        public Person(string name, string surname)
        {
            Name = name;
            Surname = surname;
        }

        public string Name { get; set; }
        public string Surname { get; set; }
    }

Also I know that it is possible to bind it to a Person object and then point it to a viewmodel command, but that's not what I'm looking for.

I've created a demo project for this problem.

What I have tried

1. Specify Command for MenuItem in a DataTemplate (accepted answer)

<ContextMenu>
    <ContextMenu.ItemContainerStyle>
        <Style TargetType="MenuItem">
            <Setter Property="Command" Value="{Binding DataContext.DisplaySurname, RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=Window}}"/>
            <Setter Property="CommandParameter" Value="{Binding Name}"/>
        </Style>
    </ContextMenu.ItemContainerStyle>

    <MenuItem Header="Display Name">
    <MenuItem Header="Display Surname">
</ContextMenu>

So this is the closest that I got to the result, this does trigger the command but the problem is that there can be only 1 command set for all menu items.
If there could be a way to get around this by setting the name for the style or using something else than ItemContainerStyle it could work, but I couldn't come up with anything like that.

2. Set a relative command

<ContextMenu>
    <MenuItem Header="Display Name"
                Command="{Binding DataContext.DisplayNameCommand, RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=Window}}"
                CommandParameter="{Binding Name}"/>
    <MenuItem Header="Display Surname"
                Command="{Binding DataContext.DisplaySurnameCommand, RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=Window}}"
                CommandParameter="{Binding Surname}"/>
</ContextMenu>

This returns a binding error:
Cannot find source: RelativeSource FindAncestor, AncestorType='System.Windows.Window', AncestorLevel='1'.

3. Can't bind a ContextMenu action to a Command

The accepted answer first bound to a person object, and the updated solution didn't even work, but the second answer that I've tried:

<MenuItem Header="Display Surname"
            Command="{Binding RelativeSource={RelativeSource AncestorType={x:Type Window}}, Path=DisplaySurnameCommand}"
            CommandParameter="{Binding Surname}"/>

returned a binding error of:
Cannot find source: RelativeSource FindAncestor, AncestorType='System.Windows.Window', AncestorLevel='1'.

... and besides those three I've tried many more soltuions and variations, with little to no result.

I've spent a whole day trying to find a solution to this, please help me, wpf is taking away my sanity.

Ps. this is my first post so if you have any comments then let me know.

CodePudding user response:

Alright, on the next day I found a workaround to this based on this solution. And it works great, the comamnds trigger, but - it changes the DataContext of the context menu , so the bound Person object is no longer available. So, to get around this, to acces the current object I've added a Person property inside of the ViewModel, that updates whenever the user clicks a image that opens a context menu. So in that way you know what item has been clicked.

This solution will work to both acces the bound object, and will work on any number of commands.
Not the preetiest solution, but it works great.

Working project demo

Xaml:

<ItemsControl ItemsSource="{Binding PeopleList}">
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="{Binding Name}"/>
                <TextBlock Text="{Binding Surname}"/>
                <Image Source="more_64px.png" Tag="{Binding DataContext, RelativeSource={RelativeSource AncestorType=Window}}" >
                    <Image.InputBindings>
                        <MouseBinding Gesture="LeftClick" Command="{Binding DataContext.UpdateCurrentObjectCommand, RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=Window}}" CommandParameter="{Binding}"/>
                        <MouseBinding Gesture="RightClick" Command="{Binding DataContext.UpdateCurrentObjectCommand, RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=Window}}" CommandParameter="{Binding}"/>
                    </Image.InputBindings>
                    <Image.ContextMenu>
                        <ContextMenu DataContext="{Binding Path=PlacementTarget.Tag, RelativeSource={x:Static RelativeSource.Self}}">
                            <MenuItem Header="Display Name" Command="{Binding DisplayNameCommand}"/>
                            <MenuItem Header="Display Surname" Command="{Binding DisplaySurnameCommand}"/>
                        </ContextMenu>
                    </Image.ContextMenu>
                </Image>
            </StackPanel>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

ViewModel with Person class:

public class MainWindowViewModel
    {
        public MainWindowViewModel()
        {
            DisplayNameCommand = new RelayCommand(DisplayName);
            DisplaySurnameCommand = new RelayCommand(DisplaySurname);

            PeopleList = new List<Person>();

            PeopleList.Add(new Person("Julie", "Adams"));
            PeopleList.Add(new Person("Mack", "McMack"));
            PeopleList.Add(new Person("Josh", "Broccoli"));

            UpdateCurrentObjectCommand = new RelayCommand(UpdateCurrentObject);
        }

        private void UpdateCurrentObject(object person)
        {
            CurrentPerson = (Person)person;
        }

        private Person CurrentPerson { get; set; }

        public List<Person> PeopleList { get; set; }

        public void DisplayName()
        {
            MessageBox.Show("Name: "   CurrentPerson.Name);
        }

        public void DisplaySurname()
        {
            MessageBox.Show("Name: "   CurrentPerson.Surname);
        }

        public RelayCommand DisplayNameCommand { get; }
        public RelayCommand DisplaySurnameCommand { get; }
        public RelayCommand UpdateCurrentObjectCommand { get; }
    }

    public class Person
    {
        public Person(string name, string surname)
        {
            Name = name;
            Surname = surname;
        }

        public string Name { get; set; }
        public string Surname { get; set; }
    }

If you have any questions or comments, lemme know!

  • Related