Search code examples
c#wpfmvvm

Command<int> CanExecute Not Working Properly in WPF View


I'm currently working on a WPF screen for a view where we encounter an unusual issue with commands of type Command<int> or Command<double>.

In our application, there's a control with a Command<int> bound to it. We also bind the CanExecute method to the command, which should enable or disable the control based on the current conditions. However, when we define the command as RelayCommandWpf<int>, the CanExecute function stops working correctly—it only triggers once at initialization and never again after that.

Surprisingly, if we change the command type to RelayCommandWpf<string>, the CanExecute method works as expected, and the control enables/disables as it should.

My code is basically defined like this:

Command

public RelayCommandWpf<int> MotorCommand => _motorCommand ??= new RelayCommandWpf<int>(SendMotorConfig, CanSendMotorConfig);

XAML

<Window x:Class="MotorViewExample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Motor View Example" Height="200" Width="300">
    <StackPanel>
        <Button Content="Execute Motor Command" 
                Command="{Binding MotorCommand}" 
                CommandParameter="5" 
                Width="200" 
                Height="50"/>
    </StackPanel>
</Window>

RelayCommandWpf:

using System;
using System.Diagnostics.CodeAnalysis;
using System.Windows.Input;

namespace VGUI.Boilerplate
{
    /// <summary>
    /// A generic command whose sole purpose is to relay its functionality to other objects by
    /// invoking delegates. The default return value for the CanExecute method is 'true'. This class
    /// allows you to accept command parameters in the Execute and CanExecute callback methods.
    /// </summary>
    ///
    /// <remarks>
    /// If you are using this class in WPF4.5 or above, you need to use the
    /// GalaSoft.MvvmLight.CommandWpf namespace (instead of GalaSoft.MvvmLight.Command). This will
    /// enable (or restore) the CommandManager class which handles automatic enabling/disabling of
    /// controls based on the CanExecute delegate.
    /// </remarks>
    ///
    /// <typeparam name="T">    The type of the command parameter. </typeparam>
    public class RelayCommandWpf<T> : ICommand
    {
        private readonly WeakAction<T> _execute;

        private readonly WeakFunc<T, bool> _canExecute;

        /// <summary>
        /// Initializes a new instance of the RelayCommand class that can always execute.
        /// </summary>
        ///
        /// <param name="execute">          The execution logic. IMPORTANT: If the action causes a
        ///                                 closure, you must set keepTargetAlive to true to avoid side
        ///                                 effects. </param>
        /// <param name="keepTargetAlive">  (Optional) If true, the target of the Action will be kept as
        ///                                 a hard reference, which might cause a memory leak. You should
        ///                                 only set this parameter to true if the action is causing a
        ///                                 closure. See http://galasoft.ch/s/mvvmweakaction. </param>
        ///
        /// ### <exception cref="ArgumentNullException">    If the execute argument is null. </exception>
        public RelayCommandWpf(Action<T> execute, bool keepTargetAlive = false)
            : this(execute, null, keepTargetAlive)
        {
        }

        /// <summary>   Initializes a new instance of the RelayCommand class. </summary>
        ///
        /// <exception cref="ArgumentNullException">    If the execute argument is null. </exception>
        ///
        /// <param name="execute">          The execution logic. IMPORTANT: If the action causes a
        ///                                 closure, you must set keepTargetAlive to true to avoid side
        ///                                 effects. </param>
        /// <param name="canExecute">       The execution status logic.  IMPORTANT: If the func causes a
        ///                                 closure, you must set keepTargetAlive to true to avoid side
        ///                                 effects. </param>
        /// <param name="keepTargetAlive">  (Optional) If true, the target of the Action will be kept as
        ///                                 a hard reference, which might cause a memory leak. You should
        ///                                 only set this parameter to true if the action is causing a
        ///                                 closure. See http://galasoft.ch/s/mvvmweakaction. </param>
        public RelayCommandWpf(Action<T> execute, Func<T, bool> canExecute, bool keepTargetAlive = false)
        {
            if (execute == null)
            {
                throw new ArgumentNullException("execute");
            }

            _execute = new WeakAction<T>(execute, keepTargetAlive);

            if (canExecute != null)
            {
                _canExecute = new WeakFunc<T, bool>(canExecute, keepTargetAlive);
            }
        }

        /// <summary>   Occurs when changes occur that affect whether the command should execute. </summary>
        public event EventHandler CanExecuteChanged
        {
            add
            {
                if (_canExecute != null)
                {
                    CommandManager.RequerySuggested += value;
                }
            }

            remove
            {
                if (_canExecute != null)
                {
                    CommandManager.RequerySuggested -= value;
                }
            }
        }

        /// <summary>   Raises the <see cref="CanExecuteChanged" /> event. </summary>
        [SuppressMessage(
            "Microsoft.Performance",
            "CA1822:MarkMembersAsStatic",
            Justification = "The this keyword is used in the Silverlight version")]
        [SuppressMessage(
            "Microsoft.Design",
            "CA1030:UseEventsWhereAppropriate",
            Justification = "This cannot be an event")]
        public void RaiseCanExecuteChanged()
        {
            CommandManager.InvalidateRequerySuggested();
        }

        /// <summary>
        /// Defines the method that determines whether the command can execute in its current state.
        /// </summary>
        ///
        /// <param name="parameter">    Data used by the command. If the command does not require data to
        ///                             be passed, this object can be set to a null reference. </param>
        ///
        /// <returns>   true if this command can be executed; otherwise, false. </returns>
        public bool CanExecute(object parameter)
        {
            if (_canExecute == null)
            {
                return true;
            }

            if (_canExecute.IsStatic || _canExecute.IsAlive)
            {
                if (parameter == null
                    && typeof(T).IsValueType)
                {
                    return _canExecute.Execute(default(T));
                }

                if (parameter == null || parameter is T)
                {
                    return (_canExecute.Execute((T)parameter));
                }
            }

            return false;
        }

        /// <summary>   Defines the method to be called when the command is invoked. </summary>
        ///
        /// <param name="parameter">    Data used by the command. If the command does not require data to
        ///                             be passed, this object can be set to a null reference. </param>
        public virtual void Execute(object parameter)
        {
            var val = parameter;

            if (parameter != null
                && parameter.GetType() != typeof(T))
            {
                if (parameter is IConvertible)
                {
                    val = Convert.ChangeType(parameter, typeof(T), null);
                }
            }

            if (CanExecute(val)
                && _execute != null
                && (_execute.IsStatic || _execute.IsAlive))
            {
                if (val == null)
                {
                    if (typeof(T).IsValueType)
                    {
                        _execute.Execute(default(T));
                    }
                    else
                    {
                        // ReSharper disable ExpressionIsAlwaysNull
                        _execute.Execute((T)val);
                        // ReSharper restore ExpressionIsAlwaysNull
                    }
                }
                else
                {
                    _execute.Execute((T)val);
                }
            }
        }
    }
}

Does anyone know why RelayCommandWpf<int> or RelayCommandWpf<double> would cause CanExecute to stop working in WPF? Is there a known issue with these types or something specific to integer or double commands in WPF?

Any help or insights would be appreciated. Thanks!


Solution

  • The expression CommandParameter="5" in XAML assigns the string "5" to the CommandParameter property, so that _canExecute is never called - because parameter is T is false.

    The relevant code fragment (with unnecessary parentheses removed):

    if (parameter == null || parameter is T)
    {
        return _canExecute.Execute((T)parameter);
    }
    

    You may either modify the RelayCommand class to check whether a string parameter could be converted to a T value, or you explicitly assign an int value in XAML, e.g. like this:

    xmlns:sys="clr-namespace:System;assembly=mscorlib"
    ...
    
    <Button ...>
        <Button.CommandParameter>
            <sys:Int32>5</sys:Int32>
        </Button.CommandParameter>
    </Button>