1
votes

I'm having trouble getting the CanExecute method of my command to work property. I've bound a command to a button that is inside of a DataGrid. I've bound the CommandParameter to the button's DataContext, which happens to be a record for a row in the DataGrid.

What I expect to happen is for the CanExecute method to be re-evaluated when the CommandParameter binding changes, which in this case would be the row's DataContext property being set. But instead of evaluating the CanExecute method against the row data, it looks like the CanExecute method is being evaluated before the row gets its DataContext and it is never re-evaluated after the DataContext has been updated.

Can you tell me how to get the CanExecute method of my command to be evaluated against each row's DataContext?

I've created a sample application to demonstrate my problem. Here's the code:

The code-behind for the MainWindow.xaml

public partial class MainWindow : Window
{
    public ObservableCollection<LogRecord> Records { get; private set; }
    public ICommand SignOutCommand { get; private set; }
    public MainWindow()
    {
        InitializeComponent();
        DataContext = this;
        Records = new ObservableCollection<LogRecord>();
        SignOutCommand = new SignOutCommand();
        CreateDemoData();
    }
    private void CreateDemoData()
    {
        for (int i = 0; i < 5; i++)
        {
            Records.Add(new LogRecord());
        }
    }
}

public class LogRecord : INotifyPropertyChanged
{
    private DateTime _EntryTime;
    public DateTime EntryTime
    {
        get { return _EntryTime; }
        set
        {
            if (_EntryTime == value) return;
            _EntryTime = value;
            RaisePropertyChanged("EntryTime");
        }
    }

    private DateTime? _ExitTime;
    public DateTime? ExitTime
    {
        get { return _ExitTime; }
        set 
        {
            if (_ExitTime == value) return;
            _ExitTime = value;
            RaisePropertyChanged("ExitTime");
        }
    }

    public LogRecord()
    {
        EntryTime = DateTime.Now;
    }

    #region Implementation of INotifyPropertyChanged

    public event PropertyChangedEventHandler PropertyChanged;

    public void RaisePropertyChanged(string propertyName)
    {
        if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }

    #endregion
}

public class SignOutCommand : ICommand
{
    #region Implementation of ICommand

    public void Execute(object parameter)
    {
        var record = parameter as LogRecord;
        if (record == null) return;
        record.ExitTime = DateTime.Now;
    }

    public bool CanExecute(object parameter)
    {
        var record = parameter as LogRecord;
        return record != null && !record.ExitTime.HasValue;
    }

    public event EventHandler CanExecuteChanged;

    #endregion
}

The XAML for MainWindow.xaml

<Window x:Class="Command_Spike.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="MainWindow"
    Width="525"
    Height="350">
<DataGrid ItemsSource="{Binding Path=Records}" IsReadOnly="True" AutoGenerateColumns="False">
    <DataGrid.Columns>
        <DataGridTextColumn Header="Entry Time" Binding="{Binding Path=EntryTime}" />
        <DataGridTextColumn Header="Exit Time" Binding="{Binding Path=ExitTime}" />
        <DataGridTemplateColumn>
            <DataGridTemplateColumn.CellTemplate>
                <DataTemplate>
                    <Button Command="{Binding RelativeSource={RelativeSource Mode=FindAncestor,
                                                                             AncestorType=Window},
                                              Path=DataContext.SignOutCommand}"
                            CommandParameter="{Binding}"
                            Content="Sign Out" />
                </DataTemplate>
            </DataGridTemplateColumn.CellTemplate>
        </DataGridTemplateColumn>
    </DataGrid.Columns>
</DataGrid>

If you load up the example code, you can see that all of the Sign Out buttons are disabled because in each row, the CanExecute method is receiving null as the parameter instead of the row-specific data that I want. If this sample were working properly, all of the buttons would be enabled initially and would only disable after a value in the Exit Time column was set.

1

1 Answers

2
votes

You are not setting up the custom command correctly. In your current example, you don't need to create a command manually that implements ICommand, you simply need to create a Routed or RoutedUI command and wire up the appropriate handlers. Remove your SignOutCommand object, then modify your Window code like the following:

public partial class MainWindow: Window
{
    public ObservableCollection<LogRecord> Records { get; private set; }
    public static RoutedUICommand SignOutCommand { get; private set; }

    public MainWindow()
    {
        InitializeComponent();
        DataContext = this;
        Records = new ObservableCollection<LogRecord>();
        CreateDemoData();

        SignOutCommand = new RoutedUICommand();
        CommandBinding cb = new CommandBinding(SignOutCommand, OnSignOut, OnCanSignOut);
        this.CommandBindings.Add(cb);
    }


    private void CreateDemoData()
    {
        for (int i = 0; i < 5; i++)
        {
            Records.Add(new LogRecord());
        }
    }

    private void OnCanSignOut(object sender, CanExecuteRoutedEventArgs e)
    {
        var record = e.Parameter as LogRecord;
        e.CanExecute = record != null && !record.ExitTime.HasValue;

    }

    private void OnSignOut(object sender, ExecutedRoutedEventArgs e)
    {
        var record = e.Parameter as LogRecord;
        if (record == null) return;
        record.ExitTime = DateTime.Now;
    }
}

then, modify your DataTemplate like follows (basically, just remove the DataContext from the Path):

<dg:DataGridTemplateColumn>
  <dg:DataGridTemplateColumn.CellTemplate>
    <DataTemplate>
       <Button Command="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Window}, Path=SignOutCommand}" CommandParameter="{Binding}" Content="Sign Out" />
    </DataTemplate>
  </dg:DataGridTemplateColumn.CellTemplate>
</dg:DataGridTemplateColumn>

Using this approach, your sign out button will be correctly enabled when the DataContext is set.