1

I am trying to put a ContextMenu into cells of one column of a DataGrid. Clicking on a menu item of the ContextMenu should execute a command.

enter image description here

I can show the ContextMenu but the command is not executed and a binding error shows

ICommand TimePointOnCommand property not found on object of type Customer.

I dont want to have the ICommand on the Customer object but on the ViewModel

Is there some way to execute the command TimePointOnCommand whenever the user selects a menu item in the contect menu?

The project is as Prism project generated by the Prism template "Prism Full App"

This is my view

<Grid>
    <StackPanel>
        <DataGrid ItemsSource="{Binding Customers}" AutoGenerateColumns="False">
            <DataGrid.Columns>
                <DataGridTextColumn Header="Name" Binding="{Binding Name}" Width="*" />
                <DataGridTextColumn Header="Name" Binding="{Binding Id}" Width="*" />
                <DataGridTemplateColumn>
                    <DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <TextBlock Text="{Binding Name}"

                                       HorizontalAlignment="Stretch">

                                <TextBlock.ContextMenu>
                                    <ContextMenu>
                                        <MenuItem
                                            Header="An Zeitpunkt festlegen"
                                            Command="{  Binding Path=TimePointOnCommand}" />
                                    </ContextMenu>
                                </TextBlock.ContextMenu>
                            </TextBlock>
                        </DataTemplate>
                    </DataGridTemplateColumn.CellTemplate>
                </DataGridTemplateColumn>
            </DataGrid.Columns>
        </DataGrid>
    </StackPanel>
</Grid>

This is the ViewModel

using System;
using System.Collections.ObjectModel;
using Prism.Commands;
using Prism.Regions;
using PrismWpf.Core.Mvvm;
using PrismWpf.Services.Interfaces;

namespace PrismWpf.Modules.ModuleName.ViewModels
{
    public class ViewAViewModel : RegionViewModelBase
    {

        public DelegateCommand TimePointOnCommand { get; private set; }

        private ObservableCollection<Customer> _customers;

   
        public ObservableCollection<Customer> Customers
        {
            get => _customers;
            set => SetProperty(ref _customers, value);
        }

  
  
        public ViewAViewModel(IRegionManager regionManager, IMessageService messageService) :
            base(regionManager)
        {
            TimePointOnCommand = new DelegateCommand(SetOn);

            Customers = new ObservableCollection<Customer>
            {
                new() { Name = "John Doe", Id = 1, IsValid = true, Birthdate = new DateTime(2022, 4, 15, 10, 4, 2) },
                new() { Name = "Jane Smith", Id = 2, IsValid = false, Birthdate = new DateTime(2022, 6, 15, 8, 2, 2) }
            };
        }

        private void SetOn()
        {
        }

        public override void OnNavigatedTo(NavigationContext navigationContext)
        {
            
        }
    }
}

EDIT

The issue only happens for a ContextMenu For a button i can use

<Button Command="{Binding RelativeSource={RelativeSource AncestorType={x:Type UserControl},Mode=FindAncestor},Path=DataContext.TimePointOnCommand}" >Go</Button> 

But I realy need a ContextMenu here

Mathias F
  • 15,906
  • 22
  • 89
  • 159
  • It's been a while since I've used wpf, but have you tried setting the datacontext to ViewAViewModel? – Shak Ham Jul 20 '23 at 13:59
  • Not an exact duplicate, but close enough [WPF ContextMenu woes: How do I set the DataContext of the ContextMenu?](https://stackoverflow.com/questions/15033522/wpf-contextmenu-woes-how-do-i-set-the-datacontext-of-the-contextmenu) You want to move the command on the customer, probably – Haukinger Jul 20 '23 at 14:01
  • @Haukinger I tried the accepted answer of that post but for me PlacementTarget.Tag is not existing – Mathias F Jul 20 '23 at 14:13
  • @ShakHam How would I do that? – Mathias F Jul 20 '23 at 14:14
  • You can do it in the wpf itself: Or in the UI c# class: public YourView() { InitializeComponent(); DataContext = new ViewAViewModel(); } – Shak Ham Jul 20 '23 at 14:17
  • So just wondering, what if you do Binding TimePointOnCommand instead of Binding Path=TimePointOnCommand – Shak Ham Jul 20 '23 at 14:36
  • you can use anything as a substitue for the `Tag`, I once defined a proxy in the resources that just holds the root data context: `{Binding Path=Data.SomeCommandForTheContextMenu, Source={StaticResource Proxy}}` – Haukinger Jul 21 '23 at 07:57

2 Answers2

0

This is a special case: the ContextMenu is not part of the same visual that the TextBlock belongs to. And the TextBlock or the DataGridTemplateColumn is not a child of the visual tree that the DataGrid belongs to. Inside the ContextMenu you are two disconnected visual trees away from your view model.

One option is to move the TimePointOnCommand command to the item model. The item model is the value of the ContextMenu.DataContext property.

An alternative solution is to move the ContextMenu.
Because your ContextMenu does not depend on the individual data items, you can safely move the ConetxtMenu to the DataGrid. From there you have easy access to the proper DataContext:

<DataGrid>
  <DataGrid.ContextMenu>
    <ContextMenu DataContext="{Binding RelativeSource={RelativeSource Self}, Path=(ContextMenuService.PlacementTarget).DataContext}">  
      <MenuItem Header="An Zeitpunkt festlegen"
                Command="{Binding TimePointOnCommand}" />
    </ContextMenu>
  </DataGrid.ContextMenu>

  <DataGrid.Columns>
    ...
  </DataGrid.Columns>
</DataGrid>

Update

To associate the ContextMenu with a particular column, you can define a DataGrid.CellStyle instead. Let the Style implement the appropriate triggers that identify the correct column to enable the ContextMenu.

The following example only shows a ContextMenu for the second column (DataGridColumn.DisplayIndex == 1).
A IValueConverter is used to get the DataGrid.DataContext via visual tree traversal:

<DataGrid AutoGenerateColumns="False">
  <DataGrid.CellStyle>
    <Style TargetType="DataGridCell">
      <Setter Property="ContextMenuService.IsEnabled"
              Value="False" />
      <Setter Property="ContextMenu">
        <Setter.Value>
          <ContextMenu DataContext="{Binding RelativeSource={RelativeSource Self}, Path=(ContextMenuService.PlacementTarget), Converter={StaticResource DataGridCellToDataGridDataContextConverter}}">
            <MenuItem Header="An Zeitpunkt festlegen"
                      Command="{Binding TimePointOnCommand}" />
          </ContextMenu>
        </Setter.Value>
      </Setter>

      <Style.Triggers>
        <DataTrigger Binding="{Binding RelativeSource={RelativeSource Self}, Path=Column.DisplayIndex}"
                     Value="1">
          <Setter Property="ContextMenuService.IsEnabled"
                  Value="True" />
        </DataTrigger>
      </Style.Triggers>
    </Style>
  </DataGrid.CellStyle>

  <DataGrid.Columns>
    ...
  </DataGrid.Columns>
</DataGrid>
public class DataGridCellToDataGridDataContextConverter : IValueConverter
{
  public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 
    => value is DataGridCell dataGridCell 
      && TryFindVisualParent(dataGridCell, out DataGrid dataGrid)
        ? dataGrid.DataContext
        : null;

  private bool TryFindVisualParent<TParent>(DependencyObject element, out TParent parent)
    where TParent : DependencyObject
  {
    while (element is not TParent and not null)
    {
      element = VisualTreeHelper.GetParent(element);
    }

    parent = element as TParent;
    return parent is not null;
  }

  public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) 
    => throw new NotSupportedException();
}
  • Is it possible to only show the ContextMenu on one of the columns? That was the reason why I had it as a child of the TemplateColumn – Mathias F Jul 20 '23 at 20:53
  • Yes it is possible. I have updated the example to show how to achieve that. –  Jul 21 '23 at 09:47
0

When using an ItemsControl like DataGrid, the data context within its xaml (I mean, every xaml code that is enclosed between <DataGrid> and </DataGrid> ) refers to the type of its ItemsSource. That is in your case the class Customer and thus the error message "ICommand TimePointOnCommand property not found on object of type Customer." makes absolutely sense ;)

In short words: your binding is wrong.

One solution would be to give your ViewA a concrete name (for example "viewA") and change the binding to something like this:

<TextBlock.ContextMenu>
   <ContextMenu>
      <MenuItem
         Header="An Zeitpunkt festlegen"
         Command="{ Binding Path=DataContext.TimePointOnCommand, ElementName="viewA"}" />
   </ContextMenu>
</TextBlock.ContextMenu>
Sandman
  • 302
  • 2
  • 14