3

I'm facing the same issue reported in these questions:

Button Command CanExecute not called when property changed

How can I force a textbox change to enable my command in WPF?

(that briefly is: my command-linked button doesn't get enabled when it should) but with a slight difference: I already tried to invoke CommandManager.InvalidateRequerySuggested(), with no result.

The weirdest thing is that the button gets enabled only as soon as I "trigger" any event on the window, like clicking with the mouse in any point of the window.

Here is the code to reproduce the issue:

<Window x:Class="WpfApplication1.MainWindow"
    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:WpfApplication1"
    mc:Ignorable="d"
    Title="MainWindow" Height="350" Width="525">
<Window.CommandBindings>
    <CommandBinding Command="{x:Static local:Commands.RunTask}" x:Name="cmdRunTask" CanExecute="CanExecuteRunTask" Executed="ExecuteRunTask">    </CommandBinding>
    <CommandBinding Command="{x:Static local:Commands.PushButton}" x:Name="cmdPushButton" CanExecute="CanPushButton" Executed="PushButton"></CommandBinding>
</Window.CommandBindings>
<StackPanel>
    <Button Content="Run task"
                x:Name="runButton"
                Command="{x:Static local:Commands.RunTask}"
                >
    </Button>
    <ProgressBar x:Name="progress"
                     Value="{Binding Path=Value, Mode=OneWay}"
                 Height="30"
                     >
    </ProgressBar>
    <Button Content="Press me if you dare"
                x:Name="pushButton"
                Command="{x:Static local:Commands.PushButton}"
                >
    </Button>
</StackPanel>

using System;
using System.ComponentModel;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

namespace WpfApplication1
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            DataContext = new Model();
            ((Model)DataContext).PropertyChanged += DataContext_PropertyChanged;
            InitializeComponent();
        }

        private void DataContext_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            switch (e.PropertyName)
            {
                case "Ready":
                    CommandManager.InvalidateRequerySuggested();
                    break;
            }
        }

        void CanExecuteRunTask(object sender, CanExecuteRoutedEventArgs e)
        {
            e.CanExecute = true;
        }

        void ExecuteRunTask(object sender, ExecutedRoutedEventArgs e)
        {
            ((Model)DataContext).RunLongTask();
        }

        void CanPushButton(object sender, CanExecuteRoutedEventArgs e)
        {
            e.CanExecute = ((Model)DataContext).Ready;
        }

        void PushButton(object sender, ExecutedRoutedEventArgs e) { }
    }

    public static class Commands
    {
        public static readonly RoutedUICommand RunTask = new     RoutedUICommand();
        public static readonly RoutedUICommand PushButton = new     RoutedUICommand();
    }

    public class Model : INotifyPropertyChanged
    {
        private bool _ready = false;
        public bool Ready
        {
            get { return _ready; }
            set
            {
                _ready = value;
                RaisePropertyChanged("Ready");
            }
        }

        private int _value = 0;
        public int Value
        {
            get { return _value; }
            set
            {
                _value = value;
                RaisePropertyChanged("Value");
            }
        }

        public async void RunLongTask()
        {
            await RunLongTaskAsync();   
        }
        private Task RunLongTaskAsync()
        {
            this.Ready = false;
            Task t = new Task(() => {
                for (int i = 0; i <= 100; i++)
                {
                    Value = i;
                    System.Threading.Thread.Sleep(20);
                }
                this.Ready = true;
            });
            t.Start();
            return t;
        }

        public event PropertyChangedEventHandler PropertyChanged;
        protected void RaisePropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}
Community
  • 1
  • 1
Leonardo Spina
  • 680
  • 6
  • 15
  • Please read http://stackoverflow.com/help/how-to-ask – DotNetRussell Nov 12 '15 at 12:25
  • Hi Anthony, I suppose you meant the title wasn't clear, I hope it's better now. Thanks. – Leonardo Spina Nov 12 '15 at 12:36
  • Leo, no, I mean please read the guide on how to ask questions. This is not a blog. You don't add to your question with an answer. You don't add smiley faces. You don't post questions without actual code to test. Think of this more of a historical document. You give as much detail as possible on your problem and someone solves it. Then, in the future it can be used to answer the same question. The format of this entire thing is wrong. – DotNetRussell Nov 12 '15 at 13:35
  • I'll integrate it as you ask, but actually I don't completely agree with you, since the question adding form allows you to insert directly the answer at the same time of the question, exactly because you just want to share a solution that you found to your own problem without actually asking for help, but just to share a solution that has been hard to find. As it's actually suggested here: http://blog.stackoverflow.com/2011/07/its-ok-to-ask-and-answer-your-own-questions/ Anyway I'll add some code to test as you correctly suggest. – Leonardo Spina Nov 12 '15 at 14:18
  • Leo, I didn't say it wasn't okay to answer your own question. I do it myself on occasion. What I said, was that your question is poorly formatted and your answer contains information that should be in your question. Please conform to the site's posting standards. Thanks – DotNetRussell Nov 12 '15 at 14:46
  • 1
    Hi Anthony, you were right looks much better now. Thanks and sorry again. – Leonardo Spina Nov 12 '15 at 16:24
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/94953/discussion-between-leo-and-anthony-russell). – Leonardo Spina Nov 12 '15 at 16:24

1 Answers1

3

While the "PropertyChanged" events are automatically marshalled, the call to CommandManager.InvalidateRequerySuggested() is not.

So to fix the issue just ensure the call is made on the Dispatcher thread as follows:

    private void DataContext_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        switch (e.PropertyName)
        {
            case "Ready":
                var d = Application.Current.Dispatcher;
                if (d.CheckAccess())
                    CommandManager.InvalidateRequerySuggested();
                else
                    d.BeginInvoke((Action)(() => { CommandManager.InvalidateRequerySuggested(); }));
                break;
        }
    }

Be careful when relying on property changes that are triggered by asynchronous tasks.

I hope this can help some of you in saving some days of work...

Leonardo Spina
  • 680
  • 6
  • 15