Thursday, January 17, 2019

Re-evaluating RoutedCommands from code

I have a requirement to launch a background process that, when it finishes, creates a condition that allows a command to be executed. I need a way to tell WPF to re-evaluate a command from code. Normally this happens only when the user types or clicks the mouse.

The trick is to use Windows.Input.CommandManager.InvalidateRequerySuggested()

Here's an overly complicated example. Start a new WPF App project using C# and call it EvaluateCommand.


The main window will have two buttons and a TextBlock. Each button is linked to a command that is only executable when the TextBlock has a specific content. Each button launches a background task that changes the content of the TextBlock. We need that task to cause the commands' CanExecute methods to be re-evaluated when the task is done.

Here is the XAML

<Window x:Class="EvaluateCommand.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:EvaluateCommand"
        mc:Ignorable="d"
        DataContext="{Binding RelativeSource={RelativeSource Self}}"
        Title="Evaluate Command" SizeToContent="WidthAndHeight">
    <Window.Resources>
        <RoutedCommand x:Key="FailCommand"/>
        <RoutedCommand x:Key="PassCommand"/>
    </Window.Resources>
    <Window.CommandBindings>
        <CommandBinding Command="{StaticResource FailCommand}" CanExecute="Fail_CanExecute" Executed="Fail_Executed"/>
        <CommandBinding Command="{StaticResource PassCommand}" CanExecute="Pass_CanExecute" Executed="Pass_Executed"/>
    </Window.CommandBindings>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="auto"/>
            <ColumnDefinition Width="auto"/>
            <ColumnDefinition Width="auto"/>
        </Grid.ColumnDefinitions>
        <Button Grid.Column="0" Content="Make it Fail" Command="{StaticResource FailCommand}" Width="100" Margin="20"/>
        <TextBlock Grid.Column="1" Text="{Binding Status}" Width="100" Margin="20"/>
        <Button Grid.Column="2" Content="Make it Pass" Command="{StaticResource PassCommand}" Width="100" Margin="20"/>
    </Grid>
</Window>


Here's the code. Note InvalidateRequerySuggested cannot be called from the background worker thread. It must be called from the UI thread once the background worker has finished. The sleep is to make it obvious the [Make it Pass] button is not being re-evaluated from the initial button click.

using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Input;
using System.Runtime.CompilerServices;

namespace EvaluateCommand
{

    public partial class MainWindow : Window, INotifyPropertyChanged
    {
        private string _Status = "PASS";

        public string Status
        {
            get { return _Status; }
            set { SetProperty(ref _Status, value); }
        }
        public MainWindow()
        {
            InitializeComponent();
        }

        private void Fail_CanExecute(object sender, CanExecuteRoutedEventArgs e)
        {
            e.CanExecute = (Status == "PASS");
        }

        private void Fail_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            SetStatusAsync("FAIL");
        }

        private void Pass_CanExecute(object sender, CanExecuteRoutedEventArgs e)
        {
            e.CanExecute = (Status == "FAIL");
        }

        private void Pass_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            SetStatusAsync("PASS");
        }

        private void SetStatusAsync(String NewStatus)
        {
            Status = "Processing...";
            BackgroundWorker BW = new BackgroundWorker();
            BW.DoWork += SetStatus;
            BW.RunWorkerCompleted += EvaluateCommands;
            BW.RunWorkerAsync(NewStatus);
            BW.Dispose();
        }

        private void SetStatus(object sender, DoWorkEventArgs e)
        {
            System.Threading.Thread.Sleep(1000);
            Dispatcher.Invoke(new Action(() => Status = e.Argument.ToString()));
        }

        private void EvaluateCommands(object sender, RunWorkerCompletedEventArgs e)
        {
            System.Windows.Input.CommandManager.InvalidateRequerySuggested();
        }

        public event PropertyChangedEventHandler PropertyChanged;
        public bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
        {
            if (Object.Equals(storage, value)) return false;
            storage = value;
            OnPropertyChanged(propertyName);
            return true;
        }
        private void OnPropertyChanged(String PropertyName = "")
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(PropertyName));
            }
        }

    }
}