WPF MVVM TreeView SelectedItem

Posted

技术标签:

【中文标题】WPF MVVM TreeView SelectedItem【英文标题】: 【发布时间】:2011-11-01 12:00:54 【问题描述】:

这不可能这么难。 WPF 中的 TreeView 不允许您设置 SelectedItem,表示该属性为 ReadOnly。我有 TreeView 填充,甚至在它的数据绑定集合更改时更新。

我只需要知道选择了什么项目。我正在使用 MVVM,因此没有代码隐藏或变量来引用树视图。 This is the only solution 我找到了,但这是一个明显的 hack,它在 XAML 中创建了另一个元素,该元素使用 ElementName 绑定将自己设置为 treeviews 选定项,然后您也必须绑定您的 Viewmodel。 Several 其他 questions 被问及此问题,但没有给出其他可行的解决方案。

我见过this question,但是使用给出的答案会给我编译错误,由于某种原因,我无法向我的项目添加对 blend sdk System.Windows.Interactivity 的引用。它说“unknown error system.windows has not been preloaded”,我还没有弄清楚如何解决这个问题。

对于奖励积分:Microsoft 为什么要将此元素的 SelectedItem 属性设为只读?

【问题讨论】:

有附加属性的方法,我会创建一个答案 或者您可以使用代码隐藏来为您处理。树视图中缺少可绑定的 SelectedItem 是一个众所周知的缺陷。我确实称其为缺陷,因为一个简单的解决方法可以提供解决方案。 最简单的方法之一可能是:***.com/questions/1238304/… 相关:***.com/questions/9880589/… 【参考方案1】:

您不应该真的需要直接处理 SelectedItem 属性,将IsSelected 绑定到您的视图模型上的属性并在那里跟踪所选项目。

草图:

<TreeView ItemsSource="Binding TreeData">
    <TreeView.ItemContainerStyle>
        <Style TargetType="x:Type TreeViewItem">
            <Setter Property="IsSelected" Value="Binding IsSelected" />
        </Style>
    </TreeView.ItemContainerStyle>
</TreeView>
public class TViewModel : INotifyPropertyChanged

    private static object _selectedItem = null;
    // This is public get-only here but you could implement a public setter which
    // also selects the item.
    // Also this should be moved to an instance property on a VM for the whole tree, 
    // otherwise there will be conflicts for more than one tree.
    public static object SelectedItem
    
        get  return _selectedItem; 
        private set
        
            if (_selectedItem != value)
            
                _selectedItem = value;
                OnSelectedItemChanged();
            
        
    

    static virtual void OnSelectedItemChanged()
    
        // Raise event / do other things
    

    private bool _isSelected;
    public bool IsSelected
    
        get  return _isSelected; 
        set
        
            if (_isSelected != value)
            
                _isSelected = value;
                OnPropertyChanged("IsSelected");
                if (_isSelected)
                
                    SelectedItem = this;
                
            
        
    

    public event PropertyChangedEventHandler PropertyChanged;
    protected virtual void OnPropertyChanged(string propertyName)
    
        var handler = this.PropertyChanged;
        if (handler != null)
            handler(this, new PropertyChangedEventArgs(propertyName));
    

【讨论】:

我不明白这将如何移回树上。 SelectedItem = this 发生在 TreeViewItem 级别,在 TreeViewItem 上设置属性,而不是树视图。我仍然需要找到设置了该属性的 TreeViewItem,而我又回到了第一格。 @Tyrsius: 伙计,我可以对人们从 SO 中复制粘贴代码大喊大叫……你从未见过引发PropertyChanged 的属性吗?您应该能够在不使用该扩展方法(您显然没有)的情况下提高它。 是的,我已经在大约十几个半教程中看到了它。很抱歉,我没有理解您没有提供的上下文。 不得不说,喜欢上面的代码 sn-p,读起来像梦一样,绝对喜欢简单。以上或 GalaSofts EventToCommand 将是我选择的处理已选择节点的方法。让我们看看哪个工作最快... = ) @H.B.因为您依赖于静态属性 (TViewModel.SelectedItem),所以当您当前进程中有多个 TreeView 时,此解决方案不起作用。【参考方案2】:

您可以创建一个可绑定且具有 getter 和 setter 的附加属性:

public class TreeViewHelper

    private static Dictionary<DependencyObject, TreeViewSelectedItemBehavior> behaviors = new Dictionary<DependencyObject, TreeViewSelectedItemBehavior>();

    public static object GetSelectedItem(DependencyObject obj)
    
        return (object)obj.GetValue(SelectedItemProperty);
    

    public static void SetSelectedItem(DependencyObject obj, object value)
    
        obj.SetValue(SelectedItemProperty, value);
    

    // Using a DependencyProperty as the backing store for SelectedItem.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty SelectedItemProperty =
        DependencyProperty.RegisterAttached("SelectedItem", typeof(object), typeof(TreeViewHelper), new UIPropertyMetadata(null, SelectedItemChanged));

    private static void SelectedItemChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
    
        if (!(obj is TreeView))
            return;

        if (!behaviors.ContainsKey(obj))
            behaviors.Add(obj, new TreeViewSelectedItemBehavior(obj as TreeView));

        TreeViewSelectedItemBehavior view = behaviors[obj];
        view.ChangeSelectedItem(e.NewValue);
    

    private class TreeViewSelectedItemBehavior
    
        TreeView view;
        public TreeViewSelectedItemBehavior(TreeView view)
        
            this.view = view;
            view.SelectedItemChanged += (sender, e) => SetSelectedItem(view, e.NewValue);
        

        internal void ChangeSelectedItem(object p)
        
            TreeViewItem item = (TreeViewItem)view.ItemContainerGenerator.ContainerFromItem(p);
            item.IsSelected = true;
        
    

将包含该类的命名空间声明添加到您的 XAML 并按如下方式绑定(本地是我命名命名空间声明的方式):

<TreeView ItemsSource="Binding Path=Root.Children"
          local:TreeViewHelper.SelectedItem="Binding Path=SelectedItem, Mode=TwoWay"/>

现在您可以绑定所选项目,并在您的视图模型中设置它以通过编程方式更改它,以应对这种需求。当然,这是假设您在该特定属性上实现了 INotifyPropertyChanged。

【讨论】:

这看起来很有希望,但我无法正确实现它。 getter 在树视图构造时触发,并在请求属性时触发,但 setter 永远不会触发。 @Bas:每个TreeViewItem 本身就是一个ItemsControl,您不能确定它的子代已经生成。您必须检查 ItemContainerGenerator 是否返回 null,展开当前的 TreeViewItem,等待它完成生成,然后递归重试。在一棵可能需要很多时间的大树上 是的,但是如果您希望能够设置所选项目,那么您可以通过任何方式进行操作。 此解决方案中存在 memoryleak。您的静态 behaviors 字典包含所有已永久附加到内存中的 TreeView(包括它们可能拥有的所有项目)。【参考方案3】:

以 MVVM 可接受的方式解决此问题的一种非常不寻常但非常有效的方法如下:

    在 TreeView 所在的同一 View 上创建可见性折叠 ContentControl。适当地命名它,并将其内容绑定到 viewmodel 中的某个 SelectedSomething 属性。此 ContentControl 将“持有”所选对象并处理它的绑定 OneWayToSource; 收听 TreeView 中的 SelectedItemChanged,并在代码隐藏中添加一个处理程序以将您的 ContentControl.Content 设置为新选择的项目。

XAML:

<ContentControl x:Name="SelectedItemHelper" Content="Binding SelectedObject, Mode=OneWayToSource" Visibility="Collapsed"/>
<TreeView ItemsSource="Binding SomeCollection"
    SelectedItemChanged="TreeView_SelectedItemChanged">

代码背后:

    private void TreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
    
        SelectedItemHelper.Content = e.NewValue;
    

视图模型:

    public object SelectedObject  // Class is not actually "object"
    
        get  return _selected_object; 
        set
        
            _selected_object = value;
            RaisePropertyChanged(() => SelectedObject);
            Console.WriteLine(SelectedObject);
        
    
    object _selected_object;

【讨论】:

【参考方案4】:

使用OneWayToSource绑定模式。这不起作用。见编辑。

编辑:根据this question,这似乎是微软的错误或“设计”行为;不过,发布了一些解决方法。这些对您的 TreeView 有用吗?

Microsoft Connect 问题:https://connect.microsoft.com/WPF/feedback/details/523865/read-only-dependency-properties-does-not-support-onewaytosource-bindings

Microsoft 于 2010 年 1 月 10 日下午 2:46 发布

我们今天无法在 WPF 中执行此操作,原因与我们无法支持的原因相同 对不是 DependencyProperties 的属性的绑定。运行时 绑定的每个实例状态保存在 BindingExpression 中,它 我们将目标 DependencyObject 存储在 EffectiveValueTable 中。 当目标属性不是 DP 或 DP 是只读的时,有 没有地方存放 BindingExpression。

我们有可能有一天会选择扩展绑定功能 针对这两种情况。我们经常被问到它们。在 换句话说,您的请求已经在我们的功能列表中 考虑在未来的版本中。

感谢您的反馈。

【讨论】:

这不起作用,它会给出与任何其他模式相同的错误。 SelectedItem 属性是只读的,您不能完全设置它。 我为你调查过,看起来它是出于某种原因设计的。检查我编辑中的链接。 那么微软打算如何让我们从树视图中获得选择? 我会尝试在 SO 问题中发布的解决方法之一或在 Microsoft Connect 问题上发布的解决方法之一(那里发布了 2 个代码示例)。虽然至少现在我们知道为什么现在不可能做到这一点,而且微软可能会在未来的版本中改变 BindingExpressions 的存储方式,但任何人都知道为什么 TreeView 的 SelectedItem 一开始是只读的。跨度> 【参考方案5】:

我决定使用隐藏代码和视图模型代码的组合。 xaml 是这样的:

<TreeView 
                    Name="tvCountries"
                ItemsSource="Binding Path=Countries"
                ItemTemplate="StaticResource ResourceKey=countryTemplate"   
                    SelectedValuePath="Name"
                    SelectedItemChanged="tvCountries_SelectedItemChanged">

后面的代码

private void tvCountries_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
    
        var vm = this.FindResource("vm") as ViewModels.CoiEditorViewModel;
        if (vm != null)
        
            var treeItem = sender as TreeView;
            vm.TreeItemSelected = treeItem.SelectedItem;
        
    

在视图模型中有一个 TreeItemSelected 对象,然后您可以在视图模型中访问它。

【讨论】:

【参考方案6】:

您始终可以创建一个使用 ICommand 的 DependencyProperty 并侦听 TreeView 上的 SelectedItemChanged 事件。这可能比绑定 IsSelected 容易一些,但我想你会因为其他原因最终绑定 IsSelected。如果您只想绑定 IsSelected,您可以随时让您的项目在 IsSelected 更改时发送消息。然后,您可以在程序中的任何位置收听这些消息。

【讨论】:

以上是关于WPF MVVM TreeView SelectedItem的主要内容,如果未能解决你的问题,请参考以下文章

WPF - MVVM Treeview获取所选项目[重复]

WPF - MVVM ???TreeView

WPF TreeView IsExpanded 绑定不上的问题

用户自定义TreeView控制双击冒泡事件WPF MVVM问题,怎么解决

WPF中TreeView的使用

WPF TreeView内部拖动项