MVVM bind RelayCommand CanExecute to a Property?

后端 未结 3 950
情歌与酒
情歌与酒 2021-01-14 02:04

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 Timer

相关标签:
3条回答
  • 2021-01-14 02:39

    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

    0 讨论(0)
  • 2021-01-14 02:47

    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.

    0 讨论(0)
  • 2021-01-14 02:52

    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;
        }
    }
    
    0 讨论(0)
提交回复
热议问题