Mengikat data ke SelectedItem di WPF Treeview


241

Bagaimana saya bisa mengambil item yang dipilih dalam WPF-treeview? Saya ingin melakukan ini di XAML, karena saya ingin mengikatnya.

Anda mungkin berpikir bahwa itu SelectedItemtetapi ternyata tidak ada hanya dapat dibaca dan karenanya tidak dapat digunakan.

Inilah yang ingin saya lakukan:

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

Saya ingin mengikat SelectedItemke properti di Model saya.

Tapi ini memberi saya kesalahan:

Properti 'SelectedItem' hanya baca dan tidak dapat diatur dari markup.

Sunting: Oke, ini cara saya memecahkan masalah ini:

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

dan dalam codebehindfile dari xaml saya:

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

51
Man ini menyebalkan. Itu hanya memukul saya juga. Saya datang ke sini berharap menemukan bahwa ada cara yang layak dan saya hanya seorang idiot. Ini adalah pertama kalinya saya sedih bahwa saya bukan orang idiot ..
Andrei Rînea

6
ini benar-benar menyebalkan dan mengacaukan konsep yang mengikat
Delta

Semoga ini bisa membantu seseorang untuk mengikat ke item tampilan pohon yang dipilih berubah panggil kembali di Icommand jacobaloysious.wordpress.com/2012/02/19/…
jacob aloysious

9
Dalam hal pengikatan dan MVVM, kode di belakang tidak "dilarang", melainkan kode di belakang yang harus mendukung tampilan. Menurut pendapat saya dari semua solusi lain yang pernah saya lihat, kode di belakang adalah pilihan yang jauh lebih baik karena masih berurusan dengan "mengikat" pandangan ke viewmodel. Satu-satunya negatif adalah bahwa jika Anda memiliki tim dengan desainer yang hanya bekerja di XAML, kode di belakangnya bisa rusak / terabaikan. Ini adalah harga kecil untuk membayar solusi yang membutuhkan 10 detik untuk diterapkan.
nrjohnstone

Salah satu solusi termudah mungkin: stackoverflow.com/questions/1238304/…
JoanComasFdz

Jawaban:


240

Saya menyadari bahwa jawaban ini sudah diterima, tetapi saya menyatukannya untuk menyelesaikan masalah. Ini menggunakan ide yang mirip dengan solusi Delta, tetapi tanpa perlu mensubklasifikasikan 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;
    }
}

Anda kemudian dapat menggunakan ini di XAML Anda sebagai:

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

Semoga ini bisa membantu seseorang!


5
Seperti yang ditunjukkan Brent, saya juga perlu menambahkan Mode = TwoWay ke penjilidan. Saya bukan "Blender" jadi tidak terbiasa dengan kelas <> Perilaku dari System.Windows.Interactivity. Majelis adalah bagian dari Expression Blend. Bagi mereka yang tidak ingin membeli / menginstal percobaan untuk mendapatkan perakitan ini, Anda dapat mengunduh BlendSDK yang mencakup System.Windows.Interactivity. BlendSDK 3 untuk 3.5 ... Saya pikir itu BlendSDK 4 untuk 4.0. Catatan: Ini hanya memungkinkan Anda untuk mendapatkan item apa yang dipilih, tidak memungkinkan Anda untuk mengatur item yang dipilih
Mike Rowley

4
Anda juga dapat mengganti UIPropertyMetadata dengan FrameworkPropertyMetadata (null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnSelectedItemChanged));
Filimindji

3
Ini akan menjadi pendekatan untuk menyelesaikan masalah: stackoverflow.com/a/18700099/4227
bitbonk

2
@Lukas persis seperti yang ditunjukkan dalam cuplikan kode XAML di atas. Ganti saja {Binding SelectedItem, Mode=TwoWay}dengan{Binding MyViewModelField, Mode=TwoWay}
Steve Greatrex

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


43

Jawab dengan properti terlampir dan tidak ada ketergantungan eksternal, jika perlu pernah muncul!

Anda dapat membuat properti terlampir yang dapat diikat dan memiliki pengambil dan penyetel:

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

Tambahkan deklarasi namespace yang berisi kelas itu ke XAML Anda dan ikat sebagai berikut (lokal adalah bagaimana saya menamakan deklarasi namespace):

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

    </TreeView>

Sekarang Anda dapat mengikat item yang dipilih, dan juga mengaturnya dalam model tampilan Anda untuk mengubahnya secara terprogram, jika persyaratan itu pernah muncul. Ini, tentu saja, dengan asumsi bahwa Anda menerapkan INotifyPropertyChanged pada properti tertentu.


4
+1, jawaban terbaik di utas ini imho. Tidak ada ketergantungan pada System.Windows.Interactivity, dan memungkinkan pengikatan dua arah (pengaturan terprogram di lingkungan MVVM). Sempurna.
Chris Ray

5
Masalah dengan pendekatan ini adalah perilaku hanya akan mulai berfungsi setelah item yang dipilih telah ditetapkan sekali melalui pengikatan (yaitu dari ViewModel). Jika nilai awal di VM adalah nol, maka ikatan tidak akan memperbarui nilai DP, dan perilaku tidak akan diaktifkan. Anda dapat memperbaikinya menggunakan item yang dipilih default yang berbeda (misalnya item yang tidak valid).
Markus

6
@ Mark: Cukup gunakan objek baru () alih-alih nol di atas ketika membuat instance UIPropertyMetadata properti terlampir. Masalahnya harus hilang kalau begitu ...
barnacleboy

2
Para pemain untuk TreeViewItem gagal bagi saya, saya berasumsi karena saya menggunakan HierarchicalDataTemplate yang diterapkan dari sumber daya dengan tipe data. Tetapi jika Anda menghapus ChangeSelectedItem, maka mengikat ke model tampilan dan mengambil item berfungsi dengan baik.
Casey Sebben

1
Saya juga mengalami masalah dengan para pemeran untuk TreeViewItem. Pada titik itu, ItemContainerGenerator hanya berisi referensi ke item root, tapi saya membutuhkannya untuk bisa mendapatkan item yang juga bukan root. Jika Anda meneruskan referensi ke satu, para pemain gagal dan mengembalikan nol. Tidak yakin bagaimana ini bisa diperbaiki?
Bob Tway 3-15

39

Yah, saya menemukan solusinya. Ini memindahkan kekacauan, sehingga MVVM berfungsi.

Pertama-tama tambahkan kelas ini:

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

dan tambahkan ini ke xaml Anda:

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

3
Ini adalah SATU-SATUNYA hal yang nyaris berhasil bagi saya sejauh ini. Saya sangat suka solusi ini.
Rachael

1
Tidak tahu mengapa tetapi itu tidak berhasil untuk saya :( Saya berhasil mendapatkan item yang dipilih dari pohon tetapi tidak sebaliknya - untuk mengubah item yang dipilih dari luar pohon.
Erez

Akan sedikit lebih rapi untuk mengatur properti dependensi sebagai BindsTwoWayByDefault maka Anda tidak perlu menentukan TwoWay di XAML
Stephen Holt

Ini adalah pendekatan terbaik. Itu tidak menggunakan referensi Interaktivitas, tidak menggunakan kode di belakang, tidak memiliki kebocoran memori seperti beberapa perilaku. Terima kasih.
Alexandru Dicu

Seperti yang disebutkan, solusi ini tidak bekerja dengan penjilidan 2 arah. Jika Anda menetapkan nilai dalam viewmodel, perubahan tidak merambat ke TreeView.
Richard Moore

25

Jawabannya sedikit lebih banyak daripada yang diharapkan OP ... Tapi saya harap itu setidaknya bisa membantu seseorang.

Jika Anda ingin menjalankan ICommandsetiap kali SelectedItemperubahan, Anda dapat mengikat perintah pada suatu peristiwa dan penggunaan properti SelectedItemdi ViewModeltidak diperlukan lagi.

Untuk melakukannya:

1- Tambahkan referensi ke System.Windows.Interactivity

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

2- Ikatkan perintah ke acara tersebut 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
Referensi System.Windows.Interactivitydapat diinstal dari NuGet: nuget.org/packages/System.Windows.Interactivity.WPF
Junle Li

Saya sudah mencoba menyelesaikan masalah ini selama berjam-jam, saya sudah menerapkan ini tetapi perintah saya tidak berfungsi, bisakah Anda membantu saya?
Alfie

1
Ada yang memperkenalkan Perilaku XAML untuk WPF oleh Microsoft pada akhir tahun 2018. Ini dapat digunakan sebagai ganti dari System.Windows.Interactivity. Itu berhasil untuk saya (dicoba dengan proyek .NET Core). Untuk mengatur semuanya, cukup tambahkan paket nuget Microsoft.Xaml.Behaviors.Wpf , ubah namespace menjadi xmlns:i="http://schemas.microsoft.com/xaml/behaviors". Untuk mendapatkan info lebih lanjut - silakan lihat blog
rychlmoj

19

Ini dapat dicapai dengan cara yang 'lebih baik' dengan hanya menggunakan binding dan EventToCommand GalaSoft MVVM Light library. Dalam VM Anda, tambahkan perintah yang akan dipanggil ketika item yang dipilih diubah, dan inisialisasi perintah untuk melakukan tindakan apa pun yang diperlukan. Dalam contoh ini saya menggunakan RelayCommand dan hanya akan mengatur properti 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; }
}

Kemudian tambahkan perilaku EventToCommand di xaml Anda. Ini sangat mudah menggunakan campuran.

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

Ini adalah solusi yang bagus terutama jika Anda sudah menggunakan toolkit MvvmLight. Namun itu tidak menyelesaikan masalah pengaturan simpul yang dipilih dan meminta treeview memperbarui pilihan.
keft

12

Semua menjadi rumit ... Gunakan Caliburn Micro (http://caliburnmicro.codeplex.com/)

Melihat:

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

ViewModel:

public void SetSelectedItem(YourNodeViewModel item) {}; 

5
Ya ... dan di mana bagian yang menetapkan SelectedItem di TreeView ?
mnn

Caliburn bagus dan elegan. Bekerja cukup mudah untuk hierarki bersarang
Purusartha

8

Saya menemukan halaman ini mencari jawaban yang sama dengan penulis asli, dan membuktikan selalu ada lebih dari satu cara untuk melakukannya, solusi bagi saya bahkan lebih mudah daripada jawaban yang diberikan di sini sejauh ini, jadi saya pikir saya mungkin juga menambahkan ke tumpukan.

Motivasi untuk mengikat adalah untuk tetap bagus & MVVM. Kemungkinan penggunaan ViewModel adalah memiliki properti dengan nama seperti "CurrentThingy", dan di tempat lain, DataContext pada beberapa hal lain terikat dengan "CurrentThingy".

Daripada melalui langkah-langkah tambahan yang diperlukan (misalnya: perilaku kustom, kontrol pihak ke-3) untuk mendukung pengikatan yang bagus dari TreeView ke Model saya, dan kemudian dari hal lain ke Model saya, solusi saya adalah menggunakan Elemen sederhana yang mengikat hal lain untuk TreeView.SelectedItem, daripada mengikat hal lain ke ViewModel saya, sehingga melewatkan pekerjaan tambahan yang diperlukan.

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

Tentu saja, ini bagus untuk membaca item yang dipilih saat ini, tetapi tidak mengaturnya, yang saya butuhkan.


1
Apa itu lokal: MyThingyDetailsView? Saya mendapatkan informasi lokal: MyThingyDetailsView menyimpan item yang dipilih, tetapi bagaimana model tampilan Anda mendapatkan info ini? Ini terlihat seperti cara yang bagus dan bersih untuk melakukan ini, tetapi saya perlu sedikit info lagi ...
Bob Horn

local: MyThingyDetailsView hanyalah sebuah UserControl penuh XAML yang membuat tampilan detail tentang satu instance "thingy". Itu tertanam di tengah tampilan lain sebagai konten, dengan DataContext tampilan ini adalah item tampilan pohon yang dipilih saat ini, menggunakan Elemen mengikat.
Wes

6

Anda mungkin juga dapat menggunakan properti TreeViewItem.IsSelected


Saya pikir ini mungkin jawaban yang benar. Tapi saya ingin melihat contoh atau rekomendasi praktik terbaik tentang bagaimana properti IsSelected dari Item diteruskan ke TreeView.
anhoppe

3

Ada juga cara untuk membuat properti SelectedItem bindable XAML tanpa menggunakan Interaksi. Perilaku.

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
}

Anda kemudian dapat menggunakan ini di XAML Anda sebagai:

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

3

Saya mencoba semua solusi dari pertanyaan ini. Tidak ada yang menyelesaikan masalah saya sepenuhnya. Jadi saya pikir lebih baik menggunakan kelas warisan seperti itu dengan properti SelectedItem yang didefinisikan ulang. Ini akan bekerja dengan sempurna jika Anda memilih elemen pohon dari GUI dan jika Anda menetapkan nilai properti ini dalam kode Anda

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

Akan jauh lebih cepat jika UpdateLayout () dan IsExpanded tidak dipanggil untuk beberapa node. Ketika tidak diperlukan untuk memanggil UpdateLayout () dan IsExpanded? Ketika item pohon dikunjungi sebelumnya. Bagaimana cara mengetahuinya? ContainerFromItem () mengembalikan null untuk node yang tidak dikunjungi. Jadi kita dapat memperluas simpul induk hanya ketika ContainerFromItem () mengembalikan nol untuk anak-anak.
CoperNick

3

Persyaratan saya adalah untuk solusi berbasis PRISM-MVVM di mana TreeView diperlukan dan objek terikat adalah tipe Collection <> dan karenanya membutuhkan HierarchicalDataTemplate. BindableSelectedItemBehavior default tidak akan dapat mengidentifikasi TreeViewItem anak. Untuk membuatnya berfungsi dalam skenario ini.

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

Ini memungkinkan untuk beralih melalui semua elemen terlepas dari level.


Terima kasih! Ini adalah satu-satunya yang berfungsi untuk skenario saya yang tidak seperti milik Anda.
Robert

Bekerja sangat baik, dan tidak menyebabkan binding yang dipilih / diperluas menjadi bingung .
Rusty

2

Saya menyarankan tambahan untuk perilaku yang disediakan oleh Steve Greatrex. Perilakunya tidak mencerminkan perubahan dari sumber karena itu mungkin bukan kumpulan TreeViewItems. Jadi itu adalah masalah menemukan TreeViewItem di pohon yang datacontext adalah Nilai yang dipilih dari sumber. TreeView memiliki properti yang dilindungi yang disebut "ItemsHost", yang menampung koleksi TreeViewItem. Kita bisa mendapatkannya melalui refleksi dan berjalan di pohon mencari item yang dipilih.

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

Dengan cara ini perilaku berfungsi untuk binding dua arah. Atau, dimungkinkan untuk memindahkan akuisisi ItemsHost ke metode OnAttached Behavior, menghemat biaya tambahan menggunakan refleksi setiap kali pembaruan yang mengikat.


2

WPF MVVM TreeView SelectedItem

... adalah jawaban yang lebih baik, tetapi tidak menyebutkan cara untuk mendapatkan / mengatur SelectedItem di ViewModel.

  1. Tambahkan properti boolean IsSelected ke ItemViewModel Anda, dan ikat ke dalam Style Setter untuk TreeViewItem.
  2. Tambahkan properti SelectedItem ke ViewModel Anda yang digunakan sebagai DataContext untuk TreeView. Ini adalah bagian yang hilang dalam solusi di atas.
    'ItemVM ...
    Properti Publik Terpilih Sebagai Boolean
        Dapatkan
            Kembali _func.SelectedNode Is Me
        End Get
        Set (nilai Sebagai Boolean)
            Jika nilai Terpilih Kemudian
                _func.SelectedNode = If (value, Me, Nothing)
            Berakhir jika
            RaisePropertyChange ()
        Set Akhir
    Akhiri Properti
    'TreeVM ...
    Barang Publik Memilih Barang Sebagai ItemVM
        Dapatkan
            Kembalikan _selectedItem
        End Get
        Set (nilai Sebagai ItemVM)
            If _selectedItem Is value Then
                Kembali
            Berakhir jika
            Dim prev = _selectedItem
            _selectedItem = nilai
            If prev IsNot Nothing Then
                prev.IsSelected = Salah
            Berakhir jika
            If _selectedItem IsNot Nothing Then
                _selectedItem.IsSelected = Benar
            Berakhir jika
        Set Akhir
    Akhiri Properti
<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

Setelah mempelajari Internet selama sehari saya menemukan solusi saya sendiri untuk memilih item setelah membuat treeview normal di lingkungan WPF / C # yang normal

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

Itu juga dapat dilakukan dengan menggunakan properti IsSelected dari item TreeView. Begini cara saya mengelolanya,

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

Kemudian di ViewModel yang berisi data yang terikat dengan TreeView Anda, cukup berlangganan acara di kelas TreeViewItem.

TreeViewItem.OnItemSelected += TreeViewItemSelected;

Dan akhirnya, laksanakan handler ini dalam ViewModel yang sama,

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

Dan tentu saja mengikat,

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

Ini sebenarnya adalah solusi yang berperingkat rendah. Dengan mengubah cara berpikir Anda dan mengikat properti IsSelected dari setiap elemen treeview, dan menggelembungkan peristiwa IsSelected Anda bisa menggunakan fungsionalitas bawaan yang berfungsi baik dengan ikatan dua arah. Saya sudah mencoba banyak solusi yang diusulkan untuk masalah ini, dan ini adalah yang pertama yang berhasil. Hanya sedikit rumit untuk dipasang. Terima kasih.
Richard Moore

1

Saya tahu utas ini berumur 10 tahun tetapi masalahnya masih ada ....

Pertanyaan aslinya adalah 'untuk mengambil' item yang dipilih. Saya juga perlu "mendapatkan" item yang dipilih di viewmodel saya (tidak disetel). Dari semua jawaban di utas ini, yang oleh 'Wes' adalah satu-satunya yang mendekati masalah secara berbeda: Jika Anda dapat menggunakan 'Item yang Dipilih' sebagai target untuk penyatuan data, gunakan sebagai sumber untuk penyatuan data. Wes melakukannya ke properti tampilan lain, saya akan melakukannya ke properti viewmodel:

Kami membutuhkan dua hal:

  • Buat properti ketergantungan dalam viewmodel (dalam kasus saya ketik 'MyObject' karena treeview saya terikat ke objek dari tipe 'MyObject')
  • Bind dari Treeview. Item yang dipilih ke properti ini di konstruktor View (ya itu adalah kode di belakang tetapi, kemungkinan Anda akan init datacontext Anda di sana juga)

Model tampilan:

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

Lihat konstruktor:

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

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

0

(Mari kita semua sepakat bahwa TreeView jelas-jelas rusak sehubungan dengan masalah ini. Mengikat ke SelectedItem sudah jelas. Sigh )

Saya membutuhkan solusi untuk berinteraksi dengan benar dengan properti IsSelected dari TreeViewItem, jadi inilah cara saya melakukannya:

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

Dengan XAML ini:

<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

Saya membawa Anda solusi saya yang menawarkan fitur-fitur berikut:

  • Mendukung 2 cara mengikat

  • Otomatis memperbarui properti TreeViewItem.IsSelected (sesuai dengan SelectedItem)

  • Tidak ada subklasifikasi TreeView

  • Item yang terikat pada ViewModel dapat berupa jenis apa pun (bahkan nol)

1 / Rekatkan kode berikut di CS Anda:

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 / Contoh penggunaan dalam file XAML Anda

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

0

Saya mengusulkan solusi ini (yang saya anggap paling mudah dan bebas dari kebocoran memori) yang berfungsi sempurna untuk memperbarui item yang dipilih ViewModel dari item yang dipilih View.

Harap dicatat bahwa mengubah item yang dipilih dari ViewModel tidak akan memperbarui item yang dipilih dari View.

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

Penggunaan XAML

<l:TreeViewEx ItemsSource="{Binding Path=Items}" SelectedItemEx="{Binding Path=SelectedItem}" >
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.