Dữ liệu liên kết với chọnItem trong WPF Treeview


240

Làm cách nào tôi có thể truy xuất mục được chọn trong WPF-treeview? Tôi muốn làm điều này trong XAML, vì tôi muốn liên kết nó.

Bạn có thể nghĩ rằng nó là SelectedItemnhư vậy nhưng dường như không tồn tại là chỉ đọc và do đó không thể sử dụng.

Đây là những gì tôi muốn làm:

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

Tôi muốn liên kết SelectedItemtài sản trên Model của tôi.

Nhưng điều này cho tôi lỗi:

Thuộc tính 'ChọnItem' chỉ đọc và không thể được đặt từ đánh dấu.

Chỉnh sửa: Ok, đây là cách mà tôi đã giải quyết điều này:

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

và trong codebehindfile của xaml của tôi:

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

50
Người đàn ông này hút. Nó chỉ đánh tôi quá. Tôi đến đây với hy vọng tìm thấy rằng có một cách tốt và tôi chỉ là một thằng ngốc. Đây là lần đầu tiên tôi buồn vì mình không phải là một thằng ngốc ..
Andrei Rînea

6
điều này thực sự hấp dẫn và làm rối tung khái niệm ràng buộc
Delta

Hy vọng điều này có thể giúp ai đó liên kết với một mục xem cây đã thay đổi, gọi lại trên Icommand jacobaloysious.wordpress.com/2012/02/19/ mẹo
jacob aloysious 19/212

9
Về mặt ràng buộc và MVVM, mã phía sau không bị "cấm", thay vào đó, mã phía sau sẽ hỗ trợ cho chế độ xem. Theo ý kiến ​​của tôi từ tất cả các giải pháp khác mà tôi đã thấy, mã phía sau là một lựa chọn tốt hơn rất nhiều vì nó vẫn xử lý "ràng buộc" chế độ xem với chế độ xem. Điểm trừ duy nhất là nếu bạn có một nhóm với một nhà thiết kế chỉ hoạt động trong XAML, mã phía sau có thể bị hỏng / bị bỏ qua. Đó là một cái giá nhỏ để trả cho một giải pháp mất 10 giây để thực hiện.
nrjohnstone

Một trong những giải pháp đơn giản nhất có lẽ là: stackoverflow.com/questions/1238304/ từ
JoanComasFdz

Câu trả lời:


240

Tôi nhận ra điều này đã có một câu trả lời được chấp nhận, nhưng tôi kết hợp nó lại để giải quyết vấn đề. Nó sử dụng một ý tưởng tương tự như giải pháp của Delta, nhưng không cần phải phân lớp 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;
    }
}

Sau đó, bạn có thể sử dụng điều này trong XAML của mình dưới dạng:

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

Hy vọng nó sẽ giúp được ai đó!


5
Như Brent đã chỉ ra, tôi cũng cần thêm Mode = TwoWay vào liên kết. Tôi không phải là "Máy xay sinh tố" nên không quen thuộc với lớp Hành vi <> từ System.Windows.Interactivity. Việc lắp ráp là một phần của Expression Blend. Đối với những người không muốn mua / cài đặt bản dùng thử để có bản lắp ráp này, bạn có thể tải xuống BlendSDK bao gồm System.Windows.Interactivity. BlendSDK 3 cho 3.5 ... Tôi nghĩ đó là BlendSDK 4 cho 4.0. Lưu ý: Điều này chỉ cho phép bạn lấy mục nào được chọn, không cho phép bạn đặt mục đã chọn
Mike Rowley

4
Bạn cũng có thể thay thế UIPropertyMetadata bằng FrameworkPropertyMetadata (null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnSelectedItemChanged));
Filimindji

3
Đây sẽ là một cách tiếp cận để giải quyết vấn đề: stackoverflow.com/a/18700099/4227
bitbonk

2
@Lukas chính xác như được hiển thị trong đoạn mã XAML ở trên. Chỉ cần thay thế {Binding SelectedItem, Mode=TwoWay}bằng{Binding MyViewModelField, Mode=TwoWay}
Steve Greatrex

4
@Pascal đóxmlns:e="http://schemas.microsoft.com/expression/2010/interactivity"
Steve Greatrex

46

Thuộc tính này tồn tại: TreeView.SelectedItem

Nhưng nó chỉ đọc, vì vậy bạn không thể gán nó thông qua một ràng buộc, chỉ lấy nó


Tôi chấp nhận câu trả lời này, bởi vì tôi đã tìm thấy liên kết này, nó cho phép câu trả lời của riêng tôi: msdn.microsoft.com/en-us/l Library / ms788714.aspx
Natrium

1
Vì vậy, tôi có thể có điều này TreeView.SelectedItemảnh hưởng đến một thuộc tính trên mô hình khi người dùng chọn một mục (aka OneWayToSource) không?
Shimmy Weitzhandler

43

Trả lời với các thuộc tính kèm theo và không có phụ thuộc bên ngoài, nếu cần phát sinh!

Bạn có thể tạo một thuộc tính đính kèm có thể ràng buộc và có getter và 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;
        }
    }
}

Thêm khai báo không gian tên chứa lớp đó vào XAML của bạn và liên kết như sau (cục bộ là cách tôi đặt tên cho khai báo không gian tên):

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

    </TreeView>

Bây giờ bạn có thể liên kết mục đã chọn và cũng đặt nó trong mô hình chế độ xem của mình để thay đổi mục đó theo chương trình, nếu yêu cầu đó xuất hiện. Tất nhiên, điều này là giả sử rằng bạn triển khai INotifyPropertyChanged trên thuộc tính cụ thể đó.


4
+1, câu trả lời tốt nhất trong chủ đề này imho. Không phụ thuộc vào System.Windows.Interactivity và cho phép liên kết hai chiều (cài đặt theo chương trình trong môi trường MVVM). Hoàn hảo.
Chris Ray

5
Một vấn đề với cách tiếp cận này là hành vi sẽ chỉ bắt đầu hoạt động khi mục được chọn đã được đặt một lần thông qua liên kết (tức là từ ViewModel). Nếu giá trị ban đầu trong VM là null, thì ràng buộc sẽ không cập nhật giá trị DP và hành vi sẽ không được kích hoạt. Bạn có thể khắc phục điều này bằng cách sử dụng một mục được chọn mặc định khác (ví dụ: một mục không hợp lệ).
Đánh dấu

6
@Mark: Đơn giản chỉ cần sử dụng đối tượng mới () thay vì null ở trên khi khởi tạo UIPropertyMetadata của thuộc tính đính kèm. Vấn đề sẽ biến mất sau đó ...
barnacleboy

2
Tôi cho rằng việc chuyển sang TreeViewItem không thành công đối với tôi bởi vì tôi đang sử dụng HVELicalDataTemplate được áp dụng từ tài nguyên bởi kiểu dữ liệu. Nhưng nếu bạn loại bỏ ChangeSelectedItem, thì liên kết với một khung nhìn và lấy ra mục hoạt động tốt.
Casey Sebben

1
Tôi cũng gặp vấn đề với việc chuyển sang TreeViewItem. Vào thời điểm đó, ItemContainerGenerator chỉ chứa các tham chiếu đến các mục gốc, nhưng tôi cần nó để có thể nhận được các mục không phải root. Nếu bạn chuyển một tham chiếu đến một tham chiếu, diễn viên thất bại và trả về null. Không chắc chắn làm thế nào điều này có thể được sửa chữa?
Bob Tway

39

Vâng, tôi tìm thấy một giải pháp. Nó di chuyển mớ hỗn độn, để MVVM hoạt động.

Đầu tiên thêm lớp này:

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));
}

và thêm phần này vào xaml của bạn:

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

3
Đây là điều DUY NHẤT đã đến gần để làm việc cho tôi cho đến nay. Tôi thực sự thích giải pháp này.
Rachael

1
Không biết tại sao nhưng nó không hoạt động với tôi :( Tôi đã thành công khi lấy vật phẩm đã chọn từ cây nhưng không phải ngược lại - để thay đổi vật phẩm đã chọn từ bên ngoài cây.
Erez

Sẽ gọn gàng hơn một chút để đặt thuộc tính phụ thuộc là BindsTwoWayByDefault, sau đó bạn không cần chỉ định TwoWay trong XAML
Stephen Holt

Đây là cách tiếp cận tốt nhất. Nó không sử dụng tham chiếu Tương tác, nó không sử dụng mã phía sau, nó không bị rò rỉ bộ nhớ như một số hành vi. Cảm ơn bạn.
Alexandru Dicu

Như đã đề cập, giải pháp này không hoạt động với ràng buộc 2 chiều. Nếu bạn đặt giá trị trong chế độ xem, thay đổi sẽ không lan truyền đến TreeView.
Richard Moore

25

Nó trả lời nhiều hơn một chút so với OP đang mong đợi ... Nhưng tôi hy vọng nó có thể giúp đỡ ít nhất một ai đó.

Nếu bạn muốn thực thi ICommandbất cứ khi nào SelectedItemthay đổi, bạn có thể liên kết một lệnh trên một sự kiện và việc sử dụng một SelectedItemthuộc tính trong ViewModelkhông cần thiết nữa.

Làm như vậy:

1- Thêm tham chiếu đến System.Windows.Interactivity

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

2- Ràng buộc lệnh cho sự kiện 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
Tham chiếu System.Windows.Interactivitycó thể được cài đặt từ NuGet: nuget.org/packages/System.Windows.Interactivity.WPF
Junle Li

Tôi đã cố gắng giải quyết vấn đề này trong nhiều giờ, tôi đã thực hiện điều này nhưng lệnh của tôi không hoạt động, xin vui lòng giúp tôi được không?
Alfie

1
Microsoft đã giới thiệu các Hành vi XAML cho WPF vào cuối năm 2018. Nó có thể được sử dụng thay cho System.Windows.Interactivity. Nó đã làm việc với tôi (đã thử với dự án .NET Core). Để thiết lập mọi thứ, chỉ cần thêm gói nuget Microsoft.Xaml.Behaviors.Wpf , thay đổi không gian tên thành xmlns:i="http://schemas.microsoft.com/xaml/behaviors". Để biết thêm thông tin - vui lòng xem blog
rychlmoj

19

Điều này có thể được thực hiện theo cách 'đẹp hơn' chỉ bằng cách sử dụng ràng buộc và EventToCommand của thư viện GalaSoft MVVM Light. Trong VM của bạn, thêm một lệnh sẽ được gọi khi thay đổi mục đã chọn và khởi tạo lệnh để thực hiện bất kỳ hành động nào là cần thiết. Trong ví dụ này, tôi đã sử dụng RelayCommand và sẽ chỉ đặt thuộc tính chọnClCluster.

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; }
}

Sau đó thêm hành vi EventToCommand trong xaml của bạn. Điều này thực sự dễ dàng bằng cách sử dụng hỗn hợp.

<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>

Đây là một giải pháp tốt đặc biệt nếu bạn đang sử dụng bộ công cụ MvvmLight. Tuy nhiên, nó không giải quyết được vấn đề thiết lập nút đã chọn và cập nhật treeview lựa chọn.
keft 5/10/2015

12

Tất cả đều phức tạp ... Đi với Caliburn Micro (http://caliburnmicro.codeplex.com/)

Lượt xem:

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

ViewModel:

public void SetSelectedItem(YourNodeViewModel item) {}; 

5
Có ... và đâu là phần thiết lập ChọnItem trên TreeView ?
mnn

Caliburn là tốt đẹp và thanh lịch. Hoạt động khá dễ dàng cho các hệ thống phân cấp lồng nhau
Purusartha

8

Tôi đã xem trang này để tìm câu trả lời giống như tác giả ban đầu và chứng minh rằng luôn có nhiều hơn một cách để làm điều đó, giải pháp cho tôi thậm chí còn dễ dàng hơn các câu trả lời được cung cấp ở đây cho đến nay, vì vậy tôi nghĩ rằng tôi cũng có thể thêm vào đến đống.

Động lực cho sự ràng buộc là giữ cho nó đẹp & MVVM. Cách sử dụng có thể có của ViewModel là có một thuộc tính với tên như "CurrentT Breathy" và ở một nơi khác, DataContext trên một số thứ khác bị ràng buộc với "CurrentT Breathy".

Thay vì thực hiện các bước bổ sung cần thiết (ví dụ: hành vi tùy chỉnh, kiểm soát của bên thứ 3) để hỗ trợ một liên kết đẹp từ TreeView cho Mô hình của tôi và sau đó từ một thứ khác vào Mô hình của tôi, giải pháp của tôi là sử dụng Phần tử đơn giản ràng buộc thứ khác vào TreeView.SelectedItem, thay vì ràng buộc điều khác với ViewModel của tôi, do đó bỏ qua công việc bổ sung cần thiết.

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}" />

Tất nhiên, điều này là tuyệt vời để đọc mục hiện đang được chọn, nhưng không đặt nó, đó là tất cả những gì tôi cần.


1
Địa phương là gì: MyT BreathyDetailsView? Tôi nhận được địa phương đó: MyT BreathyDetailsView giữ mục đã chọn, nhưng làm thế nào để mô hình xem của bạn có được thông tin này? Đây có vẻ là một cách hay, gọn gàng để làm điều này, nhưng tôi chỉ cần thêm một chút thông tin ...
Bob Horn

local: MyT BreathyDetailsView chỉ đơn giản là một UserControl có đầy đủ XAML tạo nên một cái nhìn chi tiết về một trường hợp "điều". Nó được nhúng ở giữa chế độ xem khác dưới dạng nội dung, với DataContext của chế độ xem này là mục chế độ xem dạng cây hiện được chọn, sử dụng liên kết Phần tử.
Wes

6

Bạn cũng có thể sử dụng thuộc tính TreeViewItem.IsSelected


Tôi nghĩ rằng đây có thể là câu trả lời chính xác. Nhưng tôi muốn xem một ví dụ hoặc khuyến nghị thực hành tốt nhất về cách thuộc tính IsSelected của các Mục được chuyển đến TreeView.
anhoppe

3

Ngoài ra còn có một cách để tạo thuộc tính ChọnItem ràng buộc XAML mà không cần sử dụng Tương tác.

public static class BindableSelectedItemHelper
{
    #region Properties

    public static readonly DependencyProperty SelectedItemProperty = DependencyProperty.RegisterAttached("SelectedItem", typeof(object), typeof(BindableSelectedItemHelper),
        new FrameworkPropertyMetadata(null, OnSelectedItemPropertyChanged));

    public static readonly DependencyProperty AttachProperty = DependencyProperty.RegisterAttached("Attach", typeof(bool), typeof(BindableSelectedItemHelper), new PropertyMetadata(false, Attach));

    private static readonly DependencyProperty IsUpdatingProperty = DependencyProperty.RegisterAttached("IsUpdating", typeof(bool), typeof(BindableSelectedItemHelper));

    #endregion

    #region Implementation

    public static void SetAttach(DependencyObject dp, bool value)
    {
        dp.SetValue(AttachProperty, value);
    }

    public static bool GetAttach(DependencyObject dp)
    {
        return (bool)dp.GetValue(AttachProperty);
    }

    public static string GetSelectedItem(DependencyObject dp)
    {
        return (string)dp.GetValue(SelectedItemProperty);
    }

    public static void SetSelectedItem(DependencyObject dp, object value)
    {
        dp.SetValue(SelectedItemProperty, value);
    }

    private static bool GetIsUpdating(DependencyObject dp)
    {
        return (bool)dp.GetValue(IsUpdatingProperty);
    }

    private static void SetIsUpdating(DependencyObject dp, bool value)
    {
        dp.SetValue(IsUpdatingProperty, value);
    }

    private static void Attach(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
        TreeListView treeListView = sender as TreeListView;
        if (treeListView != null)
        {
            if ((bool)e.OldValue)
                treeListView.SelectedItemChanged -= SelectedItemChanged;

            if ((bool)e.NewValue)
                treeListView.SelectedItemChanged += SelectedItemChanged;
        }
    }

    private static void OnSelectedItemPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
        TreeListView treeListView = sender as TreeListView;
        if (treeListView != null)
        {
            treeListView.SelectedItemChanged -= SelectedItemChanged;

            if (!(bool)GetIsUpdating(treeListView))
            {
                foreach (TreeViewItem item in treeListView.Items)
                {
                    if (item == e.NewValue)
                    {
                        item.IsSelected = true;
                        break;
                    }
                    else
                       item.IsSelected = false;                        
                }
            }

            treeListView.SelectedItemChanged += SelectedItemChanged;
        }
    }

    private static void SelectedItemChanged(object sender, RoutedEventArgs e)
    {
        TreeListView treeListView = sender as TreeListView;
        if (treeListView != null)
        {
            SetIsUpdating(treeListView, true);
            SetSelectedItem(treeListView, treeListView.SelectedItem);
            SetIsUpdating(treeListView, false);
        }
    }
    #endregion
}

Sau đó, bạn có thể sử dụng điều này trong XAML của mình dưới dạng:

<TreeView  helper:BindableSelectedItemHelper.Attach="True" 
           helper:BindableSelectedItemHelper.SelectedItem="{Binding SelectedItem, Mode=TwoWay}">

3

Tôi đã thử tất cả các giải pháp của câu hỏi này. Không ai giải quyết vấn đề của tôi đầy đủ. Vì vậy, tôi nghĩ rằng tốt hơn là sử dụng lớp kế thừa như vậy với thuộc tính được xác định lại là ChọnItem. Nó sẽ hoạt động hoàn hảo nếu bạn chọn phần tử cây từ GUI và nếu bạn đặt giá trị thuộc tính này trong mã của mình

public class TreeViewEx : TreeView
{
    public TreeViewEx()
    {
        this.SelectedItemChanged += new RoutedPropertyChangedEventHandler<object>(TreeViewEx_SelectedItemChanged);
    }

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

    #region SelectedItem

    /// <summary>
    /// Gets or Sets the SelectedItem possible Value of the TreeViewItem object.
    /// </summary>
    public new object SelectedItem
    {
        get { return this.GetValue(TreeViewEx.SelectedItemProperty); }
        set { this.SetValue(TreeViewEx.SelectedItemProperty, value); }
    }

    // Using a DependencyProperty as the backing store for MyProperty.  This enables animation, styling, binding, etc...
    public new static readonly DependencyProperty SelectedItemProperty =
        DependencyProperty.Register("SelectedItem", typeof(object), typeof(TreeViewEx),
        new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, SelectedItemProperty_Changed));

    static void SelectedItemProperty_Changed(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
    {
        TreeViewEx targetObject = dependencyObject as TreeViewEx;
        if (targetObject != null)
        {
            TreeViewItem tvi = targetObject.FindItemNode(targetObject.SelectedItem) as TreeViewItem;
            if (tvi != null)
                tvi.IsSelected = true;
        }
    }                                               
    #endregion SelectedItem   

    public TreeViewItem FindItemNode(object item)
    {
        TreeViewItem node = null;
        foreach (object data in this.Items)
        {
            node = this.ItemContainerGenerator.ContainerFromItem(data) as TreeViewItem;
            if (node != null)
            {
                if (data == item)
                    break;
                node = FindItemNodeInChildren(node, item);
                if (node != null)
                    break;
            }
        }
        return node;
    }

    protected TreeViewItem FindItemNodeInChildren(TreeViewItem parent, object item)
    {
        TreeViewItem node = null;
        bool isExpanded = parent.IsExpanded;
        if (!isExpanded) //Can't find child container unless the parent node is Expanded once
        {
            parent.IsExpanded = true;
            parent.UpdateLayout();
        }
        foreach (object data in parent.Items)
        {
            node = parent.ItemContainerGenerator.ContainerFromItem(data) as TreeViewItem;
            if (data == item && node != null)
                break;
            node = FindItemNodeInChildren(node, item);
            if (node != null)
                break;
        }
        if (node == null && parent.IsExpanded != isExpanded)
            parent.IsExpanded = isExpanded;
        if (node != null)
            parent.IsExpanded = true;
        return node;
    }
} 

Sẽ nhanh hơn nhiều nếu UpdateLayout () và IsExpanded không được gọi cho một số nút. Khi không cần thiết phải gọi UpdateLayout () và IsExpanded? Khi mục cây đã được truy cập trước đó. Làm sao để biết điều đó? ContainerFromItem () trả về null cho các nút không được truy cập. Vì vậy, chúng ta chỉ có thể mở rộng nút cha khi ContainerFromItem () trả về null cho trẻ em.
CoperNick

3

Yêu cầu của tôi là về giải pháp dựa trên PRISM-MVVM trong đó cần có TreeView và đối tượng bị ràng buộc thuộc kiểu Bộ sưu tập <> và do đó cần HVELicalDataTemplate. BindableSelectedItemBehavior mặc định sẽ không thể xác định TreeViewItem con. Để làm cho nó hoạt động trong kịch bản này.

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;
    }
}

Điều này cho phép lặp qua tất cả các yếu tố không phân biệt cấp độ.


Cảm ơn bạn! Đây là cái duy nhất hoạt động cho kịch bản của tôi không giống với kịch bản của bạn.
Robert

Hoạt động rất tốt và không làm cho các ràng buộc được chọn / mở rộng bị lẫn lộn .
Rusty

2

Tôi đề nghị bổ sung vào hành vi được cung cấp bởi Steve Greatrex. Hành vi của anh ta không phản ánh các thay đổi từ nguồn vì nó có thể không phải là một bộ sưu tập TreeViewItems. Vì vậy, vấn đề tìm TreeViewItem trong cây mà datacontext làValue được chọn từ nguồn. TreeView có một thuộc tính được bảo vệ gọi là "Itemhost", chứa bộ sưu tập TreeViewItem. Chúng ta có thể lấy nó thông qua sự phản chiếu và đi trên cây để tìm kiếm vật phẩm đã chọn.

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;
    }

Bằng cách này, hành vi làm việc cho các ràng buộc hai chiều. Ngoài ra, có thể di chuyển việc mua lại Itemhost sang phương thức OnAttached của Behavior, tiết kiệm chi phí sử dụng sự phản chiếu mỗi khi cập nhật ràng buộc.


2

TreeF WPF TreeView chọnItem

... là một câu trả lời tốt hơn, nhưng không đề cập đến một cách để có được / thiết lập mục chọn trong ViewModel.

  1. Thêm một thuộc tính boolean IsSelected vào ItemViewModel của bạn và liên kết với nó trong Style Setter cho TreeViewItem.
  2. Thêm một thuộc tính chọnItem vào ViewModel của bạn được sử dụng làm DataContext cho TreeView. Đây là phần còn thiếu trong giải pháp trên.
    'ItemVM ...
    Tài sản công cộng được chọn là Boolean
        Được
            Trả về _func.SelectedNode là tôi
        Kết thúc nhận
        Đặt (giá trị là Boolean)
            Nếu giá trị được chọn thì
                _func.SelectedNode = If (value, Me, nothing)
            Kết thúc nếu
            RaisePropertyChange ()
        Bộ cuối
    Tài sản cuối
    'TreeVM ...
    Tài sản công cộng được chọn như ItemVM
        Được
            Trả về _selectedItem
        Kết thúc nhận
        Đặt (giá trị là ItemVM)
            Nếu _selectedItem là giá trị thì
                Trở về
            Kết thúc nếu
            Dim trước = _selectedItem
            _selectedItem = value
            Nếu trước đó là Không có gì thì
                trước.IsSelected = Sai
            Kết thúc nếu
            Nếu _selectedItem IsNot thì không
                _selectedItem.IsSelected = Đúng
            Kết thúc nếu
        Bộ cuối
    Tài sản cuối
<TreeView ItemsSource="{Binding Path=TreeVM}" 
          BorderBrush="Transparent">
    <TreeView.ItemContainerStyle>
        <Style TargetType="TreeViewItem">
            <Setter Property="IsExpanded" Value="{Binding IsExpanded}"/>
            <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}"/>
        </Style>
    </TreeView.ItemContainerStyle>
    <TreeView.ItemTemplate>
        <HierarchicalDataTemplate ItemsSource="{Binding Children}">
            <TextBlock Text="{Binding Name}"/>
        </HierarchicalDataTemplate>
    </TreeView.ItemTemplate>
</TreeView>

1

Sau khi nghiên cứu Internet một ngày, tôi đã tìm thấy giải pháp của riêng mình để chọn một mục sau khi tạo một treeview bình thường trong môi trường WPF / C # bình thường

private void BuildSortTree(int sel)
        {
            MergeSort.Items.Clear();
            TreeViewItem itTemp = new TreeViewItem();
            itTemp.Header = SortList[0];
            MergeSort.Items.Add(itTemp);
            TreeViewItem prev;
            itTemp.IsExpanded = true;
            if (0 == sel) itTemp.IsSelected= true;
            prev = itTemp;
            for(int i = 1; i<SortList.Count; i++)
            {

                TreeViewItem itTempNEW = new TreeViewItem();
                itTempNEW.Header = SortList[i];
                prev.Items.Add(itTempNEW);
                itTempNEW.IsExpanded = true;
                if (i == sel) itTempNEW.IsSelected = true;
                prev = itTempNEW ;
            }
        }

1

Nó cũng có thể được thực hiện bằng cách sử dụng thuộc tính IsSelected của mục TreeView. Đây là cách tôi quản lý nó,

public delegate void TreeviewItemSelectedHandler(TreeViewItem item);
public class TreeViewItem
{      
  public static event TreeviewItemSelectedHandler OnItemSelected = delegate { };
  public bool IsSelected 
  {
    get { return isSelected; }
    set 
    { 
      isSelected = value;
      if (value)
        OnItemSelected(this);
    }
  }
}

Sau đó, trong ViewModel chứa dữ liệu mà TreeView của bạn bị ràng buộc, chỉ cần đăng ký vào sự kiện trong lớp TreeViewItem.

TreeViewItem.OnItemSelected += TreeViewItemSelected;

Và cuối cùng, triển khai trình xử lý này trong cùng ViewModel,

private void TreeViewItemSelected(TreeViewItem item)
{
  //Do something
}

Và ràng buộc tất nhiên,

<Setter Property="IsSelected" Value="{Binding IsSelected}" />    

Đây thực sự là một giải pháp được đánh giá thấp. Bằng cách thay đổi cách suy nghĩ và ràng buộc thuộc tính IsSelected của từng yếu tố treeview và làm nổi bật các sự kiện IsSelected mà bạn có để sử dụng chức năng tích hợp hoạt động tốt với ràng buộc hai chiều. Tôi đã thử nhiều giải pháp được đề xuất cho vấn đề này và đây là giải pháp đầu tiên đã có hiệu quả. Chỉ cần một chút phức tạp để lên dây. Cảm ơn.
Richard Moore

1

Tôi biết chủ đề này đã 10 tuổi nhưng vấn đề vẫn còn tồn tại ....

Câu hỏi ban đầu là 'để lấy' mục đã chọn. Tôi cũng cần phải "lấy" mục đã chọn trong chế độ xem của mình (không đặt mục đó). Trong tất cả các câu trả lời trong chuỗi này, câu trả lời của 'Wes' là câu trả lời duy nhất tiếp cận vấn đề khác nhau: Nếu bạn có thể sử dụng 'Mục được chọn' làm mục tiêu cho cơ sở dữ liệu, hãy sử dụng nó làm nguồn cho việc ghi dữ liệu. Wes đã làm điều đó cho một thuộc tính view khác, tôi sẽ làm điều đó với một thuộc tính viewmodel:

Chúng ta cần hai điều:

  • Tạo thuộc tính phụ thuộc trong chế độ xem (trong trường hợp của tôi là loại 'MyObject' vì treeview của tôi bị ràng buộc với đối tượng của loại 'MyObject')
  • Liên kết từ Treeview.SelectedItem với thuộc tính này trong hàm tạo của View (vâng, đó là mã phía sau nhưng, có khả năng là bạn cũng sẽ khởi tạo datacontext của mình ở đó)

Chế độ xem:

public static readonly DependencyProperty SelectedTreeViewItemProperty = DependencyProperty.Register("SelectedTreeViewItem", typeof(MyObject), typeof(MyViewModel), new PropertyMetadata(OnSelectedTreeViewItemChanged));

    private static void OnSelectedTreeViewItemChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        (d as MyViewModel).OnSelectedTreeViewItemChanged(e);
    }

    private void OnSelectedTreeViewItemChanged(DependencyPropertyChangedEventArgs e)
    {
        //do your stuff here
    }

    public MyObject SelectedWorkOrderTreeViewItem
    {
        get { return (MyObject)GetValue(SelectedTreeViewItemProperty); }
        set { SetValue(SelectedTreeViewItemProperty, value); }
    }

Xem nhà xây dựng:

Binding binding = new Binding("SelectedItem")
        {
            Source = treeView, //name of tree view in xaml
            Mode = BindingMode.OneWay
        };

        BindingOperations.SetBinding(DataContext, MyViewModel.SelectedTreeViewItemProperty, binding);

0

(Tất cả chúng ta đều đồng ý rằng TreeView rõ ràng đã bị đánh bại đối với vấn đề này. Liên kết với chọnItem sẽ là hiển nhiên. Thở dài )

Tôi cần giải pháp để tương tác đúng với thuộc tính IsSelected của TreeViewItem, vì vậy đây là cách tôi đã làm:

// the Type CustomThing needs to implement IsSelected with notification
// for this to work.
public class CustomTreeView : TreeView
{
    public CustomThing SelectedCustomThing
    {
        get
        {
            return (CustomThing)GetValue(SelectedNode_Property);
        }
        set
        {
            SetValue(SelectedNode_Property, value);
            if(value != null) value.IsSelected = true;
        }
    }

    public static DependencyProperty SelectedNode_Property =
        DependencyProperty.Register(
            "SelectedCustomThing",
            typeof(CustomThing),
            typeof(CustomTreeView),
            new FrameworkPropertyMetadata(
                null,
                FrameworkPropertyMetadataOptions.None,
                SelectedNodeChanged));

    public CustomTreeView(): base()
    {
        this.SelectedItemChanged += new RoutedPropertyChangedEventHandler<object>(SelectedItemChanged_CustomHandler);
    }

    void SelectedItemChanged_CustomHandler(object sender, RoutedPropertyChangedEventArgs<object> e)
    {
        SetValue(SelectedNode_Property, SelectedItem);
    }

    private static void SelectedNodeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var treeView = d as CustomTreeView;
        var newNode = e.NewValue as CustomThing;

        treeView.SelectedCustomThing = (CustomThing)e.NewValue;
    }
}

Với XAML này:

<local:CustonTreeView ItemsSource="{Binding TreeRoot}" 
    SelectedCustomThing="{Binding SelectedNode,Mode=TwoWay}">
    <TreeView.ItemContainerStyle>
        <Style TargetType="TreeViewItem">
            <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
        </Style>
    </TreeView.ItemContainerStyle>
</local:CustonTreeView>

0

Tôi mang đến cho bạn giải pháp của tôi cung cấp các tính năng sau:

  • Hỗ trợ ràng buộc 2 cách

  • Tự động cập nhật các thuộc tính TreeViewItem.IsSelected (theo ChọnItem)

  • Không có phân lớp TreeView

  • Các mục bị ràng buộc với ViewModel có thể thuộc bất kỳ loại nào (thậm chí là null)

1 / Dán đoạn mã sau vào CS của bạn:

public class BindableSelectedItem
{
    public static readonly DependencyProperty SelectedItemProperty = DependencyProperty.RegisterAttached(
        "SelectedItem", typeof(object), typeof(BindableSelectedItem), new PropertyMetadata(default(object), OnSelectedItemPropertyChangedCallback));

    private static void OnSelectedItemPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var treeView = d as TreeView;
        if (treeView != null)
        {
            BrowseTreeViewItems(treeView, tvi =>
            {
                tvi.IsSelected = tvi.DataContext == e.NewValue;
            });
        }
        else
        {
            throw new Exception("Attached property supports only TreeView");
        }
    }

    public static void SetSelectedItem(DependencyObject element, object value)
    {
        element.SetValue(SelectedItemProperty, value);
    }

    public static object GetSelectedItem(DependencyObject element)
    {
        return element.GetValue(SelectedItemProperty);
    }

    public static void BrowseTreeViewItems(TreeView treeView, Action<TreeViewItem> onBrowsedTreeViewItem)
    {
        var collectionsToVisit = new System.Collections.Generic.List<Tuple<ItemContainerGenerator, ItemCollection>> { new Tuple<ItemContainerGenerator, ItemCollection>(treeView.ItemContainerGenerator, treeView.Items) };
        var collectionIndex = 0;
        while (collectionIndex < collectionsToVisit.Count)
        {
            var itemContainerGenerator = collectionsToVisit[collectionIndex].Item1;
            var itemCollection = collectionsToVisit[collectionIndex].Item2;
            for (var i = 0; i < itemCollection.Count; i++)
            {
                var tvi = itemContainerGenerator.ContainerFromIndex(i) as TreeViewItem;
                if (tvi == null)
                {
                    continue;
                }

                if (tvi.ItemContainerGenerator.Status == System.Windows.Controls.Primitives.GeneratorStatus.ContainersGenerated)
                {
                    collectionsToVisit.Add(new Tuple<ItemContainerGenerator, ItemCollection>(tvi.ItemContainerGenerator, tvi.Items));
                }

                onBrowsedTreeViewItem(tvi);
            }

            collectionIndex++;
        }
    }

}

2 / Ví dụ sử dụng trong tệp XAML của bạn

<TreeView myNS:BindableSelectedItem.SelectedItem="{Binding Path=SelectedItem, Mode=TwoWay}" />  

0

Tôi đề xuất giải pháp này (mà tôi cho là dễ rò rỉ nhất và bộ nhớ miễn phí) hoạt động hoàn hảo để cập nhật mục đã chọn của ViewModel từ mục đã chọn của View.

Xin lưu ý rằng việc thay đổi mục đã chọn từ ViewModel sẽ không cập nhật mục đã chọn của Chế độ xem.

public class TreeViewEx : TreeView
{
    public static readonly DependencyProperty SelectedItemExProperty = DependencyProperty.Register("SelectedItemEx", typeof(object), typeof(TreeViewEx), new FrameworkPropertyMetadata(default(object))
    {
        BindsTwoWayByDefault = true // Required in order to avoid setting the "BindingMode" from the XAML
    });

    public object SelectedItemEx
    {
        get => GetValue(SelectedItemExProperty);
        set => SetValue(SelectedItemExProperty, value);
    }

    protected override void OnSelectedItemChanged(RoutedPropertyChangedEventArgs<object> e)
    {
        SelectedItemEx = e.NewValue;
    }
}

Sử dụng XAML

<l:TreeViewEx ItemsSource="{Binding Path=Items}" SelectedItemEx="{Binding Path=SelectedItem}" >
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.