WPF/MVVM: Child control recognize DataContext as Model from its parent control's ItemsSource instead of View Model

Trọng Tín Võ 20 Reputation points
2023-03-26T05:18:01.1733333+00:00

I just reproduced the problem by making a similar new project.

I'm having an ItemsControl that contains buttons representing projects.

Now I want to add a context menu with options: view, edit, delete when the player right clicks on the project. I did this as follows:

  • Create Models folder and Project model inside it:
namespace BindingFailureReproduce.Models
{
    public class Project
    {
        private int _id;
        private string _name = "";
        private string _description = "";
        public int ID
        {
            get { return _id; }
            set { _id = value; }
        }
        public string Name
        {
            get { return _name; }
            set { _name = value; }
        }
        public string Description
        {
            get { return _description; }
            set { _description = value; }
        }
        public Project()
        {

        }
        public Project(int id, string name, string description)
        {
            _id = id;
            _name = name;
            _description = description;
        }
    }
}

  • Create ViewModels folder and ViewModelBase inside it:
using System;
using System.ComponentModel;

namespace BindingFailureReproduce.ViewModels
{
    public class ViewModelBase : INotifyPropertyChanged, IDisposable
    {
        public event PropertyChangedEventHandler PropertyChanged;
        protected void OnPropertyChanged(string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
        public virtual void Dispose() { }
    }
}

  • Create RelayCommand:
using System;
using System.Windows.Input;

namespace BindingFailureReproduce
{
    public class RelayCommand : ICommand
    {
        private readonly Predicate<object> _canExecute;
        private readonly Action<object> _execute;

        public RelayCommand(Predicate<object> canExecute, Action<object> execute)
        {
            _canExecute = canExecute;
            _execute = execute;
        }

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

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

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

  • Create MainWindowViewModel inherited ViewModelBase in ViewModels folder:
using BindingFailureReproduce.Models;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Input;

namespace BindingFailureReproduce.ViewModels
{
    public class MainWindowViewModel : ViewModelBase
    {
        private List<Project> _projectList;
        public List<Project> ProjectList
        {
            get
            {
                return _projectList;
            }
            set
            {
                _projectList = value;
                OnPropertyChanged(nameof(_projectList));
            }
        }
        private void ViewProject()
        {
            MessageBox.Show("View project");
        }
        private void EditProject()
        {
            MessageBox.Show("Edit project");
        }
        private void DeleteProject()
        {
            MessageBox.Show("Delete project");
        }
        private ICommand? _cmdViewProject;
        public ICommand CmdViewProject
        {
            get
            {
                _cmdViewProject ??= new RelayCommand(
                    p => true,
                    p => ViewProject());
                return _cmdViewProject;
            }
        }
        private ICommand? _cmdEditProject;
        public ICommand CmdEditProject
        {
            get
            {
                _cmdEditProject ??= new RelayCommand(
                    p => true,
                    p => EditProject());
                return _cmdEditProject;
            }
        }
        private ICommand? _cmdDeleteProject;
        public ICommand CmdDeleteProject
        {
            get
            {
                _cmdDeleteProject ??= new RelayCommand(
                    p => true,
                    p => DeleteProject());
                return _cmdDeleteProject;
            }
        }
        public MainWindowViewModel()
        {
            _projectList = new List<Project>{new Project(1, "Web project", "This is web project"),
                                             new Project(2, "AI project", "This is AI project"),
                                             new Project(3, "Android project", "This is android project"),
                                             new Project(4, "Embedded project", "This is embedded project"),
                                             new Project(5, "Game project", "This is game project")};
        }
    }
}

  • My MainWindow.xaml:
<Window x:Class="BindingFailureReproduce.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:BindingFailureReproduce"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <ScrollViewer Width="700" Height="350" x:Name="MyScrollViewer" VerticalScrollBarVisibility="Visible">
            <ItemsControl ItemsSource="{Binding ProjectList}">
                <ItemsControl.ItemsPanel>
                    <ItemsPanelTemplate>
                        <WrapPanel Width="700"/>
                    </ItemsPanelTemplate>
                </ItemsControl.ItemsPanel>
                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <Button Margin="10 20 10 20" Width="200" Height="100">
                            <Button.ContextMenu>
                                <ContextMenu Name="cm" StaysOpen="true">
                                    <MenuItem Command="{Binding CmdViewProject}" Header="View"/>
                                    <MenuItem Command="{Binding CmdEditProject}" Header="Edit"/>
                                    <MenuItem Command="{Binding CmdDeleteProject}" Header="Delete"/>
                                </ContextMenu>
                            </Button.ContextMenu>
                            <StackPanel Orientation="Vertical">
                                <TextBlock Text="{Binding Name}" Margin="5,0"></TextBlock>
                                <TextBlock Text="{Binding Description}" Margin="5,0"></TextBlock>
                            </StackPanel>
                        </Button>
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ItemsControl>
        </ScrollViewer>
    </Grid>
</Window>

  • I set the DataContext of MainWindow to the MainWindowViewModel() in MainWindow.xaml.cs:
using BindingFailureReproduce.ViewModels;
using System.Windows;

namespace BindingFailureReproduce
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            this.DataContext = new MainWindowViewModel();
        }
    }
}

I used Button.ContextMenu to achieve what I want and it look good. But when I click on MenuItem to View, Edit or Delete project, nothing happened. My app not crash but the XAML Binding Failures Window show the error:

 Count: 1
 DataContext: Project
 Binding Path: CmdViewProject
 Target: MenuItem.Command
 Target Type: ICommand
 Description: CmdViewProject property not found on object of type Project
 File: mainwindow.xaml
 Line: 22
 Project: BindingFailureReproduced

I also have 2 more similar errors with the remaining 2 commands at line 23 and 24 in MainWindow.xaml

I was expecting the MenuItem's DataContext to be the MainWindowViewModel but in reality it's taking the Project model as the DataContext

Please show me how to properly bind command in MainWindowViewModel to MenuItem. Thank you very much.

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,775 questions
C#
C#
An object-oriented and type-safe programming language that has its roots in the C family of languages and includes support for component-oriented programming.
10,931 questions
0 comments No comments
{count} votes

Accepted answer
  1. Hui Liu-MSFT 48,531 Reputation points Microsoft Vendor
    2023-03-27T07:17:20.1033333+00:00

    Hi,@Trọng Tín Võ. Welcome Microsoft Q&A.

    Basically the elements in the ItemsControl (or ContextMenu) are not part of the visual or logical tree, and therefore cannot find the DataContext of your UserControl.

    You could try the following code, it works fine.

     <Window.Resources>
            <local:BindingProxy x:Key="DataContextProxy" Data="{Binding}" />
        </Window.Resources>
    
    ...
    
    
     <ContextMenu Name="cm" StaysOpen="true">
                                        <MenuItem Command="{Binding Data.CmdViewProject, Source={StaticResource DataContextProxy}}" Header="View"/>
                                        <MenuItem Command="{Binding Data.CmdEditProject, Source={StaticResource DataContextProxy}}" Header="Edit"/>
                                        <MenuItem Command="{Binding Data.CmdDeleteProject, Source={StaticResource DataContextProxy}}" Header="Delete"/>
     </ContextMenu>
    
     public class BindingProxy : Freezable
        {
            #region Overrides of Freezable
    
            protected override Freezable CreateInstanceCore()
            {
                return new BindingProxy();
            }
    
            #endregion
    
            public object Data
            {
                get { return (object)GetValue(DataProperty); }
                set { SetValue(DataProperty, value); }
            }
    
            // Using a DependencyProperty as the backing store for Data.  This enables animation, styling, binding, etc...
            public static readonly DependencyProperty DataProperty =
                DependencyProperty.Register("Data", typeof(object), typeof(BindingProxy), new UIPropertyMetadata(null));
        }
    

    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 additional answers

Sort by: Most helpful
  1. Sedat SALMAN 13,830 Reputation points
    2023-03-26T08:09:03.47+00:00

    you can use a workaround by using the Tag property of the Button to store the DataContext, and then bind the MenuItem Command to the Tag property of the Button

    
    <Button Margin="10 20 10 20" Width="200" Height="100" Tag="{Binding DataContext, RelativeSource={RelativeSource AncestorType=ItemsControl}}">
        <Button.ContextMenu>
            <ContextMenu Name="cm" StaysOpen="true">
                <MenuItem Command="{Binding Path=PlacementTarget.Tag.CmdViewProject, RelativeSource={RelativeSource AncestorType=ContextMenu}}" Header="View"/>
                <MenuItem Command="{Binding Path=PlacementTarget.Tag.CmdEditProject, RelativeSource={RelativeSource AncestorType=ContextMenu}}" Header="Edit"/>
                <MenuItem Command="{Binding Path=PlacementTarget.Tag.CmdDeleteProject, RelativeSource={RelativeSource AncestorType=ContextMenu}}" Header="Delete"/>
            </ContextMenu>
        </Button.ContextMenu>
        <StackPanel Orientation="Vertical">
            <TextBlock Text="{Binding Name}" Margin="5,0"></TextBlock>
            <TextBlock Text="{Binding Description}" Margin="5,0"></TextBlock>
        </StackPanel>
    </Button>
    
    
    1 person found this answer helpful.

  2. Sedat SALMAN 13,830 Reputation points
    2023-03-26T05:25:27.01+00:00

    try the following

    
    <ContextMenu Name="cm" StaysOpen="true">
        <MenuItem Command="{Binding Path=DataContext.CmdViewProject, RelativeSource={RelativeSource AncestorType=ItemsControl}}" Header="View"/>
        <MenuItem Command="{Binding Path=DataContext.CmdEditProject, RelativeSource={RelativeSource AncestorType=ItemsControl}}" Header="Edit"/>
        <MenuItem Command="{Binding Path=DataContext.CmdDeleteProject, RelativeSource={RelativeSource AncestorType=ItemsControl}}" Header="Delete"/>
    </ContextMenu>
    
    

    since

    The issue you are encountering is due to the fact that the MenuItem is inside the DataTemplate of the ItemsControl. As a result, the DataContext of the MenuItem is set to the Project object, not the MainWindowViewModel.

    To fix this issue, you need to set the Command binding of the MenuItem to the MainWindowViewModel instead of the Project object. You can achieve this by using the RelativeSource to traverse up the visual tree and find the ItemsControl, then bind to its DataContext.


Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.