6

Continuing down the path to MVVM, I have come to button commands. After quite a bit of trial and error I finally have a working example of a Button_Click command using ICommand.

My issue is that now I have a generic event made, I can't get which button was clicked to apply some logic to it. In my example, I have not used anything where I could get the Sender information. Usually something like this below using RoutedEventArgs:

Button button = (Button)sender;

So this is what I have so far.

The ICommand class:

public class CommandHandler : ICommand
{
    private Action _action;
    private bool _canExecute;
    public CommandHandler(Action action, bool canExecute)
    {
        _action = action;
        _canExecute = canExecute;
    }

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

    public event EventHandler CanExecuteChanged;

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

And the code to make the action:

private ICommand _clickCommand;
public ICommand ClickCommand => _clickCommand ?? (_clickCommand = new CommandHandler(MyAction, _canExecute));

public ViewModelBase()
{
    _canExecute = true;            
}

public void MyAction()
{
    //Apply logic here to differentiate each button
}

And the XAML,

<Button Command="{Binding ClickCommand}" Style="{StaticResource RedButtonStyle}">MyButton</Button>

How would I go about identifying which button is being clicked when binding the same command to other buttons?

KyloRen
  • 2,691
  • 5
  • 29
  • 59

5 Answers5

5

You probably shouldn't, but if you want to, you can use CommandParameter=""

You should just use 2 ICommands though.

XAML:

<Button Command="{Binding ClickCommandEvent}" CommandParameter="Jack"/>

ViewModel:

public RelayCommand ClickCommandEvent { get; set; }

public SomeClass()
{
    ClickCommandEvent = new RelayCommand(ClickExecute);
}

public void ClickExecute(object param)
{
    System.Diagnostics.Debug.WriteLine($"Clicked: {param as string}");

    string name = param as string;
    if (name == "Jack")
        HighFive();
}

and your RelayCommand class would be this boiler plate:

public class RelayCommand : ICommand
{
    #region Fields
    readonly Action<object> _execute;
    readonly Predicate<object> _canExecute;

    #endregion

    #region Constructors
    public RelayCommand(Action<object> execute) : this(execute, null) { }

    public RelayCommand(Action<object> execute, Predicate<object> canExecute)
    {
        if (execute == null)
            throw new ArgumentNullException("execute");

        _execute = execute;
        _canExecute = canExecute;
    }
    #endregion

    #region ICommand Members
    public bool CanExecute(object parameter)
    {
        return _canExecute == null ? true : _canExecute(parameter);
    }

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

    public void Execute(object parameter)
    {
        _execute(parameter);
    }
    #endregion
}
Xander Luciano
  • 3,753
  • 7
  • 32
  • 53
  • 1
    Thanks for the answer, for learning sake I am going to try this, but I will go with separate commands in the project. Cheers – KyloRen Mar 04 '17 at 02:32
  • 1
    Love the enthusiasm! Let me know if you run into any trouble! You'll love MVVM once you get a hang of it though. Have you done an `INotifyPropertyChanged` yet? – Xander Luciano Mar 04 '17 at 02:36
  • 1
    I love this stuff. Yes , I have implemented `INotifyPropertyChanged`, I started with `DataGrid` and all its idosyncrosis, can update, add new records etc, but still can't figure out how to delete a record with `Context.Menu`. So I am leaving that for now and started on button commands. The one I am dreading the most is `TreeView` and MVVM. – KyloRen Mar 04 '17 at 02:46
  • I loved how easy it was for me to pull one section out of of my Main application and make it it's own View! I didn't change a single binding, I just passed each view an instance of the same data context and it just worked! This was my result: http://i.imgur.com/UHGYi1T.gifv – Xander Luciano Mar 04 '17 at 02:50
  • That looks like a fun project! Probably should not chat here, if you want to have a bit of a chat, lets open a private room. – KyloRen Mar 04 '17 at 02:52
3

This will give you the clicked Button :

<Button Command="{Binding ClickCommand}" 
        CommandParameter="{Binding RelativeSource={RelativeSource Self}}"/>
AnjumSKhan
  • 9,647
  • 1
  • 26
  • 38
1

You're not supposed to bind all buttons to the same command. Just make a different command for each button.

Thomas Levesque
  • 286,951
  • 70
  • 623
  • 758
  • OK I see, so in that respect, it is similar to your named handlers in non MVVM scenario, but this way the code is decoupled from the view. Thanks – KyloRen Mar 04 '17 at 02:23
1

This is where WPF relies on too much code for simple actions. I mean 30 lines of code for handling a button click is a little ridiculous but somehow we talk ourselves into it being a "good" thing because it follws a pattern.

Excessive code is excessive code. There is no reason why this should be this complicated.

MattE
  • 1,044
  • 1
  • 14
  • 34
  • 1
    it's not 30 lines of code for every button instance though ... just once in your library. How else would you handle making a button disabled due to some state of your application data? Tightly coupled spaghetti code is tightly coupled spaghetti code. – Peregrine Nov 19 '18 at 07:42
0

Sometimes this is unavoidable to direct several commands to the same function to reuse the same code. I found this approach the easiest way to identify which control has been clicked. In the Xaml I set ElementName for both buttons like this:

      <Button Grid.Row="0"
              Grid.Column="0"
              Content="Read File"                  
              Command="{Binding ReadProButtonClick}"
              CommandParameter="{Binding ElementName=ReadFile}"/>          

      <Button Grid.Row="0"                  
              Grid.Column="1"
              Content="Batch Convert"                 
              Command="{Binding ReadProButtonClick}"
              CommandParameter="{Binding ElementName=BatchMode}"/>

And then in the ViewModel, I get the name of the buttons like this:

 Private void ReadProButton(Object context) {           
        Controls.Button btnClicked = CType(context, System.Windows.Controls.Button)           
        processName = btnClicked.Name
        ...
 }

So, processName will be BatchMode or ReadFile depends on what the user clicked on.

Ehsan
  • 767
  • 7
  • 18