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.