53

I got a WPF application that shows a button bound to a command like that:

<Button Command="{Binding Path=TestrunStartCommand}" Content="GO!">

The command is defined like that:

public ICommand TestrunStartCommand
{
    get { return new RelayCommand(TestrunStartExecute, () => !IsTestrunInProgress); }
}

public bool IsTestrunInProgress
{
    get{
        return _isTestrunInProgress;
    }
    set{
        _isTestrunInProgress = value;
        RaisePropertyChanged(IsTestrunInProgressPropertyName);
    }
}   

The problem is, the button won't be enabled immediately after I set IsTestrunInProgress to false, but only after I click inside the application window.

Could you help me understand this behaviour and show me how to fix this?

Further reading: wpf command pattern - when does it query canexecute

Marcel Gosselin
  • 4,610
  • 2
  • 31
  • 54
nabulke
  • 11,025
  • 13
  • 65
  • 114
  • `only after I click inside the application window`; are you implying that the currently active window in the OS is **not** this programs window? Or in other words, this application is up and running, but you're in Notepad and you can just see the window in the background. – Mike Perrenoud Jan 23 '13 at 12:10
  • 2
    @MichaelPerrenoud: No, the application window is active and has focus. It appears as if the `CanExecuteChanged` is only evaluated if I click inside my window. – nabulke Jan 23 '13 at 12:13
  • RelayCommand from Galasoft library works effeciently – HichemSeeSharp Jan 23 '13 at 12:30
  • 1
    @HichemC I'm sure it works efficiently, the error certainly is on my (beginner) side. But where is my mistake? – nabulke Jan 23 '13 at 12:33

7 Answers7

65

The ICommand interface exposes an event ICommand.CanExecuteChanged which is used to inform the UI when to re-determine the IsEnabled state of command driven UI components.

Depending upon the implementation of the RelayCommand you are using, you may need to raise this event; Many implementations expose a method such as RelayCommand.RaiseCanExecuteChanged() which you can invoke to force the UI to refresh.

Some implementations of the RelayCommand make use of CommandManager.RequerySuggested, in which case you will need to call CommandManager.InvalidateRequerySuggested() to force the UI to refresh.

Long story short, you will need to call one of these methods from your property setter.

Update

As the state of the button is being determined when the active focus is changing, I believe the CommandManager is being used. So in the setter of your property, after assigning the backing field, invoke CommandManager.InvalidateRequerySuggested().

Update 2

The RelayCommand implementation is from the MVVM light toolkit. When consumed from WPF/.NET, the implementation wraps the methods and events exposed from the CommandManager. This will mean that these commands work automagically in the majority of situations (where the UI is altered, or the focused element is changed). But in a few cases, such as this one, you will need to manually force the command to re-query. The proper way to do this using this library would be to call the RaiseCanExecuteChanged() method on the RelayCommand.

Lukazoid
  • 19,016
  • 3
  • 62
  • 85
  • 3
    You are right: I got the `RaiseCanExecuteChanged` method in my `RelayCommand`. But what I don't understand is that I got many working buttons and I _never_ raise this event manually, and somehow it works as if by magic. What is different in this case? I think there is still something missing in that puzzle... – nabulke Jan 23 '13 at 12:21
  • And all these buttons are using the same implementation of `RelayCommand`? Could you perhaps tell me which implementation you are using? i.e. which library the class is from? – Lukazoid Jan 23 '13 at 12:29
  • 1
    Yes all buttons use the same `RelayCommand`. I'm using the [mvvm light framework](https://mvvmlight.codeplex.com/). – nabulke Jan 23 '13 at 12:31
  • It seems like when used from WPF, the `RelayCommand` merely uses the `CommandManager`, `RaiseCanExecuteChanged()` is merely a wrapper around `CommandManager.InvalidateRequerySuggested()`. The exact requirements for the `CommandManager` to automatically raises `RequerySuggested` is not 100%. It usually occurs when focus changes or the UI changes. Perhaps the other buttons do something to this effect. Simply doing what I stated above should solve this problem for you. Why your other buttons work, I wouldn't be 100% sure without looking at the complete code. – Lukazoid Jan 23 '13 at 12:40
  • 21
    If you're using WPF 4.5 or above, you need to use the RelayCommand from the namespace GalaSoft.MvvmLight.CommandWpf instead of GalaSoft.MvvmLight.Command. That will re-enable the RelayCommand to use the CommandManager. – Samir Mar 10 '15 at 20:23
  • I have the same behaviour as the OP, even using CommandWpf.RelayCommand and calling myCommand.RaiseCanExecuteChanged() right after changing the CanExecute condition. (The IsEnabled property updates only after clicking on the UI). WPF 4.0, MvvmLight 5.2.0.37222 – Gobe Feb 07 '16 at 19:05
  • I am not using any MVVM toolkit and get the same behavior. CommandManager.InvalidateRequerySuggested(); is working for me. – Carol Jun 02 '16 at 22:11
  • 6
    Another important thing I noticed, if you set the (change-causing) property from a non-UI thread, you need to call the mentioned methods via the Dispatcher. Otherwise, WPF just swallows the notification. – Andreas Duering Feb 16 '18 at 12:19
  • @AndreasDuering thanks. Buttons get updated CanExecute just after subscription OnPropertyChanged and call RaiseCanExecuteChanged via Application.Current.Dispatcher.Invoke in event handler – cratu Feb 02 '19 at 08:55
  • @AndreasDuering Your suggestion on Dispatcher is what helped me. – Ray Sep 03 '19 at 00:29
61

This is so important and easy to miss, I am repeating what @Samir said in a comment. Mr Laurent Bugnion wrote in his blog:

In WPF 4 and WPF 4.5, however, there is a catch: The CommandManager will stop working after you upgrade MVVM Light to V5. What you will observe is that your UI elements (buttons, etc) will stop getting disabled/enabled when the RelayCommand’s CanExecute delegate returns false.

If you are in a hurry, here is the fix: In any class that uses the RelayCommand, replace the line saying:

using GalaSoft.MvvmLight.Command;

with:

using GalaSoft.MvvmLight.CommandWpf;
Riegardt Steyn
  • 5,431
  • 2
  • 34
  • 49
5

You can try with CommandManager.InvalidateRequerySuggested.

Anyway this did not help me sometimes in the past. For me the best solution turned out to be to bind the boolean property to the Button.IsEnabled dependency property.

In your case something like

IsEnabled={Binding IsTestrunInProgress}
Klaus78
  • 11,648
  • 5
  • 32
  • 28
2

The issue is, the ICommand Property TestrunStartCommand is always returning a new command object whenever it is accessed.

A simple fix is to create the ICommand object once and use it again and again.

private ICommand _testRunCommand = null;
public ICommand TestrunStartCommand
{
    get 
    { 
        return _testRunCommand ?? (_testRunCommand = new RelayCommand(TestrunStartExecute, () => !IsTestrunInProgress)); 
    }
}

This was quite a simple fix and it worked for me.

Amit Kumar
  • 69
  • 3
1

Addition to Riegardt Steyn's answer above: https://stackoverflow.com/a/33503341/1964969

If you don't want to change Command to CommandWpf usage (as that two RelayCommand versions are not compatible inbetween), another workaround could be to not instantiate a command at the declaration place. Use constructor code instead:

public class SomeVMClass
{
    // CanExecute won't work:
    // Declaration and instantiation same place
    public RelayCommand MyCommand1 => new RelayCommand(MyBusinessLogic, MyCanExecuteValidator);

    // CanExecute will work
    // Declaration only
    public RelayCommand MyCommand2 { get; private set; }

    public SomeVMClass()
    {
        // Let's instantiate our declared command
        MyCommand2 = new RelayCommand(MyBusinessLogic, MyCanExecuteValidator);
       ...
Yury Schkatula
  • 5,291
  • 2
  • 18
  • 42
0

Blockquote

In your Command class change CanExcutedChanged to this

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

This is example of my command class

public class SaveConfigCommand : ICommand
{
    public MyViewModel VM { get; set; }

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

    public SaveConfigCommand(MyViewModel vm)
    {
        VM = vm;
    }

    public bool CanExecute(object? parameter)
    {
        MyObjectModel model = parameter as MyObjectModel;

        if (model == null)
            return false;

        // Validate others properties here 

        return true;
    }

    public void Execute(object? parameter)
    {
        VM.MyMethodInViewModel();
    }
}
ice thailand
  • 163
  • 1
  • 6
0

If you are using a BackgroundWorker (not necessarily what OP is asking), the CommandManager.InvalidateRequerySuggested() will only work inside the ProgressChanged callback and not on the DoWork callback.

//Not going to work.
private void Worker_DoWork(object sender, DoWorkEventArgs e)
{
    Dispatcher.CurrentDispatcher.Invoke(CommandManager.InvalidateRequerySuggested, DispatcherPriority.Render);
}

//Works!
private void Worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
    Dispatcher.CurrentDispatcher.Invoke(CommandManager.InvalidateRequerySuggested, DispatcherPriority.Render);
}
Nicke Manarin
  • 3,026
  • 4
  • 37
  • 79