WPF Ağaç Görünümünde SelectedItem'e veri bağlama


241

WPF treeview'de seçilen öğeyi nasıl alabilirim? Bunu XAML'de yapmak istiyorum, çünkü bağlamak istiyorum.

Bunun var olduğunu düşünebilirsiniz, SelectedItemancak görünüşe göre var olmayan , salt okunur ve bu nedenle kullanılamaz.

Yapmak istediğim şey bu:

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

Modelimdeki SelectedItembir mülke bağlamak istiyorum .

Ama bu bana hata veriyor:

'SelectedItem' özelliği salt okunurdur ve biçimlendirmeden ayarlanamaz.

Düzenleme: Tamam, ben bu çözdü yolu:

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

ve benim xaml kodbehindfile:

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

51
Adamım bu berbat. Sadece bana da vurdu. Buraya gelmenin iyi bir yol olduğunu umarak geldim ve ben sadece bir aptalım. İlk kez aptal olmadığım için üzgünüm ..
Andrei Rînea

6
Bu gerçekten berbat ve bağlayıcı kavram berbat
Delta

Umarım bu bir ağaç seçilen öğeye bağlanmak için bazı birine yardımcı olabilir umut Icommand geri arama geri çağrı jacobaloysious.wordpress.com/2012/02/19/…
jacob aloysious

9
Bağlama ve MVVM açısından, arkasındaki kod "yasaklanmış" değildir, bunun yerine arkasındaki kod görünümü desteklemelidir. Gördüğüm diğer tüm çözümlerden bence, arkasındaki kod çok daha iyi bir seçenektir, çünkü hala görünümü viewmodel'e "bağlama" ile ilgilenmektedir. Tek olumsuz, sadece XAML'de çalışan bir tasarımcıya sahip bir ekibiniz varsa, arkasındaki kod kırılabilir / ihmal edilebilir. Uygulanması 10 saniye süren bir çözüm için küçük bir bedel.
nrjohnstone

Muhtemelen en kolay çözümlerden biri: stackoverflow.com/questions/1238304/…
JoanComasFdz

Yanıtlar:


240

Bunun zaten bir cevabının kabul edildiğinin farkındayım, ama sorunu çözmek için bir araya getirdim. Delta'nın çözümüne benzer bir fikir kullanır, ancak TreeView'i alt sınıflamaya gerek kalmadan:

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

Daha sonra bunu XAML'nizde şu şekilde kullanabilirsiniz:

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

Umarım birine yardım eder!


5
Brent'in de işaret ettiği gibi, bağlayıcıya Mode = TwoWay eklemem gerekiyordu. Ben bir "Blender" değilim bu yüzden System.Windows.Interactivity'den Behavior <> sınıfına aşina değildim. Derleme, İfade Karışımının bir parçasıdır. Bu montajı almak için deneme satın almak / yüklemek istemeyenler için System.Windows.Interactivity içeren BlendSDK'yı indirebilirsiniz. 3.5 için BlendSDK 3 ... Sanırım 4.0 için BlendSDK 4. Not: Bu sadece seçilen öğeyi almanızı sağlar, seçilen öğeyi ayarlamanıza izin vermez
Mike Rowley

4
UIPropertyMetadata'yı FrameworkPropertyMetadata ile de değiştirebilirsiniz (null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnSelectedItemChanged));
Filimindji

3
Bu sorunu çözmek için bir yaklaşım olacaktır: stackoverflow.com/a/18700099/4227
bitbonk

2
@Lukas tam olarak yukarıdaki XAML kod snippet'inde gösterildiği gibi. Sadece değiştirmek {Binding SelectedItem, Mode=TwoWay}ile{Binding MyViewModelField, Mode=TwoWay}
Steve Greatrex

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


43

İhtiyaç ortaya çıkarsa, ekli özellikler ve harici bağımlılıklar olmadan cevap verin!

Bindable ve alıcı ve ayarlayıcı içeren ekli bir özellik oluşturabilirsiniz:

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

Bu sınıfı içeren ad alanı bildirimini XAML'nize ekleyin ve aşağıdaki gibi bağlayın (yerel ad alanı bildirimini nasıl adlandırdım):

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

    </TreeView>

Artık seçilen öğeyi bağlayabilir ve bu gereksinim ortaya çıktığında onu programlı olarak değiştirmek için görünüm modelinize ayarlayabilirsiniz. Bu, elbette, belirli bir özellik için INotifyPropertyChanged uygulamasını uyguladığınızı varsayar.


4
+1, bu konudaki en iyi cevap imho. System.Windows.Interactivity'ye bağımlılık yoktur ve iki yönlü bağlanmaya izin verir (MVVM ortamında programlı olarak ayarlama). Mükemmel.
Chris Ray

5
Bu yaklaşımla ilgili bir sorun, davranışın yalnızca seçilen öğe bir kez bağlanma yoluyla (yani ViewModel'den) ayarlandıktan sonra çalışmaya başlamasıdır. VM'deki başlangıç ​​değeri null ise, bağlanma DP değerini güncellemez ve davranış etkinleştirilmez. Bunu farklı bir varsayılan seçili öğe (ör. Geçersiz bir öğe) kullanarak düzeltebilirsiniz.
Mark

6
@Mark: Ekli özelliğin UIPropertyMetadata örneğini başlatırken yukarıdaki null yerine yeni object () kullanın. Sorun o zaman gitmiş olmalı ...
barnacleboy

2
DataView tarafından kaynaklardan uygulanan bir HierarchicalDataTemplate kullanıyorum çünkü TreeViewItem döküm benim için başarısız. Ancak ChangeSelectedItem öğesini kaldırırsanız, bir görünüm modeline bağlanma ve öğeyi alma düzgün çalışır.
Casey Sebben

1
Ayrıca TreeViewItem döküm ile ilgili sorunlar yaşıyorum. Bu noktada, ItemContainerGenerator sadece kök öğelere referanslar içerir, ama ben de kök olmayan öğeleri almak için gerekir. Bir tanesine başvuru iletirseniz, yayın başarısız olur ve null değerini döndürür. Bunun nasıl düzeltilebileceğinden emin değil misiniz?
Bob Tway

39

Bir çözüm buldum. Bu karışıklığı taşır, böylece MVVM çalışır.

İlk önce bu sınıfı ekleyin:

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

ve bunu xaml'nize ekleyin:

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

3
Bu benim için şimdiye kadar çalışmaya yaklaşan SADECE şey. Bu çözümü gerçekten çok seviyorum.
Rachael

1
Neden bilmiyorum ama benim için işe yaramadı :( Seçilen öğeyi ağaçtan almayı başardım, tersi değil - seçilen öğeyi ağacın dışından değiştirmek için
Erez

Bağımlılık özelliğini BindsTwoWayByDefault olarak ayarlamak biraz daha temiz olurdu, o zaman XAML'de iki yol belirtmeniz gerekmeyecek
Stephen Holt

Bu en iyi yaklaşım. Etkileşim referansı kullanmaz, arkasında kod kullanmaz, bazı davranışlarda olduğu gibi bir bellek sızıntısı yoktur. Teşekkür ederim.
Alexandru Dicu

Belirtildiği gibi, bu çözüm 2 yollu bağlama ile çalışmaz. Değeri görünüm modelinde ayarlarsanız, değişiklik TreeView öğesine yayılmaz.
Richard Moore

25

OP'nin beklediğinden biraz daha fazla cevap veriyor ... Ama umarım en azından birine yardımcı olabilir.

Bir yürütmek istiyorsanız ICommandzaman SelectedItemdeğişti, bir olay ve bir özelliğin kullanımı ile ilgili bir komut bağlayabilir SelectedItemiçinde ViewModelartık gerekli değildir.

Böyle yaparak:

1- Referans ekleyin System.Windows.Interactivity

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

2- Komutu olaya bağlama 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
Referans NuGet'tenSystem.Windows.Interactivity kurulabilir: nuget.org/packages/System.Windows.Interactivity.WPF
Junle Li

Ben bu sorunları saatlerce çözmeye çalışıyorum, bunu uyguladım ama komutum çalışmıyor, lütfen bana yardımcı olabilir misiniz?
Alfie

1
2018 sonunda Microsoft tarafından WPF için XAML Davranışları tanıtıldı. Yerine kullanılabilir System.Windows.Interactivity. Benim için çalıştı (.NET Core projesiyle denendi). İşleri ayarlamak için Microsoft.Xaml.Behaviors.Wpf nuget paketini eklemeniz yeterlidir, ad alanını değiştirin xmlns:i="http://schemas.microsoft.com/xaml/behaviors". Daha fazla bilgi almak için lütfen bloga
rychlmoj

19

Bu sadece ciltleme ve GalaSoft MVVM Light kütüphanesinin EventToCommand kullanılarak 'daha hoş' bir şekilde gerçekleştirilebilir. VM'nize, seçili öğe değiştirildiğinde çağrılacak bir komut ekleyin ve gerekli eylemi gerçekleştirmek için komutu başlatın. Bu örnekte RelayCommand kullandım ve sadece SelectedCluster özelliğini ayarlayacağım.

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

Sonra xaml içine EventToCommand davranışını ekleyin. Karışımı kullanmak gerçekten çok kolay.

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

Özellikle MvvmLight araç kiti kullanıyorsanız bu güzel bir çözümdür. Bununla birlikte, seçilen düğümü ayarlama sorununu çözmez ve önizlemenin seçimi güncellemesini sağlar.
keft

12

Hepsi karmaşık ... Caliburn Micro ile git (http://caliburnmicro.codeplex.com/)

Görünüm:

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

ViewModel:

public void SetSelectedItem(YourNodeViewModel item) {}; 

5
Evet ... ve TreeView'de SelectedItem'i ayarlayan kısım nerede ?
mnn

Caliburn güzel ve zariftir. İç içe hiyerarşiler için oldukça kolay çalışır
Purusartha

8

Bu sayfaya orijinal yazarla aynı cevabı aradım ve bunu yapmak için her zaman birden fazla yol olduğunu kanıtladım, benim için çözüm, şimdiye kadar burada verilen cevaplardan daha kolaydı, bu yüzden de kazık.

Bağlamanın motivasyonu onu güzel tutmak ve MVVM. ViewModel'in olası kullanımı, "CurrentThingy" gibi bir ada sahip bir özelliğe sahip olmaktır ve başka bir yerde, DataContext başka bir şeye "CurrentThingy" ile bağlıdır.

TreeView'den Modelime ve daha sonra başka bir şeyden Modelime güzel bir bağlamayı desteklemek için gerekli ek adımlardan (örneğin: özel davranış, 3. taraf kontrolü) geçmektense, çözümüm diğer öğeyi bağlayıcı basit öğeyi kullanmaktı TreeView.SelectedItem, başka bir şeyi ViewModel'ime bağlamak yerine, gerekli ekstra işi atlamak yerine.

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

Tabii ki, bu şu anda seçili öğeyi okumak için harika, ama onu ayarlamıyorum, tüm ihtiyacım olan bu.


1
Yerel nedir: MyThingyDetailsView? Bu yerel olsun: MyThingyDetailsView seçilen öğeyi tutar, ancak görünüm modeliniz bu bilgileri nasıl alır? Bu, bunu yapmak için güzel, temiz bir yol gibi görünüyor, ama biraz daha fazla bilgiye ihtiyacım var ...
Bob Horn

local: MyThingyDetailsView, sadece bir "thingy" örneği hakkında ayrıntılar görünümü oluşturan XAML ile dolu bir UserControl. Başka bir görünümün ortasına içerik olarak gömülüdür, bu görünümün DataContext öğesi şu anda seçili olan ağaç görünümü öğesidir ve Öğe bağını kullanır.
Wes

6

Ayrıca TreeViewItem.IsSelected özelliğini de kullanabilirsiniz.


Bence bu doğru cevap olabilir. Ancak Öğelerin IsSelected özelliğinin TreeView'a nasıl aktarıldığına ilişkin bir örnek veya en iyi uygulama önerisi görmek istiyorum.
anhoppe

3

Interaction.Behaviors kullanmadan XAML bindable SelectedItem özelliği oluşturmanın bir yolu da vardır.

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
}

Daha sonra bunu XAML'nizde şu şekilde kullanabilirsiniz:

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

3

Bu soruların tüm çözümlerini denedim. Kimse sorunumu tam olarak çözmedi. Bu yüzden kalıtımsal sınıfı yeniden tanımlanmış SelectedItem özelliği ile kullanmak daha iyi olduğunu düşünüyorum. GUI'den ağaç öğesi seçerseniz ve kodunuzda bu özellik değerini ayarlarsanız mükemmel çalışır

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

Bazı düğümler için UpdateLayout () ve IsExpanded çağrılmamışsa çok daha hızlı olur. UpdateLayout () ve IsExpanded öğelerini çağırmak gerekmediğinde? Ağaç öğesi daha önce ziyaret edildiğinde. Bunu nasıl bilebilirim? ContainerFromItem (), ziyaret edilmeyen düğümler için null değerini döndürür. Bu nedenle üst düğümü yalnızca ContainerFromItem () çocuklar için null döndürdüğünde genişletebiliriz.
CoperNick

3

Benim gereksinim bir TreeView gerekli ve ilişkili nesne Collection <> türünde ve bu nedenle HierarchicalDataTemplate ihtiyacı PRISM-MVVM tabanlı çözüm için oldu. Varsayılan BindableSelectedItemBehavior, TreeViewItem alt öğesini tanımlayamaz. Bu senaryoda çalışmasını sağlamak.

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

Bu, seviyeden bağımsız olarak tüm öğeler arasında yineleme yapılmasını sağlar.


Teşekkür ederim! Senaryonuzdan farklı olmayan senaryom için çalışan tek kişi buydu.
Robert

Çok iyi çalışır ve seçilen / genişletilmiş bağların karışmasına neden olmaz .
Paslı

2

Steve Greatrex tarafından sağlanan davranışa bir ekleme yapmanızı öneririm. Davranışı kaynaktaki değişiklikleri yansıtmaz çünkü TreeViewItems koleksiyonu olmayabilir. Bu nedenle, ağaçta TreeViewItem öğesini hangi datacontext kaynağından seçilenDeğeri bulmak önemlidir. TreeView, TreeViewItem koleksiyonunu tutan "ItemsHost" adlı korumalı bir özelliğe sahiptir. Bunu yansıma yoluyla alabilir ve seçilen öğeyi ararken ağaçta yürüyebiliriz.

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

Bu şekilde davranış iki yönlü bağlamalar için çalışır. Alternatif olarak, ItemsHost alımını Davranışın OnAttached yöntemine taşımak ve her bağlayışında yansıma kullanma yükünü kurtarmak mümkündür.


2

WPF MVVM TreeView SelectedItem

... daha iyi bir yanıttır, ancak ViewModel'de SelectedItem'i almanın / ayarlamanın bir yolundan bahsetmez.

  1. ItemViewModel öğenize bir IsSelected boolean özelliği ekleyin ve bu öğeye TreeViewItem için bir Stil Ayarlayıcı'da bağlanın.
  2. TreeView için DataContext olarak kullanılan ViewModel'e bir SelectedItem özelliği ekleyin. Bu, yukarıdaki çözümde eksik olan parçadır.
    'ItemVM ...
    Kamusal Mal Boolean Olarak Seçildi
        Almak
            Dönüş _func.SelectedNode Benim
        Son Alın
        Set (değer Boole olarak)
            IsSelected değeri O zaman
                _func.SelectedNode = If (değer, Me, Hiçbir şey)
            End If
            RaisePropertyChange ()
        Bitiş Seti
    Son Özellik
    'TreeVM ...
    ÖğeVM Olarak Ortak Mülk Seçildi
        Almak
            Dönüş_selectedItem
        Son Alın
        Ayarla (ItemVM Olarak değer)
            _SelectedItem değerse Öyleyse
                Dönüş
            End If
            Dim prev = _selectedItem
            _selectedItem = değer
            Eğer prev isNot Nothing Öyleyse
                prev.IsSelected = Yanlış
            End If
            _SelectedItem Değilse Hiçbir Şey
                _selectedItem.IsSelected = Doğru
            End If
        Bitiş Seti
    Son Özellik
<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

Bir gün boyunca internet çalıştıktan sonra , normal bir WPF / C # ortamında normal bir ağaç görünümü oluşturduktan sonra bir öğe seçmek için kendi çözümümü buldum

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

Ayrıca TreeView öğesinin IsSelected özelliği kullanılarak da yapılabilir. İşte böyle başardım,

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

Ardından, TreeView öğenizin bağlı olduğu verileri içeren ViewModel'de, TreeViewItem sınıfındaki olaya abone olmanız yeterlidir.

TreeViewItem.OnItemSelected += TreeViewItemSelected;

Ve son olarak, bu işleyiciyi aynı ViewModel'de uygulayın,

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

Ve tabi ki,

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

Bu aslında düşük puan alan bir çözümdür. Düşünme şeklinizi değiştirerek ve her bir treeview öğesinin IsSelected özelliğini bağlayarak ve IsSelected olaylarını köpürterek, iki yönlü bağlamayla iyi çalışan yerleşik işlevsellik kullanabilirsiniz. Bu soruna önerilen birçok çözümü denedim ve bu işe yarayan ilk çözüm. Tel çekmek için biraz karmaşık. Teşekkürler.
Richard Moore

1

Bu konu 10 yaşında olduğunu biliyorum ama sorun hala var ....

Orijinal soru seçilen öğeyi 'almak' idi. Ben de benim görünüm modelinde seçilen öğeyi "almak" gerekiyordu (ayarlamak değil). Bu konudaki tüm cevaplar arasında, 'Wes' tarafından verilen soruna farklı bir şekilde yaklaşan tek cevaptır: 'Seçili Öğeyi' veri tabanı için bir hedef olarak kullanabiliyorsanız, onu veri kaynağı için bir kaynak olarak kullanın. Wes başka bir görünüm özelliği için yaptı, ben bir viewmodel özelliği için yapacağız:

İki şeye ihtiyacımız var:

  • Görünüm modelinde bir bağımlılık özelliği oluşturun (treeview'ım 'MyObject' türünün nesnesine bağlı olduğundan benim durumumda 'MyObject')
  • Treeview.SelectedItem öğesinden Görünüm yapıcısındaki bu özelliğe bağlanın (evet, kodun arkasındadır, ancak büyük olasılıkla veri bağlamınızı da orada başlatabilirsiniz)

ViewModel:

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

Yapıcıyı görüntüle:

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

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

0

(Let sadece hepsi TreeView besbelli. SelectedItem bağlama belirgin olurdu bu soruna ilişkin olarak baskın olduğu konusunda hemfikirdir. Kederlen )

Ben TreeViewItem IsSelected özelliği ile düzgün etkileşim çözümü gerekiyordu, işte nasıl yaptı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;
    }
}

Bu XAML ile:

<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

Size aşağıdaki özellikleri sunan çözümümü getiriyorum:

  • Bağlamanın 2 yolunu destekler

  • TreeViewItem.IsSelected özelliklerini otomatik olarak günceller (SelectedItem'e göre)

  • TreeView alt sınıfı yok

  • ViewModel'e bağlı öğeler herhangi bir türde olabilir (boş olsa bile)

1 / Aşağıdaki kodu CS'nize yapıştırı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 / XAML dosyanızda kullanım örneği

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

0

ViewModel'in seçili öğesini Görünüm'ün seçili öğesinden güncellemek için mükemmel bir şekilde çalışan bu çözümü (en kolay ve bellek sızıntılarını ücretsiz olarak kabul ediyorum) öneriyorum.

Seçilen öğenin ViewModel'den değiştirilmesinin, Görünüm'ün seçilen öğesini güncellemeyeceğini lütfen unutmayın.

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

XAML kullanımı

<l:TreeViewEx ItemsSource="{Binding Path=Items}" SelectedItemEx="{Binding Path=SelectedItem}" >
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.