为什么 WPF 按钮不总是触发其绑定的命令?

Why would a WPF Button not always fire its bound Command?

我有一个用户控件,它是使用 Prism 编写的应用程序的一部分。它有这个 'Back' 按钮:

<Button Content="Back"
        Command="{Binding BackCommand}"
        Padding="5 12 5 2" />

有时这个按钮不执行命令。视图模型提供了这个命令:

public class BackCommand : ICommand
{
    private readonly IInterviewController _controller;

    public BackCommand(IInterviewController controller)
    {
        _controller = controller;
    }

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

    public void Execute(object parameter)
    {
        _controller.MovePreviousTopic();
    }

    public event EventHandler CanExecuteChanged;
}

此外,用户控件还有一个 'Next' 按钮,可根据用户在第二个控件中的选择,让用户通过一系列分支问题。后退按钮应该将用户带到上一个问题。后退按钮通常执行绑定命令,所以我知道绑定有效。有时用户必须在命令触发之前单击后退按钮两次。我将 CanExecute 主体替换为始终 return true(如上所示)按钮仍然停止工作。

我在按钮上放置了临时事件处理程序,发现当命令执行失败时:

关于 SO 的每个问题我都发现有关命令未触发的问题,OP 遇到一个或另一个绑定命令的问题。由于我的命令是绑定的,所以我觉得没什么用。

编辑#2:我发现了问题的原因。组成应用程序的组件之一是试图控制焦点。要重现该问题,请将其粘贴到 window:

<StackPanel>
    <StackPanel Orientation="Horizontal">
        <TextBlock Text="Current count:"/>
        <TextBlock Text="{Binding Counter}"/>
    </StackPanel>
    <CheckBox x:Name="MyCheckbox" IsChecked="{Binding MaintainFocus}" LostFocus="MaintainFocus" >Maintain Focus</CheckBox>
    <Button Command="{Binding MyCommand}" Content="Test" Width="100" />
</StackPanel>

将其粘贴到 window 后面的代码中: public 部分 class 主要 Window : Window { 私有只读 ViewModel _vm;

    public MainWindow()
    {
        DataContext = _vm = new ViewModel();
        InitializeComponent();
    }

    private void MaintainFocus(object sender, RoutedEventArgs e)
    {
        if (_vm.MaintainFocus)
            Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Input,
                new Action(() => MyCheckbox.Focus()));
    }
}

public class ViewModel : INotifyPropertyChanged
{
    public ViewModel()
    {
        MyCommand = new TestCommand(this);
    }

    public ICommand MyCommand { get; set; }

    private int _counter;

    public int Counter
    {
        get { return _counter; }
        set
        {
            _counter = value;
            OnPropertyChanged();
        }
    }

    public bool MaintainFocus { get; set; }

    public event PropertyChangedEventHandler PropertyChanged;

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

public class TestCommand : ICommand
{
    private readonly ViewModel _viewModel;

    public TestCommand(ViewModel viewModel)
    {
        _viewModel = viewModel;
    }

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

    public void Execute(object parameter)
    {
        _viewModel.Counter++;
    }

    public event EventHandler CanExecuteChanged;
}

现在,如果选中保持焦点复选框,测试按钮将不会触发。

点击 Image 应该引发 Click 事件。但是,如果您设置 Padding 属性 并在实际 Image 之外单击,则不会引发 Click 事件。

您可以通过将 Border 元素放在具有 Transparent 背景的 Grid 中来解决此问题:

<Style TargetType="Button">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="Button">
                <Grid Background="Transparent">
                    <Border BorderThickness="0"
                            Background="Transparent"
                            Margin="{TemplateBinding Control.Padding}">
                        <Image Name="Image"
                               Source="{Binding NormalImage, RelativeSource={RelativeSource TemplatedParent}}" />
                    </Border>
                </Grid>
                <ControlTemplate.Triggers>
                    <Trigger Property="IsPressed"
                                 Value="True">
                        <Setter TargetName="Image"
                                Property="Source"
                                Value="{Binding PressedImage, RelativeSource={RelativeSource TemplatedParent}}" />
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

但请注意,当您提出问题时,您应该始终提供Minimal, Complete, and Verifiable example您的问题。

我的问题的解决方法是不允许按钮获得焦点:

<Button Command="{Binding MyCommand}"
        Focusable="False"
        Content="Test"
        Width="100" />

对于 WPF 的 ICommand,如果按钮在命令执行之前失去焦点似乎是个问题,但如果按钮从一开始就没有获得焦点则不是问题。

我很幸运,这些按钮的功能已通过控件上的 Tab 导航提供,因此我不需要重构代码设置焦点。由于 UI 设计者不想在按钮上显示键盘焦点,因此在我看来这是双赢。

我最初遇到的问题之一是如何调试此问题。我一直没有得到合适的答案,所以我将在下面总结我的过程:

  1. 不要使用断点 - 这些会干扰调试鼠标交互。
  2. Debug.WriteLine("<something meaningful>"); 添加到每个因鼠标单击而调用的 event/method。这告诉我哪些事件有效,哪些无效,以及顺序。它并没有真正帮助。
  3. 因为这是重新发送更改的结果,所以我将最近的更改一一注释掉,直到我确定导致问题的代码。这最终是对导致问题的组件的调用。
  4. 一旦我在我的更改中发现有问题的代码行,我就取消提交该行并单步执行该调用。这让我想到了使用 Dispatcher 设置焦点的代码。
  5. 这一行非常可疑,所以我构建了一个模仿代码的示例项目并验证了问题的根源。

也许这对以后的人有帮助。