WPF MVVM TreeView选定项

40
这不应该这么难。WPF中的TreeView不允许您设置SelectedItem,因为该属性是只读的。我已经让TreeView填充了,甚至在绑定数据集合更改时更新了它。我只需要知道选定了哪个项目。我正在使用MVVM,因此没有可以引用TreeView的codebehind或变量。这是我找到的唯一解决方法,但它是一个明显的hack,它创建另一个在XAML中使用ElementName绑定的元素,以将自身设置为树视图的选定项,然后您必须将ViewModel绑定到它。有关此问题的其他 问题,但没有给出其他可行的解决方案。
我看到这个问题,但使用给定的答案会给我编译错误,原因是我无法将对Blend SDK System.Windows.Interactivity的引用添加到我的项目中。它说“未预加载未知错误系统.windows”,我还没有想出如何解决这个问题。
额外加分:为什么微软要将此元素的SelectedItem属性设置为只读?

有一种方法是使用附加属性,我会创建一个答案。 - Bas
4
您可以使用CodeBehind来处理此操作。TreeView中缺少可绑定的SelectedItem是一个众所周知的缺陷。我称其为缺陷,因为简单的解决方法可以提供解决方案。 - user1228
可能最简单的方法之一:https://dev59.com/MEjSa4cB1Zd3GeqPIMVs - JoanComasFdz
相关:https://dev59.com/m2kw5IYBdhLWcg3wdKQv - StayOnTarget
6个回答

56

您不需要直接处理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));
    }
}

4
我不明白这样做怎么能使它返回到树的上层。SelectedItem = this 是在TreeViewItem级别上发生的,它设置了TreeViewItem的属性,而不是树形视图。我仍然需要找到那个设置了该属性的TreeViewItem,这让我又回到了起点。 - Kyeotic
4
@Tyrsius:天啊,我简直可以抱怨那些从 SO 抄代码的人了...... 你从未见过一个触发 PropertyChanged 的属性吗?你应该能够在不使用那个显然你没有的扩展方法的情况下触发它。 - H.B.
5
是的,我看到过这个问题在大约十几个教程中被提及。很抱歉我没有理解你没有提供的上下文。 - Kyeotic
5
由于您依赖于静态属性(TViewModel.SelectedItem),所以当您在当前进程中有多个 TreeView 时,该解决方案将无法工作。 - bitbonk
2
“您实际上不需要直接处理SelectedItem属性。”我不同意,我希望TreeView作为项目的容器能够以与ListBox相同的方式公开其所选项目。话虽如此,+1是因为提供了正确的答案和最佳实践解决方法。 - darkpbj
显示剩余8条评论

16

在MVVM中,解决这个问题的一个非常不寻常但相当有效的方法是:

  1. 在与TreeView相同的视图上创建一个可见性折叠的ContentControl。给它取个合适的名字,并将其内容绑定到viewmodel中的某个SelectedSomething属性。这个ContentControl将“持有”所选的对象并处理它的绑定,使用OneWayToSource;
  2. 监听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;

12

您可以创建一个可绑定的附加属性,并具有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,并按以下方式进行绑定(local 是我命名的命名空间声明):

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

现在你可以将所选项绑定,并且在需要时也可以在视图模型中进行编程更改。当然,这是假设您在该特定属性上实现了INotifyPropertyChanged。


1
这看起来很有前途,但我无法正确实现它。Getter 在 treeview 构造时和请求属性时触发,但 setter 从未触发。 - Kyeotic
2
@Bas:每个TreeViewItem本身都是一个ItemsControl,你不能确定它的子项是否已经生成。你需要检查ItemContainerGenerator是否返回null,展开当前的TreeViewItem,等待其完成生成,然后递归尝试。在一个大树中,这可能需要很长时间。 - Fredrik Hedblad
1
是的,但无论你如何做,如果你想要能够设置所选项目,这都是必须的。 - Bas
12
这个解决方案存在内存泄漏问题。你的静态 behaviors 字典会永久地保留所有曾经被附加到内存中的 TreeView(包括它们可能拥有的所有项)。请注意,这些对象将永远不会被释放回收。为避免内存泄漏,请确保正确地管理你创建的对象。 - bitbonk

5

使用OneWayToSource绑定模式。这种方法行不通。请参见编辑。

编辑:根据此问题,看起来这是Microsoft的一个bug或“按设计行为”。尽管如此,有一些解决方法贴出来了。这些方法是否适用于您的TreeView?

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

Microsoft在2010年1月10日发布

我们无法在WPF中实现此功能,因为我们不能支持非DependencyProperties属性上的绑定。绑定的每个实例运行时状态都保存在BindingExpression中,我们将其存储在目标DependencyObject的EffectiveValueTable中。当目标属性不是DP或DP为只读时,没有地方可以存储BindingExpression。

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

感谢您的反馈。


这个不起作用,它会给出与任何其他模式相同的错误。SelectedItem属性是只读的,你根本不能设置它。 - Kyeotic
我已经为您调查了这个问题,看起来这是出于某种设计原因。请检查我编辑中的链接。 - Aphex
那么微软打算如何让我们从TreeView中获取选择? - Kyeotic
我会尝试在SO问题中发布的解决方法之一,或者在Microsoft Connect问题中发布的解决方法之一(那里发布了2个代码示例)。虽然现在我们至少知道为什么现在不可能实现这一点,以及Microsoft可能会在未来的版本中更改BindingExpressions存储方式,但为什么TreeView的SelectedItem首先是只读的,这是任何人都无法理解的。 - Aphex

2

我决定使用代码后台和视图模型代码的组合。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;
        }
    }

在ViewModel中,有一个TreeItemSelected对象,您可以在ViewModel中访问它。


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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接