在WPF Treeview中绑定数据到SelectedItem

262

我该如何检索WPF-treeview中所选的项?我想在XAML中完成此操作,因为我想绑定它。

你可能认为应该使用SelectedItem,但显然这是只读的,因此无法使用。

这就是我想要做的:

<TreeView ItemsSource="{Binding Path=Model.Clusters}" 
            ItemTemplate="{StaticResource ClusterTemplate}"
            SelectedItem="{Binding Path=Model.SelectedCluster}" />

我想将SelectedItem绑定到我的模型上的属性。

但是这会导致错误:

'SelectedItem'属性是只读的,无法从标记中设置。

编辑: 好的,我解决了这个问题的方法:

<TreeView
          ItemsSource="{Binding Path=Model.Clusters}" 
          ItemTemplate="{StaticResource HoofdCLusterTemplate}"
          SelectedItemChanged="TreeView_OnSelectedItemChanged" />

我在 XAML 的代码后台文件中:

private void TreeView_OnSelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
    Model.SelectedCluster = (Cluster)e.NewValue;
}

61
这太糟糕了。我才意识到这一点。我来这里是希望找到一个好办法,但现在看起来我只是个傻瓜。这是我第一次感到难过,因为我不是个傻瓜。 - Andrei Rînea
10
这真的很糟糕,弄乱了绑定的概念。 - Delta
希望这能帮助有需要的人,在ICommand上绑定树视图项选择更改的回调函数 http://jacobaloysious.wordpress.com/2012/02/19/mvvm-binding-treeview-item-changed-to-icommand/ - jacob aloysious
10
就绑定和MVVM而言,代码后台并没有被“禁止”,相反,代码后台应该支持视图。在我看来,从我所见过的所有其他解决方案中,代码后台是一个更好的选择,因为它仍然在处理将视图绑定到视图模型。唯一的负面影响是,如果你的团队有一个只在XAML中工作的设计师,则代码后台可能会被破坏/忽略。这是为了实现只需10秒钟的解决方案而付出的小代价。 - nrjohnstone
1
我认为近十几年后,微软仍然没有解决这个可怕的开发者体验是悲哀和惊人的。这真的难以置信。 - Ian Ray
显示剩余2条评论
21个回答

277

我知道这个问题已经有一个被接受的答案了,但是我尝试着解决这个问题并且做出了一些成果。这个解决方案使用了类似Delta的方法,但是不需要对TreeView进行子类化处理:

public class BindableSelectedItemBehavior : Behavior<TreeView>
{
    #region SelectedItem Property

    public object SelectedItem
    {
        get { return (object)GetValue(SelectedItemProperty); }
        set { SetValue(SelectedItemProperty, value); }
    }

    public static readonly DependencyProperty SelectedItemProperty =
        DependencyProperty.Register("SelectedItem", typeof(object), typeof(BindableSelectedItemBehavior), new UIPropertyMetadata(null, OnSelectedItemChanged));

    private static void OnSelectedItemChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
        var item = e.NewValue as TreeViewItem;
        if (item != null)
        {
            item.SetValue(TreeViewItem.IsSelectedProperty, true);
        }
    }

    #endregion

    protected override void OnAttached()
    {
        base.OnAttached();

        this.AssociatedObject.SelectedItemChanged += OnTreeViewSelectedItemChanged;
    }

    protected override void OnDetaching()
    {
        base.OnDetaching();

        if (this.AssociatedObject != null)
        {
            this.AssociatedObject.SelectedItemChanged -= OnTreeViewSelectedItemChanged;
        }
    }

    private void OnTreeViewSelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
    {
        this.SelectedItem = e.NewValue;
    }
}

然后,您可以将此用于XAML中:

<TreeView>
    <e:Interaction.Behaviors>
        <behaviours:BindableSelectedItemBehavior SelectedItem="{Binding SelectedItem, Mode=TwoWay}" />
    </e:Interaction.Behaviors>
</TreeView>

希望这能帮到某些人!


6
正如 Brent 指出的那样,我也需要在绑定中添加 Mode=TwoWay。由于我不是“Blender”,所以并不熟悉来自 System.Windows.Interactivity 的 Behavior<> 类。该程序集是 Expression Blend 的一部分。对于那些不想购买/安装试用版以获取此程序集的人,您可以下载 BlendSDK,其中包括 System.Windows.Interactivity。BlendSDK 3 适用于 3.5 …… 我认为 BlendSDK 4 适用于 4.0。 注意:这只允许您获取已选择的项目,而不能设置已选择的项目。 - Mike Rowley
5
你可以使用FrameworkPropertyMetadata替代UIPropertyMetadata,代码如下:FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnSelectedItemChanged)。 - Filimindji
4
这是解决问题的一种方法:https://dev59.com/yGgu5IYBdhLWcg3wzKAc#18700099。 - bitbonk
7
@Pascal 这是 xmlns:e="http://schemas.microsoft.com/expression/2010/interactivity" - Steve Greatrex
7
仅当您的SelectedItem是TreeViewItem时,该方法才有效。但在MVVM世界中,这是不被允许的...为了使它适用于SelectedItem中的任何数据,我们需要获取该数据所在的容器,这有些棘手,因为每个TreeView的父节点都有自己的容器。 - JobaDiniz
显示剩余11条评论

46

存在这个属性:TreeView.SelectedItem

但它是只读的,因此您无法通过绑定进行赋值,只能检索它。


1
我接受这个答案,因为我在这里找到了这个链接,它让我得出了我的答案: http://msdn.microsoft.com/en-us/library/ms788714.aspx - Natrium
3
当用户选择一个项(又称为OneWayToSource)时,我可以让TreeView.SelectedItem影响模型上的属性吗? - Shimmy Weitzhandler

40

好的,我找到了一个解决方案。这将移动混乱的内容,以便MVVM正常工作。

首先添加此类:

public class ExtendedTreeView : TreeView
{
    public ExtendedTreeView()
        : base()
    {
        this.SelectedItemChanged += new RoutedPropertyChangedEventHandler<object>(___ICH);
    }

    void ___ICH(object sender, RoutedPropertyChangedEventArgs<object> e)
    {
        if (SelectedItem != null)
        {
            SetValue(SelectedItem_Property, SelectedItem);
        }
    }

    public object SelectedItem_
    {
        get { return (object)GetValue(SelectedItem_Property); }
        set { SetValue(SelectedItem_Property, value); }
    }
    public static readonly DependencyProperty SelectedItem_Property = DependencyProperty.Register("SelectedItem_", typeof(object), typeof(ExtendedTreeView), new UIPropertyMetadata(null));
}

并将此添加到您的 XAML 中:

 <local:ExtendedTreeView ItemsSource="{Binding Items}" SelectedItem_="{Binding Item, Mode=TwoWay}">
 .....
 </local:ExtendedTreeView>

4
到目前为止,这是唯一有效的方法。我非常喜欢这个解决方案。 - Rachael
4
不知道为什么,但它对我没有起作用:( 我成功地从树中获取了所选项目,但无法通过外部更改所选项目。 - Erez
将依赖属性设置为“BindsTwoWayByDefault”会更加整洁,这样您就不需要在XAML中指定“TwoWay”。 - Stephen Holt
这是最佳的方法。它不使用交互式引用,也不使用代码后台,也不像某些行为那样存在内存泄漏问题。谢谢。 - Alexandru Dicu
1
如前所述,该解决方案不适用于双向绑定。如果您在视图模型中设置了值,则更改不会传播到TreeView。 - Richard Moore

34

它比 OP 所期望的回答了更多...但我希望它能至少对某些人有所帮助。

如果你想在SelectedItem改变时执行一个ICommand,你可以将其与事件绑定,并且在ViewModel中不再需要使用SelectedItem属性。

操作步骤如下:

1- 添加对System.Windows.Interactivity的引用

xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"

2- 将命令绑定到事件SelectedItemChanged

<TreeView x:Name="myTreeView" Margin="1"
            ItemsSource="{Binding Directories}">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="SelectedItemChanged">
            <i:InvokeCommandAction Command="{Binding SomeCommand}"
                                   CommandParameter="
                                            {Binding ElementName=myTreeView
                                             ,Path=SelectedItem}"/>
        </i:EventTrigger>
    </i:Interaction.Triggers>
    <TreeView.ItemTemplate>
           <!-- ... -->
    </TreeView.ItemTemplate>
</TreeView>

3
可通过 NuGet 安装 System.Windows.Interactivity 引用:https://www.nuget.org/packages/System.Windows.Interactivity.WPF/ - Junle Li
我已经试了好几个小时来解决这个问题,我已经实现了但是我的命令无法工作,请问您能帮我吗? - Alfie
3
微软在2018年底推出了XAML Behaviors for WPF,它可以代替System.Windows.Interactivity。我已经使用过它(在.NET Core项目中进行了尝试)。为了设置它,只需添加Microsoft.Xaml.Behaviors.Wpf nuget包,并将命名空间更改为xmlns:i="http://schemas.microsoft.com/xaml/behaviors"即可。欲获取更多信息,请参阅博客 - rychlmoj

21

可以通过绑定和GalaSoft MVVM Light库的EventToCommand来以更优雅的方式实现。在你的VM中添加一个命令,当选定项更改时调用该命令,并初始化命令以执行必要的操作。在这个例子中,我使用了RelayCommand并只设置了SelectedCluster属性。

public class ViewModel
{
    public ViewModel()
    {
        SelectedClusterChanged = new RelayCommand<Cluster>( c => SelectedCluster = c );
    }

    public RelayCommand<Cluster> SelectedClusterChanged { get; private set; } 

    public Cluster SelectedCluster { get; private set; }
}

然后在你的XAML中添加EventToCommand行为。使用Blend非常容易实现。

<TreeView
      x:Name="lstClusters"
      ItemsSource="{Binding Path=Model.Clusters}" 
      ItemTemplate="{StaticResource HoofdCLusterTemplate}">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="SelectedItemChanged">
            <GalaSoft_MvvmLight_Command:EventToCommand Command="{Binding SelectedClusterChanged}" CommandParameter="{Binding ElementName=lstClusters,Path=SelectedValue}"/>
        </i:EventTrigger>
    </i:Interaction.Triggers>
</TreeView>

1
这是一个不错的解决方案,特别是如果您已经在使用MvvmLight工具包。但它并不能解决设置选定节点并使树形视图更新选择的问题。 - keft

12

太过复杂了... 推荐使用Caliburn Micro(http://caliburnmicro.codeplex.com/)

视图:

<TreeView Micro:Message.Attach="[Event SelectedItemChanged] = [Action SetSelectedItem($this.SelectedItem)]" />

视图模型:

public void SetSelectedItem(YourNodeViewModel item) {}; 

7
是的...那设置 TreeView 上的 SelectedItem 的部分在哪里? - mnn
Caliburn很不错,优雅大方。对于嵌套层次结构非常容易使用。 - Purusartha

9

我在寻找与原作者相同的答案时遇到了这个页面,证明了总有多种方法来解决问题。对我而言,解决方案甚至比目前提供的答案更简单,所以我想我也可以为此做出贡献。

绑定的动机是为了保持良好的MVVM模式。ViewModel的可能使用方式是具有名称为“CurrentThingy”的属性,在其他地方,某些其他事物上的DataContext绑定到“CurrentThingy”。

与其通过额外的步骤(例如:自定义行为、第三方控件)来支持从TreeView到我的Model的良好绑定,然后从其他东西到我的Model,我的解决方案是使用简单的元素绑定将其他东西绑定到TreeView.SelectedItem,而不是将其他东西绑定到我的ViewModel,从而跳过所需的额外工作。

XAML:

<TreeView x:Name="myTreeView" ItemsSource="{Binding MyThingyCollection}">
.... stuff
</TreeView>

<!-- then.. somewhere else where I want to see the currently selected TreeView item: -->

<local:MyThingyDetailsView 
       DataContext="{Binding ElementName=myTreeView, Path=SelectedItem}" />

当然,这对于阅读当前选定的项目非常有用,但不是我所需要的设置它。


1
什么是local:MyThingyDetailsView?我知道local:MyThingyDetailsView保存了所选项目,但你的视图模型如何获得这些信息?这看起来是一个不错、干净的方法,但我需要更多的信息… - Bob Horn
local:MyThingyDetailsView 是一个简单的 UserControl,它包含了关于一个“thingy”实例的详细视图的 XAML。它作为内容嵌入到另一个视图的中间,使用元素绑定,这个视图的 DataContext 是当前选定的树形视图项。 - Wes

6
你也可以使用TreeViewItem.IsSelected属性。

我认为这可能是正确的答案。但我想看到一个示例或最佳实践建议,关于如何将Items的IsSelected属性传递给TreeView。 - anhoppe
这是如何完成的?我在任何地方都没有"TreeViewItem",我有<TreeView.ItemTemplate><HierarchicalDataTemplate>... - Daniel Möller

4

我的需求是基于PRISM-MVVM的解决方案,需要使用TreeView,并且绑定的对象类型为Collection<>,因此需要使用HierarchicalDataTemplate。默认的BindableSelectedItemBehavior无法识别子TreeViewItem。为了在这种情况下使其工作。

public class BindableSelectedItemBehavior : Behavior<TreeView>
{
    #region SelectedItem Property

    public object SelectedItem
    {
        get { return (object)GetValue(SelectedItemProperty); }
        set { SetValue(SelectedItemProperty, value); }
    }

    public static readonly DependencyProperty SelectedItemProperty =
        DependencyProperty.Register("SelectedItem", typeof(object), typeof(BindableSelectedItemBehavior), new UIPropertyMetadata(null, OnSelectedItemChanged));

    private static void OnSelectedItemChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
        var behavior = sender as BindableSelectedItemBehavior;
        if (behavior == null) return;
        var tree = behavior.AssociatedObject;
        if (tree == null) return;
        if (e.NewValue == null)
            foreach (var item in tree.Items.OfType<TreeViewItem>())
                item.SetValue(TreeViewItem.IsSelectedProperty, false);
        var treeViewItem = e.NewValue as TreeViewItem;
        if (treeViewItem != null)
            treeViewItem.SetValue(TreeViewItem.IsSelectedProperty, true);
        else
        {
            var itemsHostProperty = tree.GetType().GetProperty("ItemsHost", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
            if (itemsHostProperty == null) return;
            var itemsHost = itemsHostProperty.GetValue(tree, null) as Panel;
            if (itemsHost == null) return;
            foreach (var item in itemsHost.Children.OfType<TreeViewItem>())
            {
                if (WalkTreeViewItem(item, e.NewValue)) 
                    break;
            }
        }
    }

    public static bool WalkTreeViewItem(TreeViewItem treeViewItem, object selectedValue)
    {
        if (treeViewItem.DataContext == selectedValue)
        {
            treeViewItem.SetValue(TreeViewItem.IsSelectedProperty, true);
            treeViewItem.Focus();
            return true;
        }
        var itemsHostProperty = treeViewItem.GetType().GetProperty("ItemsHost", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
        if (itemsHostProperty == null) return false;
        var itemsHost = itemsHostProperty.GetValue(treeViewItem, null) as Panel;
        if (itemsHost == null) return false;
        foreach (var item in itemsHost.Children.OfType<TreeViewItem>())
        {
            if (WalkTreeViewItem(item, selectedValue))
                break;
        }
        return false;
    }
    #endregion

    protected override void OnAttached()
    {
        base.OnAttached();
        this.AssociatedObject.SelectedItemChanged += OnTreeViewSelectedItemChanged;
    }

    protected override void OnDetaching()
    {
        base.OnDetaching();
        if (this.AssociatedObject != null)
        {
            this.AssociatedObject.SelectedItemChanged -= OnTreeViewSelectedItemChanged;
        }
    }

    private void OnTreeViewSelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
    {
        this.SelectedItem = e.NewValue;
    }
}

这使得可以遍历所有元素,无论它们的级别如何。

3

我建议对Steve Greatrex提供的行为进行补充。他的行为不能反映源中的更改,因为它可能不是TreeViewItems集合。 因此,问题在于找到树中数据上下文为源中selectedValue的TreeViewItem。 TreeView有一个受保护的属性称为“ItemsHost”,它保存TreeViewItem集合。我们可以通过反射获取它并遍历树搜索所选项目。

private static void OnSelectedItemChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
        var behavior = sender as BindableSelectedItemBehaviour;

        if (behavior == null) return;

        var tree = behavior.AssociatedObject;

        if (tree == null) return;

        if (e.NewValue == null) 
            foreach (var item in tree.Items.OfType<TreeViewItem>())
                item.SetValue(TreeViewItem.IsSelectedProperty, false);

        var treeViewItem = e.NewValue as TreeViewItem; 
        if (treeViewItem != null)
        {
            treeViewItem.SetValue(TreeViewItem.IsSelectedProperty, true);
        }
        else
        {
            var itemsHostProperty = tree.GetType().GetProperty("ItemsHost", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);

            if (itemsHostProperty == null) return;

            var itemsHost = itemsHostProperty.GetValue(tree, null) as Panel;

            if (itemsHost == null) return;

            foreach (var item in itemsHost.Children.OfType<TreeViewItem>())
                if (WalkTreeViewItem(item, e.NewValue)) break;
        }
    }

    public static bool WalkTreeViewItem(TreeViewItem treeViewItem, object selectedValue) {
        if (treeViewItem.DataContext == selectedValue)
        {
            treeViewItem.SetValue(TreeViewItem.IsSelectedProperty, true);
            treeViewItem.Focus();
            return true;
        }

        foreach (var item in treeViewItem.Items.OfType<TreeViewItem>())
            if (WalkTreeViewItem(item, selectedValue)) return true;

        return false;
    }

这样的行为适用于双向绑定。或者,可以将ItemsHost获取移动到Behavior的OnAttached方法中,以节省每次绑定更新时使用反射的开销。


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