-1

I have the command

public class RelayActionCommand : ICommand
{
    /// <summary>
    /// The Action Delegate representing a method with input parameter 
    /// </summary>
    public Action<object> ExecuteAction { get; set; }
    /// <summary>
    /// The Delegate, used to represent the method which defines criteria for the execution 
    /// </summary>
    public Predicate<object> CanExecuteAction { get; set; }

    public bool CanExecute(object parameter)
    {
        if (CanExecuteAction != null)
        {
            return CanExecuteAction(parameter);
        }
        return true;
    }

    public event EventHandler CanExecuteChanged
    {
        add { CommandManager.RequerySuggested += value; }
        remove { CommandManager.RequerySuggested -= value; }
    }

    public void Execute(object parameter)
    {
        if (ExecuteAction != null)
        {
            ExecuteAction(parameter);
        }
    }
}

To use it,

public RelayActionCommand SearchPersonCommnad { get; set; }

    DataAccess objds;
    public PersonViewModel()
    {
        Persons = new ObservableCollection<PersonInfo>();
        objds = new DataAccess();


        Persons = new ObservableCollection<PersonInfo>(objds.GetPersonData());

        var defaultView = CollectionViewSource.GetDefaultView(Persons);

        //based upon the data entered in the TextBox
        SearchPersonCommnad = new RelayActionCommand()
        {
            CanExecuteAction = n=> !String.IsNullOrEmpty(Name),
             ExecuteAction = n => defaultView.Filter = name => ((PersonInfo)name).FirstName.StartsWith(Name) 
                 || ((PersonInfo)name).LastName.StartsWith(Name) 
                 || ((PersonInfo)name).City==Name
        };

At the beginning, the button is disabled. But in running time, it changes by different situations. My question is how to set up the button's IsEnabled property with it? Which means, when ExecuteAction I have to set up the property correctly.

UPDATE:

I use ICommand not DelegateCommand.

Bigeyes
  • 1,508
  • 2
  • 23
  • 42
  • With the implementation you've chosen for `RelayActionCommand`, you'll want to look at [this answer](https://stackoverflow.com/questions/12662580/command-source-disabling-and-enabling) among the duplicates. But, you can use different implementations that allow you to raise the event directly on the `ICommand` in question (i.e. add a `RaiseCanExecuteChanged()` method on the `ICommand` object), which will work just as well (better, actually, because you don't wind up having to raise the event on _all_ your commands, just the one that changed) – Peter Duniho Jan 13 '18 at 04:38
  • I don't have `RaiseCanExecuteChanged` method in `ICommand` as it is from `System.Windows.Input` namespace. And I am not sure how to use CommandManager for the separate bool property in ViewModel. Please advise me. – Bigeyes Jan 16 '18 at 14:55
  • _"I don't have RaiseCanExecuteChanged method in ICommand"_ -- you don't need one. _You_ wrote the `ICommand` implementation (in this case, `RelayActionCommand`), so you can put whatever other public (or even private) members in that implementation you want, including a method named `RaiseCanExecuteChanged()` that raises that event. Just because WPF views the object strictly as `ICommand`, that doesn't mean you have to. You can cast back to the appropriate concrete type, or just store a reference as that type in the first place. Note that you'll have to change the `event` implementation as well – Peter Duniho Jan 16 '18 at 17:52
  • _"I am not sure how to use CommandManager for the separate bool property in ViewModel"_ -- it's not clear what you mean by that. In your code above, `CommandManager` is used for the `CanExecuteChanged` event, i.e. rather than the `RelayActionCommand` raising the event itself, it just delegates that to `CommandManager`. With that implementation your only option is to call `InvalidRequerySuggested()`. You would do that any time the `bool` value, or any other value that affects the `CanExecute()` result, changes. ... – Peter Duniho Jan 16 '18 at 17:58
  • ... If you implement `RaiseCanExecuteChanged()`, you no longer need that. Instead, you call `RaiseCanExecuteChanged()` if any value that affects _that specific `ICommand`'s_ `CanExecute()` value has changed. – Peter Duniho Jan 16 '18 at 17:58
  • "I am not sure how to use CommandManager for the separate bool property in ViewModel"---which means I have a bool property `IsSaveButtonEnabled` in the ViewModel. I want to set the value according to `CanExecute()` or not. I need to hook up them together. Because I will use the bool value `IsSaveButtonEnabled` to style the background. – Bigeyes Jan 17 '18 at 00:17

1 Answers1

-1

You can use the CanExecute method, but it is good practice is actually to avoid this, and bind the button's enabled state to a separate boolean property of the view model. Most other solutions will have unexpected effects, or be suboptimal. Why?

CanExecute is a method. This means that it needs to be polled for the button state to change. You can force the control that's using the command to re-poll on a status change, but the code is much cleaner and more straightforward if you just use a property on the view model. This is because as a method, you can't use INotifyPropertyChanged to notify for changes, whereas with a property you can.

The danger in using CanExecute is that the user will manage to click the button after the method would return false, but before the button's enablement has changed.

Edit: Code to do what you want:

public class ViewModel : INotifyPropertyChanged
{
    private int someValue;
    private bool isEnabled;

    public ViewModel()
    {
        MyCommand = new RelayActionCommand(Click);
    }

    private void Click(object obj)
    {
        //Do something.
    }

    /// <summary>
    /// Bind this to the IsEnabled property of the button, and 
    /// also the background using a convertor or see ButtonBackground.
    /// </summary>
    public bool IsEnabled => SomeValue < 5;

    /// <summary>
    /// Option 2 - use this property to bind to the background of the button.
    /// </summary>
    public Brush ButtonBackground => IsEnabled ? Brushes.SeaShell : Brushes.AntiqueWhite;

    public int SomeValue
    {
        get { return someValue; }
        set
        {
            if (value == someValue) return;
            someValue = value;
            OnPropertyChanged();
            OnPropertyChanged(nameof(IsEnabled));
            OnPropertyChanged(nameof(ButtonBackground));
        }
    }

    /// <summary>
    /// Bind this to the command of the button.
    /// </summary>
    public RelayActionCommand MyCommand { get; }

    public event PropertyChangedEventHandler PropertyChanged;

    [NotifyPropertyChangedInvocator]
    protected virtual void OnPropertyChanged
        ([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

Relay command fixed up a bit to avoid using CanExecute:

public class RelayActionCommand : ICommand
{
    public RelayActionCommand(Action<object> executeAction)
    {
        ExecuteAction = executeAction;
    }

    /// <summary>
    /// The Action Delegate representing a method with input parameter 
    /// </summary>
    public Action<object> ExecuteAction { get; }
    /// <summary>

    public bool CanExecute(object parameter)
    {
        return true;
    }

    public void Execute(object parameter)
    {
        ExecuteAction?.Invoke(parameter);
    }

    //Deliberately empty.
    public event EventHandler CanExecuteChanged
    {
        add { }
        remove { }
    }
}

EDIT 2: Code to do what you want using a DelegateCommand

Note, this does not use InvalidateRequerySuggested - mainly because it refreshes all buttons when any CanExecute changes, which is a poor solution. As you can see, this is less immediately straightforward than putting the code in the view model directly, but whatever floats your boat I guess.

public sealed class ViewModel : INotifyPropertyChanged
{
    private int calls;

    public ViewModel()
    {
        SafeOnceCommand = new RelayCommand(DoItOnce, HasDoneIt);    
    }

    private bool HasDoneIt()
    {
        return Calls == 0;
    }

    private void DoItOnce()
    {
        if (Calls > 0) throw new InvalidOperationException();
        Calls++;
    }

    public int Calls
    {
        get { return calls; }
        set
        {
            if (value == calls) return;
            calls = value;
            OnPropertyChanged();
            SafeOnceCommand.RaiseCanExecuteChanged();
        }
    }

    public RelayCommand SafeOnceCommand { get; }

    public event PropertyChangedEventHandler PropertyChanged;

    [NotifyPropertyChangedInvocator]
    private void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

public sealed class RelayCommand : ICommand
{
    private readonly Action execute;
    private readonly Func<bool> canExecute;
    private readonly List<EventHandler> invocationList = new List<EventHandler>();

    public RelayCommand(Action execute, Func<bool> canExecute)
    {
        this.execute = execute;
        this.canExecute = canExecute;
    }

    public bool CanExecute(object parameter)
    {
        return canExecute();
    }

    public void Execute(object parameter)
    {
        execute();
    }

    /// <summary>
    /// Method to raise CanExecuteChanged event
    /// </summary>
    public void RaiseCanExecuteChanged()
    {
        foreach (var elem in invocationList)
        {
            elem(null, EventArgs.Empty);
        }
    }

    public event EventHandler CanExecuteChanged
    {
        add { invocationList.Add(value); }
        remove { invocationList.Remove(value); }
    }
}
Adam Brown
  • 1,667
  • 7
  • 9
  • I want to change the button's background color based on the IsEnabled property. – Bigeyes Jan 12 '18 at 17:03
  • That's a different question - you can add a style to the button, or just bind the Background property using a converter to the boolean property on your view model - whatever you find easiest. – Adam Brown Jan 12 '18 at 17:13
  • It is not a different question. I want to set IsEnable=true when CanExecute is true. Then using style or converter. The later is another story but needs the first one. – Bigeyes Jan 12 '18 at 17:18
  • Yes. So again, this is all going to be easier with a bound property on the view model, rather than using CanExecute. I'll update my answer to show how to do this in the view model. – Adam Brown Jan 12 '18 at 17:24
  • Okay. I am waiting your updated answer then test it. – Bigeyes Jan 12 '18 at 20:58
  • _"best practice is actually to avoid this, and bind the button's enabled state to a separate boolean property of the view model"_ -- I disagree 100% with this claim. And your justification of it makes no sense: _"CanExecute is a method. This means that it needs to be polled for the button state to change"_. A property getter is a method too, so that `CanExecute()` is a method can't justify your claim. And it _doesn't need to be polled_. You just raise the `ICommand.CanExecuteChanged` event, which works exactly like the `INotifyPropertyChanged.PropertyChanged` event. – Peter Duniho Jan 13 '18 at 04:29
  • @PeterDuniho - please see this: https://stackoverflow.com/questions/1751966/commandmanager-invalidaterequerysuggested-isnt-fast-enough-what-can-i-do/1857619 – Adam Brown Jan 13 '18 at 08:34
  • @Adam: _"please see this: https://stackoverflow.com/questions/1751966/commandmanager-invalidaterequerysuggested-isnt-fast-enough-what-can-i-do/1857619"_ -- seen it. Your point is? In the very Q&A you link, the two top-voted answers do exactly what I already pointed out above: raise `CanExecuteChanged`. Why you seem to be implying that there'd be something there supporting your incorrect claim, I have no idea. – Peter Duniho Jan 13 '18 at 08:57
  • @PeterDuniho - I can't positively assert that the accepted answer to that solves the issue safely (the danger being a delay between the actual change and the state change of the button - they are making use of the dispatcher to raise the change events though). I see, however, that it requires about 200 lines of code to get that far. My solution requires a single binding, so I think it scores quite heavily over that. The point being that one line of code is typically safer and easier to understand than 200. – Adam Brown Jan 13 '18 at 10:13
  • Generalized solutions tend to be wordier, because you only have to code them once, instead of over and over again, and because they cover more scenarios. Disingenuous of you to compare strictly on a line-count, especially when the other answer offers the concise alternative. And regardless, that's far from proving an assertion of "best practice", which when you write, you are speaking for an entire community, something I don't believe you have been granted the authority to do, especially without citation. The "one line of code you seek" is the line that calls `RaiseCanExecuteChanged()`. – Peter Duniho Jan 13 '18 at 17:44
  • Point taken. I have edited the post to say "good practice" which is opinion - based. Still, it's clear that the advantages of having a property (with simplifies not only the view model code, but also the binding code for the OP) outweight any advantages of the other approach. – Adam Brown Jan 13 '18 at 20:50