WPF Binding Application Commands to ViewModel ICommand

左心房为你撑大大i 提交于 2019-12-24 18:06:12

问题


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);
}


来源:https://stackoverflow.com/questions/36393497/wpf-binding-application-commands-to-viewmodel-icommand

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!