0
votes

What I'm doing:

On my DataGrid I have a context menu to add Template items. The MenuItems are created dynamically using the ItemsSource property of the parent MenuItem. The ItemsSource is an ObservableCollection of my template objects. I want to get the Header of the dynamic MenuItems from the collection object properties, but execute a command from my main ViewModel. The main ViewModel is bound to the DataContext of the root Grid.

My issue:

The MenuItems are created properly and I can also bind the header to a property of an object in the collection. But the Command binding does not work. I can see an error in the output window: "System.Windows.Data Error: 4 : Cannot find source for binding with reference..."

Here is my code reduced to the issue (using MVVM light):

MainWindow.xaml:

<Window x:Class="TapTimesGui.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="600" Width="1000">
    <!-- root grid -->
    <Grid x:Name="RootGrid" DataContext="{Binding Source={StaticResource Locator}, Path=Main}">
        <!-- Jobs overview -->
        <DataGrid Grid.Column="0">
            <DataGrid.ContextMenu>
                <ContextMenu>
                    <!-- following works fine -->
                    <MenuItem Header="Basic command test" Command="{Binding AddTemplateJobCommand}" CommandParameter="Basic command test"/>
                    <MenuItem Header="Add template..." ItemsSource="{Binding JobTemplates}">
                        <MenuItem.ItemTemplate>
                            <DataTemplate>
                                <!-- following command does not work System.Windows.Data Error -->
                                <MenuItem Header="{Binding Name}" Command="{Binding ElementName=RootGrid, Path=DataContext.AddTemplateJobCommand}" CommandParameter="{Binding Name}"/>
                            </DataTemplate>
                        </MenuItem.ItemTemplate>
                        <!--<MenuItem.ItemContainerStyle> also does not work
                                <Style TargetType="MenuItem">
                                    <Setter Property="Header" Value="{Binding Name}"></Setter>
                                    <Setter Property="Command" Value="{Binding ElementName=RootGrid, Path=DataContext.SaveDayToNewFileCommand}"></Setter>
                                </Style>
                            </MenuItem.ItemContainerStyle>-->
                    </MenuItem>
                </ContextMenu>
            </DataGrid.ContextMenu>
        </DataGrid>
    </Grid>
</Window>

MainWindow.xaml.cs:

using System.Windows;

namespace TapTimesGui
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }
    }
}

MainViewModel.cs:

using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.CommandWpf;
using GalaSoft.MvvmLight.Messaging;
using System.Collections.ObjectModel;
using System.Windows.Input;
using TapTimesGui.Services;

namespace TapTimesGui.ViewModel
{
    /// <summary>
    /// ...
    /// </summary>
    public class MainViewModel : ViewModelBase
    {
        #region Properties and backing fields
        /// <summary>
        /// Templates for jobs
        /// </summary>
        public ObservableCollection<JobTemplate> JobTemplates
        {
            get
            {
                return _jobTemplates;
            }
        }
        private readonly ObservableCollection<JobTemplate> _jobTemplates = new ObservableCollection<JobTemplate>();
        #endregion Properties and backing fields

        #region ICommand properties
        public ICommand AddTemplateJobCommand { get; private set; }
        #endregion ICommand properties

        #region Constructors
        /// <summary>
        /// Initializes a new instance of the MainViewModel class.
        /// </summary>
        public MainViewModel()
        {
            // assign commands
            AddTemplateJobCommand = new RelayCommand<string>(AddTemplateJob);
            // populate data on start
            if (IsInDesignMode)
            {
                // Code runs in Blend --> create design time data.
            }
            else
            {
                //TODO test for templates
                JobTemplate tmpJobTemplate = new JobTemplate();
                tmpJobTemplate.Name = "Template 1";
                tmpJobTemplate.Template = "TestCustomer1 AG";
                JobTemplates.Add(tmpJobTemplate);
                tmpJobTemplate = new JobTemplate();
                tmpJobTemplate.Name = "Template 2";
                tmpJobTemplate.Template = "TestCustomer2 AG";
                JobTemplates.Add(tmpJobTemplate);
            }
        }
        #endregion Constructors

        #region Command methods
        private void AddTemplateJob(string name)
        {
            //TODO implement
            Messenger.Default.Send<NotificationMessage>(new NotificationMessage(name));
        }
        #endregion Command methods
    }
}

ViewModelLocator.cs:

/*
  In App.xaml:
  <Application.Resources>
      <vm:ViewModelLocator xmlns:vm="clr-namespace:TapTimesGui"
                           x:Key="Locator" />
  </Application.Resources>

  In the View:
  DataContext="{Binding Source={StaticResource Locator}, Path=ViewModelName}"

  You can also use Blend to do all this with the tool's support.
  See http://www.galasoft.ch/mvvm
*/

using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Ioc;
//using Microsoft.Practices.ServiceLocation; TODO http://www.mvvmlight.net/std10 chapter known issues
using CommonServiceLocator;
using GalaSoft.MvvmLight.Messaging;
using System;
using System.Windows;

namespace TapTimesGui.ViewModel
{
    /// <summary>
    /// This class contains static references to all the view models in the
    /// application and provides an entry point for the bindings.
    /// </summary>
    public class ViewModelLocator
    {
        /// <summary>
        /// Initializes a new instance of the ViewModelLocator class.
        /// </summary>
        public ViewModelLocator()
        {
            ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);

            ////if (ViewModelBase.IsInDesignModeStatic)
            ////{
            ////    // Create design time view services and models
            ////    SimpleIoc.Default.Register<IDataService, DesignDataService>();
            ////}
            ////else
            ////{
            ////    // Create run time view services and models
            ////    SimpleIoc.Default.Register<IDataService, DataService>();
            ////}

            SimpleIoc.Default.Register<MainViewModel>();
            Messenger.Default.Register<NotificationMessage>(this, NotificationMessageHandler);
            Messenger.Default.Register<Exception>(this, ExceptionMessageHandler);
        }

        public MainViewModel Main
        {
            get
            {
                return ServiceLocator.Current.GetInstance<MainViewModel>();
            }
        }

        public static void Cleanup()
        {
            // TODO Clear the ViewModels
        }

        private void NotificationMessageHandler(NotificationMessage message)
        {
            //MessageBox.Show(message.Notification);
            System.Diagnostics.Debug.WriteLine(message.Notification);
            MessageBox.Show(message.Notification);
        }

        private void ExceptionMessageHandler(Exception ex)
        {
            MessageBox.Show(ex.ToString(), "Exception", MessageBoxButton.OK, MessageBoxImage.Error);
        }
    }
}

JobTemplate.cs:

using System.ComponentModel;

namespace TapTimesGui.Services
{
    /// <summary>
    /// Named TapTimesGui.Model.Job template
    /// </summary>
    /// <remarks>Can be used e.g. to save Templates of specific objects</remarks>
    public class JobTemplate : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        public string Name
        {
            get
            {
                return _name;
            }
            set
            {
                _name = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));
            }
        }
        private string _name = "";

        public string Template //Type was not string originally, it was if my Model object
        {
            get
            {
                //TODO proper cloning
                return _template;
            }
            set
            {
                //TODO proper cloning
                _template = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Template)));
            }
        }
        private string _template = "Template";
    }
}

Versions:

  • Visual Studio Professional 2015
  • .NET Framework 4.5.2
  • MvvmLight 5.4.1.1
  • CommonServiceLocator 2.0.5 (required for MvvmLight)

I already tried to find a solution for some time. Thanks in advance for your help.

1

1 Answers

0
votes

You can access the ViewModelLocator inside the ItemTemplate as well.

<DataGrid Grid.Column="0">
     <DataGrid.ContextMenu>
          <ContextMenu>
              <MenuItem Header="Add template..." ItemsSource="{Binding JobTemplates}">
                  <MenuItem.ItemTemplate>
                      <DataTemplate>
                          <MenuItem Header="{Binding Name}" Command="{Binding Source={StaticResource Locator}, Path=Main.AddTemplateJobCommand}" CommandParameter="{Binding Name}"/>
                       </DataTemplate>
                   </MenuItem.ItemTemplate>
              </MenuItem>
          </ContextMenu>
     </DataGrid.ContextMenu>
</DataGrid>

A more general approach without MVVM light could be using a binding proxy.