ICommand CanExecute not triggering after PropertyChanged?
Asked Answered
V

7

58

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

Verditer answered 23/1, 2013 at 12:4 Comment(4)
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.Glucinum
@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.Verditer
RelayCommand from Galasoft library works effecientlyHardnett
@HichemC I'm sure it works efficiently, the error certainly is on my (beginner) side. But where is my mistake?Verditer
E
69

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.

Eustazio answered 23/1, 2013 at 12:12 Comment(10)
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...Verditer
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?Eustazio
Yes all buttons use the same RelayCommand. I'm using the mvvm light framework.Verditer
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.Eustazio
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.Bracelet
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.37222Nymphomania
I am not using any MVVM toolkit and get the same behavior. CommandManager.InvalidateRequerySuggested(); is working for me.Lepp
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.Absent
@AndreasDuering thanks. Buttons get updated CanExecute just after subscription OnPropertyChanged and call RaiseCanExecuteChanged via Application.Current.Dispatcher.Invoke in event handlerSheffield
@AndreasDuering Your suggestion on Dispatcher is what helped me.Williswillison
C
62

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;
Casto answered 3/11, 2015 at 15:47 Comment(0)
A
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}
Austen answered 23/1, 2013 at 12:13 Comment(2)
Yes, using the binding like you said and it works like a charm.Verditer
This defeats the purpose of using a command in the first place, and only works if your enabled state depends on a single property.Kemppe
L
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.

Lenlena answered 24/12, 2017 at 23:4 Comment(0)
D
1

Addition to Riegardt Steyn's answer above: https://mcmap.net/q/329317/-icommand-canexecute-not-triggering-after-propertychanged

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);
       ...
Diarthrosis answered 15/9, 2021 at 11:12 Comment(0)
T
1

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();
    }
}
Tort answered 1/2, 2022 at 7:44 Comment(0)
C
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);
}
Cursive answered 1/5, 2023 at 12:48 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.