如何让TreeViewItem的MultiTrigger仅在两个属性均变化时触发?
解决WPF MultiTrigger仅在两个属性同时变为true时触发的问题
你的问题很典型——WPF的MultiTrigger默认逻辑是只要所有条件都满足(不管是先后达到还是同时达到),就会触发EnterActions。所以当IsExpanded已经是true,再把IsSelected设为true时,两个条件都满足了,自然会触发动画。要实现“仅当两个属性同时从非true变为true”才触发,我们需要跟踪属性的状态变化,而不是只看当前状态。
下面提供两种可行的方案:
方案一:使用附加行为(Attached Behavior)(推荐,声明式+可复用)
附加行为是WPF中实现自定义交互逻辑的最佳实践之一,它可以保持XAML的整洁,同时逻辑可复用。
1. 创建附加行为类
using System.Collections.Generic; using System.Windows; using System.Windows.Controls; using System.Windows.Media.Animation; public static class ExpandSelectedBehavior { // 附加属性:是否启用该行为 public static readonly DependencyProperty EnableProperty = DependencyProperty.RegisterAttached("Enable", typeof(bool), typeof(ExpandSelectedBehavior), new PropertyMetadata(false, OnEnableChanged)); private static readonly Dictionary<TreeViewItem, (bool Expanded, bool Selected)> _previousStates = new(); public static bool GetEnable(DependencyObject obj) { return (bool)obj.GetValue(EnableProperty); } public static void SetEnable(DependencyObject obj, bool value) { obj.SetValue(EnableProperty, value); } private static void OnEnableChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is not TreeViewItem item) return; if ((bool)e.NewValue) { // 订阅属性变更事件 item.Loaded += Item_Loaded; item.Unloaded += Item_Unloaded; item.PropertyChanged += Item_PropertyChanged; } else { // 取消订阅 item.Loaded -= Item_Loaded; item.Unloaded -= Item_Unloaded; item.PropertyChanged -= Item_PropertyChanged; _previousStates.Remove(item); } } private static void Item_Loaded(object sender, RoutedEventArgs e) { var item = sender as TreeViewItem; if (item == null) return; // 初始化记录初始状态 _previousStates[item] = (item.IsExpanded, item.IsSelected); } private static void Item_Unloaded(object sender, RoutedEventArgs e) { var item = sender as TreeViewItem; if (item == null) return; _previousStates.Remove(item); } private static void Item_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) { var item = sender as TreeViewItem; if (item == null || !_previousStates.ContainsKey(item)) return; var prevState = _previousStates[item]; var currentExpanded = item.IsExpanded; var currentSelected = item.IsSelected; // 判断:当前两个属性都是true,且之前两个都是false(即同时变为true) if (currentExpanded && currentSelected && !prevState.Expanded && !prevState.Selected) { // 找到并启动动画 var itemsHost = item.Template.FindName("ItemsHost", item) as ItemsPresenter; if (itemsHost != null) { var storyboard = new Storyboard(); var scaleAnim = new DoubleAnimation { From = 0, To = 1, Duration = new Duration(TimeSpan.FromSeconds(0.2)), FillBehavior = FillBehavior.HoldEnd }; Storyboard.SetTargetName(scaleAnim, "ItemsHost"); Storyboard.SetTargetProperty(scaleAnim, new PropertyPath("(ItemsPresenter.LayoutTransform).(ScaleTransform.ScaleY)")); storyboard.Children.Add(scaleAnim); storyboard.Begin(itemsHost); // 同时设置ItemsHost可见 itemsHost.Visibility = Visibility.Visible; } } // 更新记录的状态 _previousStates[item] = (currentExpanded, currentSelected); } }
2. 在XAML中使用该行为
首先确保你的TreeViewItem模板里有ItemsHost这个ItemsPresenter,然后添加行为:
<TreeViewItem> <TreeViewItem.Style> <Style TargetType="TreeViewItem"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="TreeViewItem"> <!-- 你的模板内容,确保包含名为ItemsHost的ItemsPresenter --> <ItemsPresenter x:Name="ItemsHost" Visibility="Collapsed"> <ItemsPresenter.LayoutTransform> <ScaleTransform ScaleY="0"/> </ItemsPresenter.LayoutTransform> </ItemsPresenter> <!-- 其他模板元素 --> </ControlTemplate> </Setter.Value> </Setter> <!-- 启用附加行为 --> <Setter Property="local:ExpandSelectedBehavior.Enable" Value="True"/> </Style> </TreeViewItem.Style> </TreeViewItem>
方案二:代码后台直接处理(简单快速,适合单个控件)
如果你的场景只针对少数几个控件,不想写附加行为,可以直接在代码后台订阅PropertyChanged事件:
1. XAML中给TreeViewItem命名
<TreeViewItem x:Name="MyTreeViewItem"> <TreeViewItem.Template> <ControlTemplate TargetType="TreeViewItem"> <ItemsPresenter x:Name="ItemsHost" Visibility="Collapsed"> <ItemsPresenter.LayoutTransform> <ScaleTransform ScaleY="0"/> </ItemsPresenter.LayoutTransform> </ItemsPresenter> <!-- 其他模板元素 --> </ControlTemplate> </TreeViewItem.Template> </TreeViewItem>
2. 代码后台逻辑
private (bool Expanded, bool Selected) _prevState; public MainWindow() { InitializeComponent(); // 初始化状态 _prevState = (MyTreeViewItem.IsExpanded, MyTreeViewItem.IsSelected); // 订阅属性变更事件 MyTreeViewItem.PropertyChanged += MyTreeViewItem_PropertyChanged; } private void MyTreeViewItem_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) { if (e.PropertyName != nameof(TreeViewItem.IsExpanded) && e.PropertyName != nameof(TreeViewItem.IsSelected)) return; var currentExpanded = MyTreeViewItem.IsExpanded; var currentSelected = MyTreeViewItem.IsSelected; // 判断是否同时从false变为true if (currentExpanded && currentSelected && !_prevState.Expanded && !_prevState.Selected) { var itemsHost = MyTreeViewItem.Template.FindName("ItemsHost", MyTreeViewItem) as ItemsPresenter; if (itemsHost != null) { itemsHost.Visibility = Visibility.Visible; var storyboard = new Storyboard(); var scaleAnim = new DoubleAnimation(0, 1, TimeSpan.FromSeconds(0.2)) { FillBehavior = FillBehavior.HoldEnd }; Storyboard.SetTargetProperty(scaleAnim, new PropertyPath("(ItemsPresenter.LayoutTransform).(ScaleTransform.ScaleY)")); storyboard.Children.Add(scaleAnim); storyboard.Begin(itemsHost); } } // 更新状态 _prevState = (currentExpanded, currentSelected); }
关键逻辑说明
两种方案的核心都是跟踪属性的历史状态,只有当:
- 当前
IsExpanded和IsSelected都为true - 上一次记录的状态中,两个属性都为
false
才会触发动画和可见性设置,完美符合你“仅在两个属性都变为true时触发”的需求。
内容的提问来源于stack exchange,提问作者adyavanapalli
相关产品推荐
相关产品推荐

