0

Working off of this question, I'm attempting to make a UI with two buttons. Pressing one button disables itself and enables the other. I'm using the MVVM pattern and DelegateCommand:

MainWindow XAML

<Window x:Class="WpfApp1.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:WpfApp1"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.DataContext>
        <local:ViewModel />
    </Window.DataContext>

    <StackPanel>
        <Rectangle Height="100"></Rectangle>
        <Button Command="{Binding StartServer}" >Start Server</Button>
        <Button Command="{Binding StopServer}" >Stop Server</Button>
    </StackPanel>
</Window>

ViewModel

public class ViewModel : INotifyPropertyChanged
{
  public ViewModel()
  {
    PropertyChanged += PropertyChangedHandler;
  }

  private void PropertyChangedHandler(object sender, PropertyChangedEventArgs e)
  {
    switch (e.PropertyName)
    {
      case nameof(ServerIsRunning):
        StartServer.RaiseCanExecuteChanged();
        StopServer.RaiseCanExecuteChanged();
        break;
    }
  }

  private bool m_serverIsRunning;
  public bool ServerIsRunning
  {
    get => m_serverIsRunning;
    set
    {
      m_serverIsRunning = value;
      RaisePropertyChanged(nameof(ServerIsRunning));
    }
  }

  public DelegateCommand<object> StartServer => new DelegateCommand<object>(
    context =>
    {
      ServerIsRunning = true;
    }, e => !ServerIsRunning);

  public DelegateCommand<object> StopServer => new DelegateCommand<object>(
    context =>
    {
      ServerIsRunning = false;
    }, e => ServerIsRunning);

  public event PropertyChangedEventHandler PropertyChanged;

  public void RaisePropertyChanged(string property)
  {
    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));
  }
}

DelegateCommand

public class DelegateCommand<T> : ICommand
{
  private readonly Action<object> ExecuteAction;
  private readonly Func<object, bool> CanExecuteFunc;

  public DelegateCommand(Action<object> executeAction, Func<object, bool> canExecuteFunc)
  {
    ExecuteAction = executeAction;
    CanExecuteFunc = canExecuteFunc;
  }

  public bool CanExecute(object parameter)
  {
    return CanExecuteFunc == null || CanExecuteFunc(parameter);
  }

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

  public event EventHandler CanExecuteChanged;

  public void RaiseCanExecuteChanged()
  {
    CanExecuteChanged?.Invoke(this, EventArgs.Empty);
  }
}

The problem is, when I click on the Start Server button, the button's IsEnabled state doesn't change in the UI. Am I using the DelegateCommand improperly?

watkipet
  • 799
  • 4
  • 20

2 Answers2

3

The problem with your code is that your Command Properties use expression bodies, they are executed every time someone is getting the property, meaning everytime you ask for the StartServer command it will give you a new instance. You can check this yourself by putting a breakpoint on the getter and you see that it will be hit several times and always create a new instance of the delegate command.

In order to make your code work you have to use exactly one instance of each command, you can achieve this by using a backing field or a read-only property and set it in the conostructor:

  public MainWindowViewModel()
  {
     PropertyChanged += PropertyChangedHandler;
     StartServer = new DelegateCommand<object>(
        context =>
        {
           ServerIsRunning = true;
        }, e => !ServerIsRunning);

     StopServer = new DelegateCommand<object>(
        context =>    {
           ServerIsRunning = false;
        }, e => ServerIsRunning);
  }

  public DelegateCommand<object> StartServer
  { get; }

  public DelegateCommand<object> StopServer
  { get; }

The UI will only listen to the CanExecuteChanged event raised by the instance of the command it is bound to (via the Command={Binding ...}.

Your code could also be simplified as you could invoke the RaiseCanExecuteChanged directly instead of raising a PropertyChangedEvent and listening it to yourself:

  public bool ServerIsRunning
  {
     get => m_serverIsRunning;
     set
     {
        m_serverIsRunning = value;
        StartServer.RaiseCanExecuteChanged();
        StopServer.RaiseCanExecuteChanged();
     }
  }
huserben
  • 944
  • 1
  • 10
  • 17
2

Something like this?

public class ViewModel : INotifyPropertyChanged
{
    bool _isServerStarted;
    public ViewModel()
    {
        StartServer = new DelegateCommand(OnStartServerExecute, OnStartServerCanExecute);
        StopServer = new DelegateCommand(OnStopServerExecute, OnStopServerCanExecute);
    }

    void OnStartServerExecute()
    {
        IsServerStarted = true;
    }
    bool OnStartServerCanExecute()
    {
        return !IsServerStarted;
    }
    void OnStopServerExecute()
    {
        IsServerStarted = false;
    }
    bool OnStopServerCanExecute()
    {
        return IsServerStarted;
    }
    void RaiseCanExecuteChanged()
    {
        StartServer.RaiseCanExecuteChanged();
        StopServer.RaiseCanExecuteChanged();
    }
    public bool IsServerStarted 
    {
        get => _isServerStarted;
        set
        {
            _isServerStarted = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsServerStarted)));
            RaiseCanExecuteChanged();
        }
    }

    public DelegateCommand StartServer { get; }
    public DelegateCommand StopServer { get; }

    public event PropertyChangedEventHandler PropertyChanged;
}

You just should use CommandManager.InvalidateRequerySuggested() for updating button state.

Noisy88
  • 146
  • 1
  • 12