Learning WPF with a small editor project and designing it with MVVM in mind.
The following code is throwing "Provide value on 'System.Windows.Data.Binding' threw an exception." at run time when the XAML is first parsed. No Build errors.
How best to bind my ICommands to Application Commands Close, Save, Save As, Open, New etc.
Currently I have just the Close and New setup.
XAML Code:
<Window x:Class="Editor.Views.EditorView"
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:Editor.Views"
xmlns:vm="clr-namespace:Editor.ViewModels"
xmlns:userControls="clr-namespace:Editor.UserControls"
mc:Ignorable="d"
Title="EditorView" Height="600" Width="800" WindowStartupLocation="CenterScreen">
<Window.Resources>
<DataTemplate DataType="{x:Type vm:DocumentViewModel}">
<ContentControl Content="{Binding DocTextBox}" />
</DataTemplate>
</Window.Resources>
<Window.CommandBindings>
<CommandBinding Command="ApplicationCommands.Close"
Executed="{Binding ExitCommand}" />
<CommandBinding Command="ApplicationCommands.New"
Executed="{Binding NewDocumentCommand}" />
<!--<CommandBinding Command="ApplicationCommands.Open"
Executed="OpenDocument" />
<CommandBinding Command="ApplicationCommands.Save"
CanExecute="SaveDocument_CanExecute"
Executed="SaveDocument" />
<CommandBinding Command="ApplicationCommands.SaveAs"
Executed="SaveDocumentAs" />-->
</Window.CommandBindings>
<Window.InputBindings>
<KeyBinding Key="N" Modifiers="Control" Command="{Binding NewDocumentCommand}" />
<KeyBinding Key="F4" Modifiers="Control" Command="{Binding CloseDocumentCommand}" />
</Window.InputBindings>
<DockPanel>
<userControls:Menu x:Name="menu"
DockPanel.Dock="Top" />
<TabControl ItemsSource="{Binding Documents}" SelectedIndex="{Binding SelectedIndex}">
<TabControl.ItemTemplate>
<DataTemplate>
<WrapPanel>
<TextBlock Text="{Binding FileName}" />
<Button Command="{Binding CloseCommand}" Content="X" Margin="4,0,0,0" FontFamily="Courier New" Width="17" Height="17" VerticalContentAlignment="Center" />
</WrapPanel>
</DataTemplate>
</TabControl.ItemTemplate>
</TabControl>
</DockPanel>
</Window>
The ViewModel Code:
public class EditorViewModel : ViewModelBase
{
private static int _count = 0;
public EditorViewModel()
{
Documents = new ObservableCollection<DocumentViewModel>();
Documents.CollectionChanged += Documents_CollectionChanged;
}
#region Event Handlers
void Documents_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null && e.NewItems.Count != 0)
foreach (DocumentViewModel document in e.NewItems)
document.RequestClose += this.OnDocumentRequestClose;
if (e.OldItems != null && e.OldItems.Count != 0)
foreach (DocumentViewModel document in e.OldItems)
document.RequestClose -= this.OnDocumentRequestClose;
}
private void OnDocumentRequestClose(object sender, EventArgs e)
{
CloseDocument();
}
#endregion
#region Commands
private RelayCommand _exitCommand;
public ICommand ExitCommand
{
get { return _exitCommand ?? (_exitCommand = new RelayCommand(() => Application.Current.Shutdown())); }
}
private RelayCommand _newDocumentCommand;
public ICommand NewDocumentCommand
{
get { return _newDocumentCommand ?? (_newDocumentCommand = new RelayCommand(NewDocument)); }
}
private void NewDocument()
{
_count++;
var document = new DocumentViewModel { FileName = "New " + _count, DocTextBox = new RichTextBox() };
Documents.Add(document);
SelectedIndex = Documents.IndexOf(document);
}
private RelayCommand _closeDocumentCommand;
public ICommand CloseDocumentCommand
{
get { return _closeDocumentCommand ?? (_closeDocumentCommand = new RelayCommand(CloseDocument, param => Documents.Count > 0)); }
}
private void CloseDocument()
{
Documents.RemoveAt(SelectedIndex);
SelectedIndex = 0;
}
#endregion
#region Public Members
public ObservableCollection<DocumentViewModel> Documents { get; set; }
private int _selectedIndex = 0;
public int SelectedIndex
{
get { return _selectedIndex; }
set
{
_selectedIndex = value;
OnPropertyChanged();
}
}
#endregion
}
When you are using
CommandBinding
, arguably you are configuring commands that the view should be handling. As such, it's not clear to me that it makes sense to implement the command in the view model. Conversely, if the view model should own the command, then use its command, not a pre-defined one.It doesn't make sense to ask to bind your
ICommand
object to an application command. TheApplicationCommands
objects are themselvesICommand
implementations! (RoutedUICommand
, to be specific.)If your view model already implements
ICommand
for the standard commands, then just bind to those:If you really want to use the
ApplicationCommands
commands, then you'll need to subscribe an event handler method to theExecuted
andCanExecute
events and then delegate those to the view model. For example:Then in code-behind, something like this:
Note that you'd have to make sure in this case that you set the
CommandParameter
at the source of the command itself. I.e. includeCommandParameter={Binding ExitCommand}
in theInputBinding
andButton
where you invoke the command. This could get tedious.Alternatively, you could assume that the
DataContext
of theSource
object is your view model and get the command directly from that: