11

I have a simple-as-can be window with a button tied to a ViewModel with a command.

I expect the button to be disabled if MyCommand.CanExecute() is false. But it seems that WPF will only set the IsEnabled property when the window is first drawn. Any subsequent action does not effect the button's visible state. I am using a DelegateCommand from Prism.

My View:

<Window x:Class="WpfApplication1.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="MainWindow" Height="350" Width="525">
<Grid>
    <Button Content="Click Here" Command="{Binding MyCommand}" Width="100" Height="50"/>
</Grid>

and my ViewModel:

public class MyVM : NotificationObject
{
    public MyVM()
    {
        _myCommand = new DelegateCommand(DoStuff, CanDoStuff);
    }

    private void DoStuff()
    {
        Console.WriteLine("Command Executed");
        _myCommand.RaiseCanExecuteChanged();
    }

    private bool CanDoStuff()
    {
        var result =  DateTime.Now.Second % 2 == 0;
        Console.WriteLine("CanExecute is {0}", result);
        return result;
    }

    private DelegateCommand _myCommand;

    public ICommand MyCommand
    {
        get
        {
            return _myCommand;
        }
    }
}

50% of the time, when my application loads, the button is properly disabled. However, if it's enabled when the window loads, and I click the button to execute the command, I expect 50% of the time for the button to become disabled, but it never does. The command does not execute, but I can still click the button. How do I get WPF to understand that the button should be disabled when CanExecute() is false?

Greg Ferreri
  • 2,652
  • 5
  • 28
  • 38
  • First thing you need to do is turn up debug messages for databinding: http://i.stack.imgur.com/MF8i5.png Next, re-run and check the output window and see what errors are there. If none relate to your command binding, then your implementation of `RaiseCanExecuteChanged()` is incorrect/buggy. –  Feb 22 '13 at 21:43
  • Your CanDOStuff method is really weird! It could make your button disabled, but in next second your command could execute, but button is disabled... REally wierd. but you should call [CommandManager.InvalidateRequerySuggested()](http://msdn.microsoft.com/en-us/library/system.windows.input.commandmanager.invalidaterequerysuggested.aspx) if your CanExecute may be changed and UI is not updated because CanExecuteChanged wasn't raised. – Kapitán Mlíko Feb 22 '13 at 22:10
  • @Viktor I know it's really weird, it's supposed to be a silly example that returns true/false randomly. CommandManager.InvalidateRequerySuggested has no effect. – Greg Ferreri Feb 22 '13 at 22:30
  • @Will There are no errors in the output, and `RaiseCanExecuteChanged` is part of the Microsoft Prism library, nothing home-grown. – Greg Ferreri Feb 22 '13 at 22:34

2 Answers2

8

I see you're using Prism and its NotificationObject and DelegateCommand, so we should expect there not to be a bug in RaiseCanExecuteChanged().

However, the reason for the behaviour is that Prism's RaiseCanExecuteChanged operates synchronously, so CanDoStuff() is called while we're still inside the implementation of ICommand.Execute() and the result then appears to be ignored.

If you create another button with its own command and call _myCommand.RaiseCanExecuteChanged() from that command/button, the first button will be enabled/disabled as you expect.

Or, if you try the same thing with MVVM Light and RelayCommand your code will work because MVVM Light's RaiseCanExecuteChanged calls CommandManager.InvalidateRequerySuggested() which invokes the callback to CanDoStuff asynchronously using Dispatcher.CurrentDispatcher.BeginInvoke, avoiding the behaviour you're seeing with Prism's implementation.

Phil
  • 42,255
  • 9
  • 100
  • 100
  • Thanks for the explanation. I tried both of your suggestions, and they're spot on. Seems like an unfortunate limitation of Prism. – Greg Ferreri Feb 25 '13 at 13:53
0

You can try this (Microsoft.Practices.Prism.dll is necessary)

public class ViewModel
{
    public DelegateCommand ExportCommand { get; }

    public ViewModel()
    {
        ExportCommand = new DelegateCommand(Export, CanDoExptor);
    }

    private void Export()
    {
        //logic
    }

    private bool _isCanDoExportChecked;

    public bool IsCanDoExportChecked
    {
        get { return _isCanDoExportChecked; }
        set
        {
            if (_isCanDoExportChecked == value) return;

            _isCanDoExportChecked = value;
            ExportCommand.RaiseCanExecuteChanged();
        }
    }

    private bool CanDoExptor()
    {
        return IsCanDoExportChecked;
    }
}
isxaker
  • 8,446
  • 12
  • 60
  • 87