WPF:数据绑定 TreeViewItem -> 当先前选择另一个具有长 运行 操作的节点时,不需要选择悬停节点
WPF: Databound TreeViewItem -> Unwanted selection of hovered node when previously selected another node with a long running operation
简而言之:
TreeView 绑定到 ViewModel 的 ObservableCollection。单击具有持久操作的节点并在操作持续时将鼠标移动到其他节点上,导致 select 意外悬停节点(WPF 会这样做,用户没有鼠标单击)。 -> 为什么,我怎样才能阻止它?
长版:
我有一个 TreeView,其中的 Hierarchical-DataTemplate 绑定到自定义 ViewModel 的 ObservableCollection。构建节点结构以及 运行 大多数命令都可以正常工作。
以下是使用 EventToCommandBehaviour 绑定到 ViewModel 中的命令的 DataTemplate-Items 事件:
- PreviewMouseRightButtonDown
- PreviewMouseRightButtonUp
- PreviewMouseLeftButtonDown
- PreviewMouseLeftButtonUp
- 预览鼠标移动
- 掉落
所有命令都已成功触发。这是 XAML 数据模板:
<DataTemplate DataType="{x:Type model:TreeNodeBaseViewModel}">
<StackPanel Orientation="Horizontal" Height="16">
<StackPanel Orientation="Horizontal" Height="16" ToolTip="{Binding ToolTip}" IsHitTestVisible="True" Background="Transparent">
<Grid Height="16" Width="16" Margin="0,0,5,0" Visibility="{Binding ShowIcon, Converter={StaticResource BooleanToVisibilityConverter}, UpdateSourceTrigger=PropertyChanged}">
<Viewbox Width="{Binding PackIcon.Width}" Height="{Binding PackIcon.Height}">
<iconPacks:PackIconSimpleIcons Foreground="{Binding PackIcon.Color}" Rotation="{Binding PackIcon.Vector_Angle}" HorizontalAlignment="Center" VerticalAlignment="Center" Kind="{Binding PackIcon.Value, Mode=OneWay}" />
</Viewbox>
</Grid>
<Label Content="{Binding DisplayName}" AllowDrop="{Binding IsDropAllowed}" Foreground="{Binding Color}" Padding="0"/>
<i:Interaction.Behaviors>
<beh:EventToCommandBehavior Command="{Binding PreviewMouseRightButtonDownCommand}" Event="PreviewMouseRightButtonDown" PassArguments="True" />
<beh:EventToCommandBehavior Command="{Binding PreviewMouseRightButtonUpCommand}" Event="PreviewMouseRightButtonUp" PassArguments="True" />
<beh:EventToCommandBehavior Command="{Binding PreviewMouseLeftButtonDownCommand}" Event="PreviewMouseLeftButtonDown" PassArguments="True" />
<beh:EventToCommandBehavior Command="{Binding PreviewMouseLeftButtonUpCommand}" Event="PreviewMouseLeftButtonUp" PassArguments="True" />
<beh:EventToCommandBehavior Command="{Binding PreviewMouseMoveCommand}" Event="PreviewMouseMove" PassArguments="True" />
<beh:EventToCommandBehavior Command="{Binding DropCommand}" Event="Drop" PassArguments="True" />
</i:Interaction.Behaviors>
</StackPanel>
<Button Background="Transparent" BorderThickness="0" Margin="5,0,0,0" Visibility="{Binding IsNodePropertyButtonVisible, Converter={StaticResource BooleanToVisibilityConverter}}">
<iconPacks:PackIconMaterial Width="10" Height="10" Foreground="LightGray" HorizontalAlignment="Center" VerticalAlignment="Center" Kind="InformationOutline" />
<i:Interaction.Behaviors>
<beh:EventToCommandBehavior Command="{Binding PropertiesCommand}" Event="Click" PassArguments="True" />
</i:Interaction.Behaviors>
</Button>
</StackPanel>
</DataTemplate>
以下 TreeViewItem 属性也绑定到 ViewModel:
- 已启用
- AllowDrop
- 展开
- 是Select编辑
- 标签
这是 XAML:
<Style x:Key="MenuItemTemplateItemContainerStyle" TargetType="{x:Type TreeViewItem}">
<Setter Property="ContextMenu" Value="{DynamicResource MenuItemContextMenu}"/>
<Setter Property="IsEnabled" Value="{Binding IsEnabled, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"/>
<Setter Property="AllowDrop" Value="{Binding IsDropAllowed, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"/>
<Setter Property="IsExpanded" Value="{Binding IsExpanded, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"/>
<Setter Property="IsSelected" Value="{Binding IsSelected, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"/>
<Setter Property="Tag" Value="{Binding}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Path=IsVisible}" Value="False">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
<Trigger Property="beh:TreeNodeMouseOver.IsMouseDirectlyOverItem" Value="True">
<Setter Property="Background" Value="AliceBlue" />
</Trigger>
</Style.Triggers>
</Style>
我的问题是:
当我左键单击触发较长 运行 操作的节点时,会发生两件事:
- ViewModel 中的 IsSelected 并不总是设置,尽管它绑定到 TreeViewItem
的 IsSelected
- 如果我将鼠标移动到另一个节点上,而另一个节点的操作仍然是 运行,那么 selection 将更改为 select 悬停节点,而无需我做任何事情。为什么会这样?
我试过的一些东西:
- 如果尚未设置,则Select手动设置 – 工作正常,但不应该必须完成。
- 通过调试和查看 IsSelected 更改时的调用堆栈,我发现 WPF 似乎在另一个节点获得焦点时触发 Select 和 ChangeSelection –但为什么? ......我该如何抑制它?
- 尝试 TreeViewItem.GotFocus 事件以防止意外的 selection
这是无意中设置 IsSelected 时调用堆栈的一部分。 WPF 似乎在调用 OnGotFocus 后触发 Select 和 ChangeSelection:
[Native to Managed Transition]
[Managed to Native Transition]
PresentationFramework.dll!MS.Internal.Data.PropertyPathWorker.SetValue(object item, object value) Unknown
PresentationFramework.dll!MS.Internal.Data.ClrBindingWorker.UpdateValue(object value) Unknown
PresentationFramework.dll!System.Windows.Data.BindingExpression.UpdateSource(object value = false) Unknown
PresentationFramework.dll!System.Windows.Data.BindingExpressionBase.UpdateValue() Unknown
PresentationFramework.dll!System.Windows.Data.BindingExpression.UpdateOverride() Unknown
PresentationFramework.dll!System.Windows.Data.BindingExpressionBase.Update() Unknown
PresentationFramework.dll!System.Windows.Data.BindingExpressionBase.ProcessDirty() Unknown
PresentationFramework.dll!System.Windows.Data.BindingExpressionBase.Dirty() Unknown
PresentationFramework.dll!System.Windows.Data.BindingExpressionBase.SetValue(System.Windows.DependencyObject d, System.Windows.DependencyProperty dp, object value) Unknown
WindowsBase.dll!System.Windows.DependencyObject.SetValueCommon(System.Windows.DependencyProperty dp = {System.Windows.DependencyProperty}, object value = false, System.Windows.PropertyMetadata metadata = {System.Windows.FrameworkPropertyMetadata}, bool coerceWithDeferredReference = false, bool coerceWithCurrentValue = false, System.Windows.OperationType operationType = Unknown, bool isInternal) Unknown
WindowsBase.dll!System.Windows.DependencyObject.SetValue(System.Windows.DependencyProperty dp, object value) Unknown
PresentationFramework.dll!System.Windows.Controls.TreeView.ChangeSelection(object data = {HOSEC.UI.Tree.ViewModels.TreeNodeMainNodeViewModel}, System.Windows.Controls.TreeViewItem container = {System.Windows.Controls.TreeViewItem}, bool selected = true) Unknown
PresentationFramework.dll!System.Windows.Controls.TreeViewItem.Select(bool selected = true) Unknown
PresentationFramework.dll!System.Windows.Controls.TreeViewItem.OnGotFocus(System.Windows.RoutedEventArgs e = {System.Windows.RoutedEventArgs}) Unknown
PresentationCore.dll!System.Windows.UIElement.IsFocused_Changed(System.Windows.DependencyObject d, System.Windows.DependencyPropertyChangedEventArgs e) Unknown
WindowsBase.dll!System.Windows.DependencyObject.OnPropertyChanged(System.Windows.DependencyPropertyChangedEventArgs e) Unknown
PresentationFramework.dll!System.Windows.FrameworkElement.OnPropertyChanged(System.Windows.DependencyPropertyChangedEventArgs e) Unknown
WindowsBase.dll!System.Windows.DependencyObject.NotifyPropertyChange(System.Windows.DependencyPropertyChangedEventArgs args) Unknown
WindowsBase.dll!System.Windows.DependencyObject.UpdateEffectiveValue(System.Windows.EntryIndex entryIndex, System.Windows.DependencyProperty dp = {System.Windows.DependencyProperty}, System.Windows.PropertyMetadata metadata, System.Windows.EffectiveValueEntry oldEntry, ref System.Windows.EffectiveValueEntry newEntry = {System.Windows.EffectiveValueEntry}, bool coerceWithDeferredReference, bool coerceWithCurrentValue, System.Windows.OperationType operationType) Unknown
WindowsBase.dll!System.Windows.DependencyObject.SetValueCommon(System.Windows.DependencyProperty dp, object value, System.Windows.PropertyMetadata metadata, bool coerceWithDeferredReference, bool coerceWithCurrentValue, System.Windows.OperationType operationType, bool isInternal) Unknown
WindowsBase.dll!System.Windows.DependencyObject.SetValue(System.Windows.DependencyPropertyKey key, object value) Unknown
PresentationCore.dll!System.Windows.Input.FocusManager.OnFocusedElementChanged(System.Windows.DependencyObject d, System.Windows.DependencyPropertyChangedEventArgs e) Unknown
...
好的,这绝对是奇怪的 WPF 行为,但我现在修复了它:
首先,我停用了除 PreviewMouseLeftButtonDownCommand 之外的所有命令,并发现 TreeView 在长期操作期间失去了焦点。当出于某种原因尝试重新获得焦点时,WPF 将焦点设置到当前悬停的节点 - 这是可重现的并且不需要用户交互。
我所做的是向 MainViewModel 引入一个布尔值 DisableAutoSelection,然后如果 DisableAutoSelection 为真,则阻止更新 TreeNodeViewModel 中的 IsSelected 值。
此外,我将 TreeNodeViewModel 的 PreviewMouseLeftButtonDownCommand 中的代码移至异步任务,并在 运行 任务之前设置 DisableAutoSelection = true。等待任务完成时,我设置 DisableAutoSelection = false.
瞧,它现在可以工作了,随机选择节点的时间结束了:-)
简而言之:
TreeView 绑定到 ViewModel 的 ObservableCollection。单击具有持久操作的节点并在操作持续时将鼠标移动到其他节点上,导致 select 意外悬停节点(WPF 会这样做,用户没有鼠标单击)。 -> 为什么,我怎样才能阻止它?
长版:
我有一个 TreeView,其中的 Hierarchical-DataTemplate 绑定到自定义 ViewModel 的 ObservableCollection。构建节点结构以及 运行 大多数命令都可以正常工作。
以下是使用 EventToCommandBehaviour 绑定到 ViewModel 中的命令的 DataTemplate-Items 事件:
- PreviewMouseRightButtonDown
- PreviewMouseRightButtonUp
- PreviewMouseLeftButtonDown
- PreviewMouseLeftButtonUp
- 预览鼠标移动
- 掉落
所有命令都已成功触发。这是 XAML 数据模板:
<DataTemplate DataType="{x:Type model:TreeNodeBaseViewModel}">
<StackPanel Orientation="Horizontal" Height="16">
<StackPanel Orientation="Horizontal" Height="16" ToolTip="{Binding ToolTip}" IsHitTestVisible="True" Background="Transparent">
<Grid Height="16" Width="16" Margin="0,0,5,0" Visibility="{Binding ShowIcon, Converter={StaticResource BooleanToVisibilityConverter}, UpdateSourceTrigger=PropertyChanged}">
<Viewbox Width="{Binding PackIcon.Width}" Height="{Binding PackIcon.Height}">
<iconPacks:PackIconSimpleIcons Foreground="{Binding PackIcon.Color}" Rotation="{Binding PackIcon.Vector_Angle}" HorizontalAlignment="Center" VerticalAlignment="Center" Kind="{Binding PackIcon.Value, Mode=OneWay}" />
</Viewbox>
</Grid>
<Label Content="{Binding DisplayName}" AllowDrop="{Binding IsDropAllowed}" Foreground="{Binding Color}" Padding="0"/>
<i:Interaction.Behaviors>
<beh:EventToCommandBehavior Command="{Binding PreviewMouseRightButtonDownCommand}" Event="PreviewMouseRightButtonDown" PassArguments="True" />
<beh:EventToCommandBehavior Command="{Binding PreviewMouseRightButtonUpCommand}" Event="PreviewMouseRightButtonUp" PassArguments="True" />
<beh:EventToCommandBehavior Command="{Binding PreviewMouseLeftButtonDownCommand}" Event="PreviewMouseLeftButtonDown" PassArguments="True" />
<beh:EventToCommandBehavior Command="{Binding PreviewMouseLeftButtonUpCommand}" Event="PreviewMouseLeftButtonUp" PassArguments="True" />
<beh:EventToCommandBehavior Command="{Binding PreviewMouseMoveCommand}" Event="PreviewMouseMove" PassArguments="True" />
<beh:EventToCommandBehavior Command="{Binding DropCommand}" Event="Drop" PassArguments="True" />
</i:Interaction.Behaviors>
</StackPanel>
<Button Background="Transparent" BorderThickness="0" Margin="5,0,0,0" Visibility="{Binding IsNodePropertyButtonVisible, Converter={StaticResource BooleanToVisibilityConverter}}">
<iconPacks:PackIconMaterial Width="10" Height="10" Foreground="LightGray" HorizontalAlignment="Center" VerticalAlignment="Center" Kind="InformationOutline" />
<i:Interaction.Behaviors>
<beh:EventToCommandBehavior Command="{Binding PropertiesCommand}" Event="Click" PassArguments="True" />
</i:Interaction.Behaviors>
</Button>
</StackPanel>
</DataTemplate>
以下 TreeViewItem 属性也绑定到 ViewModel:
- 已启用
- AllowDrop
- 展开
- 是Select编辑
- 标签
这是 XAML:
<Style x:Key="MenuItemTemplateItemContainerStyle" TargetType="{x:Type TreeViewItem}">
<Setter Property="ContextMenu" Value="{DynamicResource MenuItemContextMenu}"/>
<Setter Property="IsEnabled" Value="{Binding IsEnabled, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"/>
<Setter Property="AllowDrop" Value="{Binding IsDropAllowed, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"/>
<Setter Property="IsExpanded" Value="{Binding IsExpanded, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"/>
<Setter Property="IsSelected" Value="{Binding IsSelected, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"/>
<Setter Property="Tag" Value="{Binding}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Path=IsVisible}" Value="False">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
<Trigger Property="beh:TreeNodeMouseOver.IsMouseDirectlyOverItem" Value="True">
<Setter Property="Background" Value="AliceBlue" />
</Trigger>
</Style.Triggers>
</Style>
我的问题是:
当我左键单击触发较长 运行 操作的节点时,会发生两件事:
- ViewModel 中的 IsSelected 并不总是设置,尽管它绑定到 TreeViewItem 的 IsSelected
- 如果我将鼠标移动到另一个节点上,而另一个节点的操作仍然是 运行,那么 selection 将更改为 select 悬停节点,而无需我做任何事情。为什么会这样?
我试过的一些东西:
- 如果尚未设置,则Select手动设置 – 工作正常,但不应该必须完成。
- 通过调试和查看 IsSelected 更改时的调用堆栈,我发现 WPF 似乎在另一个节点获得焦点时触发 Select 和 ChangeSelection –但为什么? ......我该如何抑制它?
- 尝试 TreeViewItem.GotFocus 事件以防止意外的 selection
这是无意中设置 IsSelected 时调用堆栈的一部分。 WPF 似乎在调用 OnGotFocus 后触发 Select 和 ChangeSelection:
[Native to Managed Transition]
[Managed to Native Transition]
PresentationFramework.dll!MS.Internal.Data.PropertyPathWorker.SetValue(object item, object value) Unknown
PresentationFramework.dll!MS.Internal.Data.ClrBindingWorker.UpdateValue(object value) Unknown
PresentationFramework.dll!System.Windows.Data.BindingExpression.UpdateSource(object value = false) Unknown
PresentationFramework.dll!System.Windows.Data.BindingExpressionBase.UpdateValue() Unknown
PresentationFramework.dll!System.Windows.Data.BindingExpression.UpdateOverride() Unknown
PresentationFramework.dll!System.Windows.Data.BindingExpressionBase.Update() Unknown
PresentationFramework.dll!System.Windows.Data.BindingExpressionBase.ProcessDirty() Unknown
PresentationFramework.dll!System.Windows.Data.BindingExpressionBase.Dirty() Unknown
PresentationFramework.dll!System.Windows.Data.BindingExpressionBase.SetValue(System.Windows.DependencyObject d, System.Windows.DependencyProperty dp, object value) Unknown
WindowsBase.dll!System.Windows.DependencyObject.SetValueCommon(System.Windows.DependencyProperty dp = {System.Windows.DependencyProperty}, object value = false, System.Windows.PropertyMetadata metadata = {System.Windows.FrameworkPropertyMetadata}, bool coerceWithDeferredReference = false, bool coerceWithCurrentValue = false, System.Windows.OperationType operationType = Unknown, bool isInternal) Unknown
WindowsBase.dll!System.Windows.DependencyObject.SetValue(System.Windows.DependencyProperty dp, object value) Unknown
PresentationFramework.dll!System.Windows.Controls.TreeView.ChangeSelection(object data = {HOSEC.UI.Tree.ViewModels.TreeNodeMainNodeViewModel}, System.Windows.Controls.TreeViewItem container = {System.Windows.Controls.TreeViewItem}, bool selected = true) Unknown
PresentationFramework.dll!System.Windows.Controls.TreeViewItem.Select(bool selected = true) Unknown
PresentationFramework.dll!System.Windows.Controls.TreeViewItem.OnGotFocus(System.Windows.RoutedEventArgs e = {System.Windows.RoutedEventArgs}) Unknown
PresentationCore.dll!System.Windows.UIElement.IsFocused_Changed(System.Windows.DependencyObject d, System.Windows.DependencyPropertyChangedEventArgs e) Unknown
WindowsBase.dll!System.Windows.DependencyObject.OnPropertyChanged(System.Windows.DependencyPropertyChangedEventArgs e) Unknown
PresentationFramework.dll!System.Windows.FrameworkElement.OnPropertyChanged(System.Windows.DependencyPropertyChangedEventArgs e) Unknown
WindowsBase.dll!System.Windows.DependencyObject.NotifyPropertyChange(System.Windows.DependencyPropertyChangedEventArgs args) Unknown
WindowsBase.dll!System.Windows.DependencyObject.UpdateEffectiveValue(System.Windows.EntryIndex entryIndex, System.Windows.DependencyProperty dp = {System.Windows.DependencyProperty}, System.Windows.PropertyMetadata metadata, System.Windows.EffectiveValueEntry oldEntry, ref System.Windows.EffectiveValueEntry newEntry = {System.Windows.EffectiveValueEntry}, bool coerceWithDeferredReference, bool coerceWithCurrentValue, System.Windows.OperationType operationType) Unknown
WindowsBase.dll!System.Windows.DependencyObject.SetValueCommon(System.Windows.DependencyProperty dp, object value, System.Windows.PropertyMetadata metadata, bool coerceWithDeferredReference, bool coerceWithCurrentValue, System.Windows.OperationType operationType, bool isInternal) Unknown
WindowsBase.dll!System.Windows.DependencyObject.SetValue(System.Windows.DependencyPropertyKey key, object value) Unknown
PresentationCore.dll!System.Windows.Input.FocusManager.OnFocusedElementChanged(System.Windows.DependencyObject d, System.Windows.DependencyPropertyChangedEventArgs e) Unknown
...
好的,这绝对是奇怪的 WPF 行为,但我现在修复了它:
首先,我停用了除 PreviewMouseLeftButtonDownCommand 之外的所有命令,并发现 TreeView 在长期操作期间失去了焦点。当出于某种原因尝试重新获得焦点时,WPF 将焦点设置到当前悬停的节点 - 这是可重现的并且不需要用户交互。
我所做的是向 MainViewModel 引入一个布尔值 DisableAutoSelection,然后如果 DisableAutoSelection 为真,则阻止更新 TreeNodeViewModel 中的 IsSelected 值。
此外,我将 TreeNodeViewModel 的 PreviewMouseLeftButtonDownCommand 中的代码移至异步任务,并在 运行 任务之前设置 DisableAutoSelection = true。等待任务完成时,我设置 DisableAutoSelection = false.
瞧,它现在可以工作了,随机选择节点的时间结束了:-)