WPF DataGrid: Dynamically-Generated ContextMenu for Individual Rows.

Stout 286 Reputation points
2020-09-21T23:35:02.277+00:00

Hi. I have a WPF DataGrid that displays a bunch of rows. Each row has a right-click ContextMenu with its own commands.

I'm not able to get the ContextMenu part right. It's complaining that "ContextMenu cannot have a logical or visual parent." Any help would be appreciated.

Thanks.

XAML File:

  <DataGrid ItemsSource="{Binding MyModules}" AutoGenerateColumns="False">

      <!-- Begin uncertain part -->

      <ContextMenu ItemsSource="{Binding Configuration.Commands}">
          <ContextMenu.ItemContainerStyle>
              <Style TargetType="x:Type ContextMenuItem">
                  <Setter Property="Command" Value="Binding Command}" />
              </Style>
          </ContextMenu.ItemContainerStyle>
      </ContextMenu>

      <!-- End uncertain part -->

      <DataGrid.Columns>
          <DataGridTemplateColumn Header="Module Name" Width="*" IsReadOnly="True">
              <DataGridTemplateColumn.CellTemplate>
                  <DataTemplate>
                      <TextBlock Text="{Binding Configuration.Name}" />
                  </DataTemplate>
              </DataGridTemplateColumn.CellTemplate>
          </DataGridTemplateColumn>

          <DataGridTemplateColumn Header="Module Description" Width="3*" IsReadOnly="True">
              <DataGridTemplateColumn.CellTemplate>
                  <DataTemplate>
                      <TextBlock Text="{Binding Configuration.Description}" />
                  </DataTemplate>
              </DataGridTemplateColumn.CellTemplate>
          </DataGridTemplateColumn>

      </DataGrid.Columns>

ViewModel Files:

  public ObservableCollection<IModule> MyModules { get; set; }

  // ...

  public interface IModule
  {
      Configuration Configuration { get; }
      void ExecuteCommand(Command command);
  }

  public class Configuration
  {
      public Guid Id { get; private set; }
      public string Name { get; private set; }
      public string Description { get; private set; }
      public List<Command> Commands = new List<Command>();

      public Configuration(string name, string description, List<Command> commands)
      {
          Id = new Guid();
          Name = name;
          Description= description;
          Commands = commands;
      }
  }
Windows Presentation Foundation
Windows Presentation Foundation
A part of the .NET Framework that provides a unified programming model for building line-of-business desktop applications on Windows.
2,635 questions
0 comments No comments
{count} votes

Accepted answer
  1. Peter Fleischer (former MVP) 19,056 Reputation points
    2020-09-22T12:59:36.23+00:00

    Hi,
    try this another approach:

    XAML:

    <Window x:Class="WpfApp1.Window83"  
            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:WpfApp83"  
            mc:Ignorable="d"  
            Title="Window83" Height="450" Width="800">  
      <Window.Resources>  
        <local:ViewModel x:Key="vm"/>  
      </Window.Resources>  
      <Grid DataContext="{StaticResource vm}">  
        <Grid.RowDefinitions>  
          <RowDefinition/>  
          <RowDefinition Height="auto"/>  
        </Grid.RowDefinitions>  
        <DataGrid ItemsSource="{Binding MyModules}" AutoGenerateColumns="False">  
          <DataGrid.Resources>  
            <ContextMenu x:Key="RowMenu" ItemsSource="{Binding MenuNames}" >  
              <ContextMenu.ItemTemplate>  
                <DataTemplate>  
                  <Grid Margin="2" >  
                    <MenuItem Header="{Binding}"   
                              Command="{Binding Source={StaticResource vm}}"  
                              CommandParameter="{Binding}"/>  
                  </Grid>  
                </DataTemplate>  
              </ContextMenu.ItemTemplate>  
            </ContextMenu>  
          </DataGrid.Resources>  
          <DataGrid.RowStyle>  
            <Style TargetType="DataGridRow" >  
              <Setter Property="ContextMenu" Value="{StaticResource RowMenu}" />  
            </Style>  
          </DataGrid.RowStyle>  
          <DataGrid.Columns>  
            <DataGridTemplateColumn Header="Module Name" Width="*" IsReadOnly="True">  
              <DataGridTemplateColumn.CellTemplate>  
                <DataTemplate>  
                  <TextBlock Text="{Binding Configuration.Name}" />  
                </DataTemplate>  
              </DataGridTemplateColumn.CellTemplate>  
            </DataGridTemplateColumn>  
            <DataGridTemplateColumn Header="Module Description" Width="3*" IsReadOnly="True">  
              <DataGridTemplateColumn.CellTemplate>  
                <DataTemplate>  
                  <TextBlock Text="{Binding Configuration.Description}" />  
                </DataTemplate>  
              </DataGridTemplateColumn.CellTemplate>  
            </DataGridTemplateColumn>  
          </DataGrid.Columns>  
        </DataGrid>  
        <Label Grid.Row="1" Content="{Binding Info}"/>  
      </Grid>  
    </Window>  
    

    And classes:

    using System;  
    using System.Collections.Generic;  
    using System.Collections.ObjectModel;  
    using System.ComponentModel;  
    using System.Runtime.CompilerServices;  
    using System.Windows;  
    using System.Windows.Data;  
    using System.Windows.Input;  
      
    namespace WpfApp83  
    {  
      public class ViewModel : ICommand, INotifyPropertyChanged  
      {  
        public ViewModel() => LoadData();  
        public CollectionViewSource cvs = new CollectionViewSource();  
        public ICollectionView MyModules { get => cvs.View; }  
      
        private void LoadData()  
        {  
          Random rnd = new Random();  
          ObservableCollection<Module> col = new ObservableCollection<Module>();  
          for (int i = 1; i < 10; i++)  
          {  
            Module m = new Module();  
            m.Configuration.Name = $"Name {i}";  
            m.Configuration.Description = $"Description {i}";  
            for (int k = 1; k < rnd.Next(3, 8); k++) m.MenuNames.Add($"Menu {k} (Name {i})");  
            col.Add(m);  
          }  
          cvs.Source = col;  
        }  
      
        public string Info { get; set; }  
        public void Execute(object parameter)  
        {  
          Info = parameter.ToString();  
          OnPropertyChanged(nameof(Info));  
        }  
      
        public event EventHandler CanExecuteChanged;  
        public bool CanExecute(object parameter) => true;  
      
        public event PropertyChangedEventHandler PropertyChanged;  
        private void OnPropertyChanged([CallerMemberName] string propName = "") =>  
          PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));  
      }  
      
      public class Module  
      {  
        public Config Configuration { get; set; } = new Config();  
        public List<string> MenuNames { get; set; } = new List<string>();  
      }  
      
      public class Config  
      {  
        public string Name { get; set; }  
        public string Description { get; set; }  
      }  
    }  
    

    26465-x.gif

    0 comments No comments

4 additional answers

Sort by: Most helpful
  1. DaisyTian-1203 11,611 Reputation points
    2020-09-22T07:38:02.25+00:00

    I will show the parts of my demo of generating ContextMenu for Individual rows Dynamically.
    Part1: Code for class MyMenuItem and Member:

    public class MyMenuItem   
        {  
            public string headerName;  
            public string HeaderName  
            {  
                get  
                {  
                    return headerName;  
                }  
                set  
                {  
                    headerName = value;                  
                }  
            }     
        }  
    
    public class Member : INotifyPropertyChanged  
        {  
      
            ObservableCollection<MyMenuItem> myMenuLists = new ObservableCollection<MyMenuItem>();  
            public ObservableCollection<MyMenuItem> MyMenuLists  
            {  
      
                get { return myMenuLists; }  
                set  
                {  
                    myMenuLists = value;  
                    NotifyPropertyChanged("MyMenuLists");  
                }  
            }  
      
            public string slotNo;  
            public string SlotNo  
            {  
                get  
                {  
                    return slotNo;  
                }  
                set  
                {  
                    slotNo = value;  
                    NotifyPropertyChanged("SlotNo");  
                }  
            }  
      
            public string mumber;  
            public string Number  
            {  
                get  
                {  
                    return mumber;  
                }  
                set  
                {  
                    mumber = value;  
                    NotifyPropertyChanged("Number");  
                }  
            }  
            public string magazinmenge;  
            public string Magazinmenge  
            {  
                get  
                {  
                    return magazinmenge;  
                }  
                set  
                {  
                    magazinmenge = value;  
                    NotifyPropertyChanged("Magazinmenge");  
                }  
            }  
      
            public event PropertyChangedEventHandler PropertyChanged;  
            public void NotifyPropertyChanged(string propertyName)  
            {  
                if (PropertyChanged != null)  
                {  
                    PropertyChanged(this, new PropertyChangedEventArgs(propertyName));  
                }  
            }  
    

    Part2: Code for ViewModel.cs:

    public class ViewModel : INotifyPropertyChanged  
        {  
            ObservableCollection<Member> _mylist = new ObservableCollection<Member>();  
            public ObservableCollection<Member> mylist  
            {  
      
                get { return _mylist; }  
                set  
                {  
                    _mylist = value;  
                    NotifyPropertyChanged("mylist");  
                }  
            }  
      
              
      
            public ViewModel()  
            {  
                ObservableCollection<MyMenuItem> menuItems1 = new ObservableCollection<MyMenuItem>() { new MyMenuItem { HeaderName="Header1-1"}, new MyMenuItem { HeaderName = "Header1-2" } };  
                ObservableCollection<MyMenuItem> menuItems2 = new ObservableCollection<MyMenuItem>() { new MyMenuItem { HeaderName="Header2-1"}, new MyMenuItem { HeaderName = "Header3-2" } };  
                ObservableCollection<MyMenuItem> menuItems3 = new ObservableCollection<MyMenuItem>() { new MyMenuItem { HeaderName="Header3-1"}, new MyMenuItem { HeaderName = "Header3-2" }, new MyMenuItem { HeaderName = "Header3-3" } };  
                  
      
                mylist = new ObservableCollection<Member>()  
                {  
                    new Member(){SlotNo = "1",Number ="Number01",Magazinmenge = "250", MyMenuLists=menuItems1},  
                    new Member(){SlotNo = "2",Number ="Number02",Magazinmenge= "250", MyMenuLists=menuItems2},  
                    new Member(){SlotNo = "3",Number ="Number03",Magazinmenge= "250", MyMenuLists=menuItems3}  
                };  
      
      
            }  
            public event PropertyChangedEventHandler PropertyChanged;  
            public void NotifyPropertyChanged(string propertyName)  
            {  
                if (PropertyChanged != null)  
                {  
                    PropertyChanged(this, new PropertyChangedEventArgs(propertyName));  
                }  
            }  
        }  
    

    Part3: Code for MainWindow.xaml

     <Window.DataContext>  
            <local:ViewModel></local:ViewModel>  
        </Window.DataContext>  
      
        <Grid>  
            <DataGrid x:Name="dataGrid"  ItemsSource="{Binding mylist}" AutoGenerateColumns="False"  SelectionMode="Extended" SelectionUnit="FullRow" >  
                <DataGrid.Resources>  
                    <ContextMenu x:Key="RowMenu" Name="myMenu"  ItemsSource="{Binding MyMenuLists}" >  
                        <ContextMenu.ItemTemplate>  
                            <DataTemplate>  
                                <Grid Margin="2" >  
                                    <MenuItem Header="{Binding HeaderName}" />  
                                </Grid>  
                            </DataTemplate>  
                        </ContextMenu.ItemTemplate>  
                    </ContextMenu>  
                </DataGrid.Resources>  
                <DataGrid.RowStyle>  
                    <Style TargetType="DataGridRow" >  
                        <Setter Property="ContextMenu" Value="{StaticResource RowMenu}" />  
                    </Style>  
                </DataGrid.RowStyle>  
      
                
                <DataGrid.Columns>  
                    <DataGridTextColumn Header="SloNumber" Width="80" Binding="{Binding SlotNo}"/>  
                    <DataGridTextColumn Header="MaterialNumer" Width="50" Binding="{Binding Number}" />  
                    <DataGridTextColumn Header="Magazinmenge" Width="50" Binding="{Binding Magazinmenge}" />  
                </DataGrid.Columns>  
            </DataGrid>  
        </Grid>  
    

    **Part4:**My demo's result picture is like below:
    26340-4.gif


    If the response is helpful, please click "Accept Answer" and upvote it.
    Note: Please follow the steps in our documentation to enable e-mail notifications if you want to receive the related email notification for this thread.

    1 person found this answer helpful.
    0 comments No comments

  2. Stout 286 Reputation points
    2020-09-22T20:13:41.883+00:00

    Thank you both for the samples. I've felt like Peter's solution was a bit easier to understand at least for me. But both samples were really informative and have helped me a lot. Appreciate your help.


  3. Peter Fleischer (former MVP) 19,056 Reputation points
    2020-09-23T07:38:00.57+00:00

    Hi,
    another approach with your code:

    <Window x:Class="WpfApp1.Window84"
            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:WpfApp84"
            mc:Ignorable="d"
            Title="MainWindow" Height="450" Width="800">
      <Window.Resources>
        <local:ViewModel x:Key="vm"/>
      </Window.Resources>
      <Grid DataContext="{StaticResource vm}">
        <DataGrid ItemsSource="{Binding MyModules}" AutoGenerateColumns="False">
          <DataGrid.Resources>
            <ContextMenu x:Key="RowMenu" ItemsSource="{Binding Configuration.Commands}" >
              <ContextMenu.ItemTemplate>
                <DataTemplate>
                  <Grid Margin="2" >
                    <MenuItem Header="{Binding Header}" 
                              Command="{Binding Source={StaticResource vm}}"
                              CommandParameter="{Binding}"/>
                  </Grid>
                </DataTemplate>
              </ContextMenu.ItemTemplate>
            </ContextMenu>
          </DataGrid.Resources>
          <DataGrid.RowStyle>
            <Style TargetType="DataGridRow" >
              <Setter Property="ContextMenu" Value="{StaticResource RowMenu}" />
            </Style>
          </DataGrid.RowStyle>
          <DataGrid.Columns>
            <DataGridTemplateColumn Header="Module Name" Width="*" IsReadOnly="True">
              <DataGridTemplateColumn.CellTemplate>
                <DataTemplate>
                  <TextBlock Text="{Binding Configuration.Name}" />
                </DataTemplate>
              </DataGridTemplateColumn.CellTemplate>
            </DataGridTemplateColumn>
            <DataGridTemplateColumn Header="Module Description" Width="3*" IsReadOnly="True">
              <DataGridTemplateColumn.CellTemplate>
                <DataTemplate>
                  <TextBlock Text="{Binding Configuration.Description}" />
                </DataTemplate>
              </DataGridTemplateColumn.CellTemplate>
            </DataGridTemplateColumn>
          </DataGrid.Columns>
        </DataGrid>
      </Grid>
    </Window>
    

    And classes:

    using System;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.Windows;
    using System.Windows.Input;
    
    namespace WpfApp84
    {
      public class ViewModel : ICommand
      {
        public ViewModel() => LoadData();
        public ObservableCollection<IModule> MyModules { get; set; }
    
        private void LoadData()
        {
          Random rnd = new Random();
          this.MyModules = new ObservableCollection<IModule>();
          for (int i = 1; i < 10; i++)
          {
            List<Command> cmds = new List<Command>();
            Module m = new Module($"Name {i}", $"Description {i}", cmds);
            for (int k = 1; k < rnd.Next(3, 8); k++)
              cmds.Add(new Command() { Header = $"Menu {k} (Name {i})", Parent = m });
            this.MyModules.Add(m);
          }
        }
    
        public void Execute(object parameter) =>
          ((Command)parameter).Parent.ExecuteCommand((Command)parameter);
    
        public event EventHandler CanExecuteChanged;
        public bool CanExecute(object parameter) => true;
      }
    
      public class Module : IModule
      {
        public Module(string name, string description, List<Command> commands) =>
          this.Configuration = new Configuration(name, description, commands);
    
        public Configuration Configuration { get; }
    
        public void ExecuteCommand(Command command) =>
          MessageBox.Show($"Selected menu item: {command.Header}");
      }
    
      public interface IModule
      {
        Configuration Configuration { get; }
        void ExecuteCommand(Command command);
      }
    
      public class Configuration
      {
        public Guid Id { get; private set; }
        public string Name { get; private set; }
        public string Description { get; private set; }
        public List<Command> Commands { get; set; } = new List<Command>();
    
        public Configuration(string name, string description, List<Command> commands)
        {
          Id = new Guid();
          Name = name;
          Description = description;
          Commands = commands;
        }
      }
    
      public class Command
      {
        public string Header { get; set; }
        public Module Parent { get; set; }
      }
    }
    
    0 comments No comments

  4. Peter Fleischer (former MVP) 19,056 Reputation points
    2020-09-23T07:40:49.287+00:00

    Hi,
    another approach with your code:

    <Window x:Class="WpfApp1.Window84"
            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:WpfApp84"
            mc:Ignorable="d"
            Title="MainWindow" Height="450" Width="800">
      <Window.Resources>
        <local:ViewModel x:Key="vm"/>
      </Window.Resources>
      <Grid DataContext="{StaticResource vm}">
        <DataGrid ItemsSource="{Binding MyModules}" AutoGenerateColumns="False">
          <DataGrid.Resources>
            <ContextMenu x:Key="RowMenu" ItemsSource="{Binding Configuration.Commands}" >
              <ContextMenu.ItemTemplate>
                <DataTemplate>
                  <Grid Margin="2" >
                    <MenuItem Header="{Binding Header}" 
                              Command="{Binding Source={StaticResource vm}}"
                              CommandParameter="{Binding}"/>
                  </Grid>
                </DataTemplate>
              </ContextMenu.ItemTemplate>
            </ContextMenu>
          </DataGrid.Resources>
          <DataGrid.RowStyle>
            <Style TargetType="DataGridRow" >
              <Setter Property="ContextMenu" Value="{StaticResource RowMenu}" />
            </Style>
          </DataGrid.RowStyle>
          <DataGrid.Columns>
            <DataGridTemplateColumn Header="Module Name" Width="*" IsReadOnly="True">
              <DataGridTemplateColumn.CellTemplate>
                <DataTemplate>
                  <TextBlock Text="{Binding Configuration.Name}" />
                </DataTemplate>
              </DataGridTemplateColumn.CellTemplate>
            </DataGridTemplateColumn>
            <DataGridTemplateColumn Header="Module Description" Width="3*" IsReadOnly="True">
              <DataGridTemplateColumn.CellTemplate>
                <DataTemplate>
                  <TextBlock Text="{Binding Configuration.Description}" />
                </DataTemplate>
              </DataGridTemplateColumn.CellTemplate>
            </DataGridTemplateColumn>
          </DataGrid.Columns>
        </DataGrid>
      </Grid>
    </Window>
    

    And classes:

    using System;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.Windows;
    using System.Windows.Input;
    
    namespace WpfApp84
    {
      public class ViewModel : ICommand
      {
        public ViewModel() => LoadData();
        public ObservableCollection<IModule> MyModules { get; set; }
    
        private void LoadData()
        {
          Random rnd = new Random();
          this.MyModules = new ObservableCollection<IModule>();
          for (int i = 1; i < 10; i++)
          {
            List<Command> cmds = new List<Command>();
            Module m = new Module($"Name {i}", $"Description {i}", cmds);
            for (int k = 1; k < rnd.Next(3, 8); k++)
              cmds.Add(new Command() { Header = $"Menu {k} (Name {i})", Parent = m });
            this.MyModules.Add(m);
          }
        }
    
        public void Execute(object parameter) =>
          ((Command)parameter).Parent.ExecuteCommand((Command)parameter);
    
        public event EventHandler CanExecuteChanged;
        public bool CanExecute(object parameter) => true;
      }
    
      public class Module : IModule
      {
        public Module(string name, string description, List<Command> commands) =>
          this.Configuration = new Configuration(name, description, commands);
    
        public Configuration Configuration { get; }
    
        public void ExecuteCommand(Command command) =>
          MessageBox.Show($"Selected menu item: {command.Header}");
      }
    
      public interface IModule
      {
        Configuration Configuration { get; }
        void ExecuteCommand(Command command);
      }
    
      public class Configuration
      {
        public Guid Id { get; private set; }
        public string Name { get; private set; }
        public string Description { get; private set; }
        public List<Command> Commands { get; set; } = new List<Command>();
    
        public Configuration(string name, string description, List<Command> commands)
        {
          Id = new Guid();
          Name = name;
          Description = description;
          Commands = commands;
        }
      }
    
      public class Command
      {
        public string Header { get; set; }
        public Module Parent { get; set; }
      }
    }
    
    0 comments No comments