MVVM bind RelayCommand CanExecute to a Property?
Asked Answered
E

3

4

I have a Timer and three buttons to control it: Start, Stop, and Pause.
Each button is bound to a RelayCommand.
I have a TimerState property of type enum TimerState. (This is useful for setting various GUI elements.)
Is there a way to somehow bind the RelayCommands' CanExecute functionality to the TimerState property?
Currently, I have 3 methods that look like this:
private bool CanStartTimer() { return (TimerState == TimerState.Stopped || TimerState == TimerState.Paused); }
In the TimerState setter, I call

StartTimerCmd.RaiseCanExecuteChanged();  

Is there a better way bind the CanExecute state of the RelayCommands to a property like TimerState?
Thanks for any insight.

Evolutionary answered 6/8, 2014 at 17:18 Comment(3)
Is there something specific you don't like about this? As long as you are notifying when the condition changes, which you are in the timer state setter, it should be fine.Lowercase
@kidshaw: it feels like the TimerState property has know what is dependent on it, rather than something like the RelayCommand CanExecute 'listening' for changes to the TimerState property.Evolutionary
Well you can bind IsEnabled property of button to a property. And remove CanExecute from RelayCommand,Hatband
M
7

I've implemented a class to handle commands, actually it's based on DelegateCommand because i'm using PRISM but it could easily be changed to be used with RelayCommand or any other class implementing ICommand

It could have bugs, i've not yet fully tested it, however it works fine in my scenarios, here it is:

public class MyDelegateCommand<TViewModel> : DelegateCommand where TViewModel : INotifyPropertyChanged {
  private List<string> _PropertiesToWatch;

  public MyDelegateCommand(TViewModel viewModelInstance, Action executedMethod)
     : base(executedMethod) {
  }

  public MyDelegateCommand(TViewModel viewModelInstance, Action executedMethod, Func<bool> canExecuteMethod)
     : base(executedMethod, canExecuteMethod) {
  }

  /// <summary>
  /// 
  /// </summary>
  /// <param name="viewModelInstance"></param>
  /// <param name="executedMethod"></param>
  /// <param name="selector"></param>
  public MyDelegateCommand(TViewModel viewModelInstance, Action executedMethod, Func<bool> canExecuteMethod, Expression<Func<TViewModel, object>> propertiesToWatch)
     : base(executedMethod, canExecuteMethod) {

     _PropertiesToWatch = RegisterPropertiesWatcher(propertiesToWatch);
     viewModelInstance.PropertyChanged += PropertyChangedHandler;
  }


  /// <summary>
  /// handler that, everytime a monitored property changes, calls the RaiseCanExecuteChanged of the command
  /// </summary>
  /// <param name="sender"></param>
  /// <param name="e"></param>
  private void PropertyChangedHandler(object sender, PropertyChangedEventArgs e) {
     if (_PropertiesToWatch.Contains(e.PropertyName)) {
        this.OnCanExecuteChanged();
     }
  }

  /// <summary>
  /// giving an expression that identify a propriety or a list of properties, return the property names obtained from the expression
  /// Examples on selector usage
  /// proprietà singola:
  ///   entity => entity.PropertyName
  /// proprietà multiple
  ///   entity => new { entity.PropertyName1, entity.PropertyName2 }
  /// </summary>
  /// <param name="selector"></param>
  /// <returns></returns>
  protected List<string> RegisterPropertiesWatcher(Expression<Func<TViewModel, object>> selector) {
     List<string> properties = new List<string>();

     System.Linq.Expressions.LambdaExpression lambda = (System.Linq.Expressions.LambdaExpression)selector;

     if (lambda.Body is System.Linq.Expressions.MemberExpression) {
        System.Linq.Expressions.MemberExpression memberExpression = (System.Linq.Expressions.MemberExpression)(lambda.Body);
        properties.Add(memberExpression.Member.Name);
     }
     else if (lambda.Body is System.Linq.Expressions.UnaryExpression) {
        System.Linq.Expressions.UnaryExpression unaryExpression = (System.Linq.Expressions.UnaryExpression)(lambda.Body);

        properties.Add(((System.Linq.Expressions.MemberExpression)(unaryExpression.Operand)).Member.Name);
     }
     else if (lambda.Body.NodeType == ExpressionType.New) {
        NewExpression newExp = (NewExpression)lambda.Body;
        foreach (var argument in newExp.Arguments) {
           if (argument is System.Linq.Expressions.MemberExpression) {
              System.Linq.Expressions.MemberExpression mExp = (System.Linq.Expressions.MemberExpression)argument;
              properties.Add(mExp.Member.Name);
           }
           else {
              throw new SyntaxErrorException("Syntax Error, selector has to be an expression that returns a new object containing a list of properties, e.g.: s => new { s.Property1, s.Property2 }");
           }
        }
     }
     else {
        throw new SyntaxErrorException("Syntax Error, selector has to be an expression that returns a new object containing a list of properties, e.g.: s => new { s.Property1, s.Property2 }");
     }

     return properties;
  }

}

note that my solution implies that this command has to be wired with the viewmodel that handle it and the viewmodel has to implement the INotifyPropertyChanged interface.

first two constructor are there so the command is backward compatible with DelegateCommand but the 3rd is the important one, that accepts a linq expression to specify which property to monitor

the usage is pretty simple and easy to understand, let me write it here with methods but of course you can create your handler methods. Suppose you have have a ViewModel called MyViewModel with two properties (PropertyX and PropertyY) that rise the propertychanged event, and somewhere in it you create an instance of SaveCommand, it would look like this:

this.SaveCommand = new MyDelegateCommand<MyViewModel>(this,
        //execute
        () => {
          Console.Write("EXECUTED");
        },
        //can execute
        () => {
          Console.Write("Checking Validity");
           return PropertyX!=null && PropertyY!=null && PropertyY.Length < 5;
        },
        //properties to watch
        (p) => new { p.PropertyX, p.PropertyY }
     );

maybe i'll create an article somewhere about this, but this snippet should be clear i hope

Mullinax answered 22/5, 2015 at 10:28 Comment(1)
Very nice. I've posted a parameterized version in another answer.Denisdenise
D
3

Fabio's answer works well. Here's a parameterized version for DelegateCommand<T>. (I've tightened up the code a little, too.)

public class DepedencyCommand<TViewModel, TArg> : DelegateCommand<TArg>
    where TViewModel : INotifyPropertyChanged
{
    private readonly List<string> _propertiesToWatch;

    public DepedencyCommand(Action<TArg> executedMethod)
        : base(executedMethod) { }

    public DepedencyCommand(Action<TArg> executedMethod, Func<TArg, bool> canExecuteMethod)
        : base(executedMethod, canExecuteMethod) { }

    public DepedencyCommand(TViewModel viewModelInstance, Action<TArg> executedMethod, Func<TArg, bool> canExecuteMethod, Expression<Func<TViewModel, object>> propertiesToWatch)
        : base(executedMethod, canExecuteMethod)
    {

        _propertiesToWatch = _RegisterPropertiesWatcher(propertiesToWatch);
        viewModelInstance.PropertyChanged += PropertyChangedHandler;
    }


    /// <summary>
    /// handler that, everytime a monitored property changes, calls the RaiseCanExecuteChanged of the command
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void PropertyChangedHandler(object sender, PropertyChangedEventArgs e)
    {
        if (_propertiesToWatch.Contains(e.PropertyName))
        {
            this.OnCanExecuteChanged();
        }
    }

    /// <summary>
    /// giving an expression that identify a propriety or a list of properties, return the property names obtained from the expression
    /// Examples on selector usage
    /// proprietà singola:
    ///   entity => entity.PropertyName
    /// proprietà multiple
    ///   entity => new { entity.PropertyName1, entity.PropertyName2 }
    /// </summary>
    /// <param name="selector"></param>
    /// <returns></returns>
    private static List<string> _RegisterPropertiesWatcher(Expression<Func<TViewModel, object>> selector)
    {
        var properties = new List<string>();

        LambdaExpression lambda = selector;

        if (lambda.Body is MemberExpression)
        {
            var memberExpression = (MemberExpression)lambda.Body;
            properties.Add(memberExpression.Member.Name);
        }
        else if (lambda.Body is UnaryExpression)
        {
            var unaryExpression = (UnaryExpression)lambda.Body;

            properties.Add(((MemberExpression)unaryExpression.Operand).Member.Name);
        }
        else if (lambda.Body.NodeType == ExpressionType.New)
        {
            var newExp = (NewExpression)lambda.Body;
            foreach (var argument in newExp.Arguments)
            {
                if (argument is MemberExpression)
                {
                    MemberExpression mExp = (MemberExpression)argument;
                    properties.Add(mExp.Member.Name);
                }
                else
                    throw new SyntaxErrorException("Syntax Error, selector has to be an expression that returns a new object containing a list of properties, e.g.: s => new { s.Property1, s.Property2 }");
            }
        }
        else
            throw new SyntaxErrorException("Syntax Error, selector has to be an expression that returns a new object containing a list of properties, e.g.: s => new { s.Property1, s.Property2 }");

        return properties;
    }
}
Denisdenise answered 20/4, 2016 at 18:10 Comment(0)
L
0

There doesn't seem to be a better solution. I know what you mean, it seems inelegant but whatever lipstick you put on it, the onus is on the objects involved in the expression to notify the command.

If your condition is based purely on other notify properties you could add your own handler to PropertyChanged, that provides a bit of abstraction.

In this case, TimerState would be a VM property. Then you can a handler to your ViewModel.PropertyChanged event. You can then inspect the property name and update your CanExecute. This is still ugly, but at least you have all the garbage in one block.

Lowercase answered 6/8, 2014 at 18:28 Comment(2)
Thanks for the reply. I had already started down the path of adding the PropertyChanged handler to the MainViewModel, and calling RelayCommand.RaiseCanExecuteChanged() in response to a change in the appropriate property (TimerState).Evolutionary
Fingers crossed that MS give us something better next time!Lowercase

© 2022 - 2024 — McMap. All rights reserved.