views:

912

answers:

3

Hi all, I'm having some difficulty with Context Menu commands on my View Model.

I'm implementing the ICommand interface for each command within the View Model, then creating a ContextMenu within the resources of the View (MainWindow), and using a CommandReference from the MVVMToolkit to access the current DataContext (ViewModel) Commands.

When I debug the application, it appears that the CanExecute method on the command is not being called except at the creation of the window, therefore my Context MenuItems are not being enabled or disabled as I would have expected.

I've cooked up a simple sample (attached here) which is indicative of my actual application and summarised below. Any help would be greatly appreciated!

This is the ViewModel

namespace WpfCommandTest
{
    public class MainWindowViewModel
    {
        private List<string> data = new List<string>{ "One", "Two", "Three" };

        // This is to simplify this example - normally we would link to
        // Domain Model properties
        public List<string> TestData
        {
            get { return data; }
            set { data = value; }
        }

        // Bound Property for listview
        public string SelectedItem { get; set; }

        // Command to execute
        public ICommand DisplayValue { get; private set; }

        public MainWindowViewModel()
        {
            DisplayValue = new DisplayValueCommand(this);
        }

    }
}

The DisplayValueCommand is such:

public class DisplayValueCommand : ICommand
{
    private MainWindowViewModel viewModel;

    public DisplayValueCommand(MainWindowViewModel viewModel)
    {
        this.viewModel = viewModel;
    }

    #region ICommand Members

    public bool CanExecute(object parameter)
    {
        if (viewModel.SelectedItem != null)
        {
            return viewModel.SelectedItem.Length == 3;
        }
        else return false;
    }

    public event EventHandler CanExecuteChanged;

    public void Execute(object parameter)
    {
        MessageBox.Show(viewModel.SelectedItem);
    }

    #endregion
}

And finally, the view is defined in Xaml:

<Window x:Class="WpfCommandTest.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:WpfCommandTest"
    xmlns:mvvmtk="clr-namespace:MVVMToolkit"
    Title="Window1" Height="300" Width="300">

    <Window.Resources>

        <mvvmtk:CommandReference x:Key="showMessageCommandReference" Command="{Binding DisplayValue}" />

        <ContextMenu x:Key="listContextMenu">
            <MenuItem Header="Show MessageBox" Command="{StaticResource showMessageCommandReference}"/>
        </ContextMenu>

    </Window.Resources>

    <Window.DataContext>
        <local:MainWindowViewModel />
    </Window.DataContext>

    <Grid>
        <ListBox ItemsSource="{Binding TestData}" ContextMenu="{StaticResource listContextMenu}" 
                 SelectedItem="{Binding SelectedItem}" />
    </Grid>
</Window>
+2  A: 

You have to keep track of when the status of CanExecute has changed and fire the ICommand.CanExecuteChanged event.

Also, you might find that it doesn't always work, and in these cases a call to CommandManager.InvalidateRequerySuggested() is required to kick the command manager in the ass.

If you find that this takes too long, check out the answer to this question.

Will
+3  A: 

To complete Will's answer, here's a "standard" implementation of the CanExecuteChanged event :

public event EventHandler CanExecuteChanged
{
    add { CommandManager.RequerySuggested += value }
    remove { CommandManager.RequerySuggested -= value }
}

(from Josh Smith's RelayCommand class)

By the way, you should probably consider using RelayCommand or DelegateCommand : you'll quickly get tired of creating new command classes for each and every command of you ViewModels...

Thomas Levesque
+1  A: 

Thank you for the speedy replies. This approach does work if you are binding the commands to a standard Button in the Window (which has access to the View Model via its DataContext), for example; CanExecute is shown to be called quite frequently when using the CommandManager as you suggest on ICommand implementing classes or by using RelayCommand and DelegateCommand.

However, binding the same commands via a CommandReference in the ContextMenu do not act in the same way.

In order for the same behaviour, I must also include the EventHandler from Josh Smith's RelayCommand, within CommandReference, but in doing so I must comment out some code from within the OnCommandChanged Method. I'm not entirely sure why it is there, perhaps it is preventing event memory leaks (at a guess!)?

  public class CommandReference : Freezable, ICommand
    {
        public CommandReference()
        {
            // Blank
        }

        public static readonly DependencyProperty CommandProperty = DependencyProperty.Register("Command", typeof(ICommand), typeof(CommandReference), new PropertyMetadata(new PropertyChangedCallback(OnCommandChanged)));

        public ICommand Command
        {
            get { return (ICommand)GetValue(CommandProperty); }
            set { SetValue(CommandProperty, value); }
        }

        #region ICommand Members

        public bool CanExecute(object parameter)
        {
            if (Command != null)
                return Command.CanExecute(parameter);
            return false;
        }

        public void Execute(object parameter)
        {
            Command.Execute(parameter);
        }

        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }

        private static void OnCommandChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            CommandReference commandReference = d as CommandReference;
            ICommand oldCommand = e.OldValue as ICommand;
            ICommand newCommand = e.NewValue as ICommand;

            //if (oldCommand != null)
            //{
            //    oldCommand.CanExecuteChanged -= commandReference.CanExecuteChanged;
            //}
            //if (newCommand != null)
            //{
            //    newCommand.CanExecuteChanged += commandReference.CanExecuteChanged;
            //}
        }

        #endregion

        #region Freezable

        protected override Freezable CreateInstanceCore()
        {
            throw new NotImplementedException();
        }

        #endregion
    }
LiamV