为什么 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(如上所示)按钮仍然停止工作。
我在按钮上放置了临时事件处理程序,发现当命令执行失败时:
- 按钮已启用
- 绑定仍然设置
- 按钮上的 PreviewMouseDown 事件触发
- Click 事件没有触发(即使我删除了命令)
- 未调用命令的 Execute 方法。
关于 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 设计者不想在按钮上显示键盘焦点,因此在我看来这是双赢。
我最初遇到的问题之一是如何调试此问题。我一直没有得到合适的答案,所以我将在下面总结我的过程:
- 不要使用断点 - 这些会干扰调试鼠标交互。
- 将
Debug.WriteLine("<something meaningful>");
添加到每个因鼠标单击而调用的 event/method。这告诉我哪些事件有效,哪些无效,以及顺序。它并没有真正帮助。
- 因为这是重新发送更改的结果,所以我将最近的更改一一注释掉,直到我确定导致问题的代码。这最终是对导致问题的组件的调用。
- 一旦我在我的更改中发现有问题的代码行,我就取消提交该行并单步执行该调用。这让我想到了使用 Dispatcher 设置焦点的代码。
- 这一行非常可疑,所以我构建了一个模仿代码的示例项目并验证了问题的根源。
也许这对以后的人有帮助。
我有一个用户控件,它是使用 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(如上所示)按钮仍然停止工作。
我在按钮上放置了临时事件处理程序,发现当命令执行失败时:
- 按钮已启用
- 绑定仍然设置
- 按钮上的 PreviewMouseDown 事件触发
- Click 事件没有触发(即使我删除了命令)
- 未调用命令的 Execute 方法。
关于 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 设计者不想在按钮上显示键盘焦点,因此在我看来这是双赢。
我最初遇到的问题之一是如何调试此问题。我一直没有得到合适的答案,所以我将在下面总结我的过程:
- 不要使用断点 - 这些会干扰调试鼠标交互。
- 将
Debug.WriteLine("<something meaningful>");
添加到每个因鼠标单击而调用的 event/method。这告诉我哪些事件有效,哪些无效,以及顺序。它并没有真正帮助。 - 因为这是重新发送更改的结果,所以我将最近的更改一一注释掉,直到我确定导致问题的代码。这最终是对导致问题的组件的调用。
- 一旦我在我的更改中发现有问题的代码行,我就取消提交该行并单步执行该调用。这让我想到了使用 Dispatcher 设置焦点的代码。
- 这一行非常可疑,所以我构建了一个模仿代码的示例项目并验证了问题的根源。
也许这对以后的人有帮助。