WPF Binding Application Commands to ViewModel ICom

2019-07-23 06:54发布

问题:

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
}

回答1:

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. The ApplicationCommands objects are themselves ICommand implementations! (RoutedUICommand, to be specific.)

If your view model already implements ICommand for the standard commands, then just bind to those:

<CommandBinding Command="{Binding ExitCommand}"/>

If you really want to use the ApplicationCommands commands, then you'll need to subscribe an event handler method to the Executed and CanExecute events and then delegate those to the view model. For example:

<CommandBinding Command="ApplicationCommands.Close"
                Executed="Close_Executed" />

Then in code-behind, something like this:

void Close_Executed(object sender, ExecutedRoutedEventArgs e)
{
    ICommand command = (ICommand)e.Parameter;

    command.Execute(null);
}

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. include CommandParameter={Binding ExitCommand} in the InputBinding and Button where you invoke the command. This could get tedious.

Alternatively, you could assume that the DataContext of the Source object is your view model and get the command directly from that:

void Close_Executed(object sender, ExecutedRoutedEventArgs e)
{
    EditorViewModel viewModel = (EditorViewModel)((FrameworkElement)e.Source).DataContext;
    ICommand command = viewModel.ExitCommand;

    command.Execute(e.Parameter);
}