Dati associati a SelectedItem in un Treeview WPF


241

Come posso recuperare l'elemento selezionato in un treeview WPF? Voglio farlo in XAML, perché voglio legarlo.

Potresti pensare che sia, SelectedItemma apparentemente che non esiste è di sola lettura e quindi inutilizzabile.

Questo è quello che voglio fare:

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

Voglio associare SelectedItema una proprietà sul mio modello.

Ma questo mi dà l'errore:

La proprietà 'SelectedItem' è di sola lettura e non può essere impostata dal markup.

Modifica: Ok, questo è il modo in cui ho risolto questo:

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

e nel codebehindfile del mio xaml:

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

51
Amico, questo fa schifo. Mi ha colpito anche solo. Sono venuto qui sperando di scoprire che esiste un modo decente e sono solo un idiota. Questa è la prima volta che sono triste di non essere un idiota ..
Andrei Rînea

6
questo fa davvero schifo e rovina il concetto di rilegatura
Delta

Spero che questo possa aiutare qualcuno a legarsi a un elemento della visualizzazione ad albero selezionato modificato richiamato su Icommand jacobaloysious.wordpress.com/2012/02/19/…
jacob aloysious

9
In termini di associazione e MVVM, il codice dietro non è "vietato", ma il codice dietro dovrebbe supportare la vista. A mio avviso da tutte le altre soluzioni che ho visto, il codice dietro è un'opzione di gran lunga migliore poiché si tratta ancora di "vincolare" la vista al modello di visualizzazione. L'unico aspetto negativo è che se hai un team con un designer che lavora solo in XAML, il codice dietro potrebbe essere rotto / trascurato. È un piccolo prezzo da pagare per una soluzione che richiede 10 secondi per essere implementata.
nrjohnstone,

Una delle soluzioni più semplici probabilmente: stackoverflow.com/questions/1238304/...
JoanComasFdz

Risposte:


240

Mi rendo conto che è già stata accettata una risposta, ma l'ho messa insieme per risolvere il problema. Utilizza un'idea simile alla soluzione Delta, ma senza la necessità di sottoclassare 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;
    }
}

È quindi possibile utilizzare questo nel tuo XAML come:

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

Spero che possa aiutare qualcuno!


5
Come ha sottolineato Brent, ho anche dovuto aggiungere Mode = TwoWay all'associazione. Non sono un "Blender", quindi non avevo familiarità con la classe Behavior <> di System.Windows.Interactivity. L'assemblaggio fa parte di Expression Blend. Per coloro che non vogliono acquistare / installare la versione di prova per ottenere questo assembly, è possibile scaricare BlendSDK che include System.Windows.Interactivity. BlendSDK 3 per 3.5 ... Penso che sia BlendSDK 4 per 4.0. Nota: questo ti consente solo di ottenere quale elemento è selezionato, non ti consente di impostare l'elemento selezionato
Mike Rowley,

4
Puoi anche sostituire UIPropertyMetadata con FrameworkPropertyMetadata (null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnSelectedItemChanged));
Filimindji,

3
Questo sarebbe un approccio per risolvere il problema: stackoverflow.com/a/18700099/4227
bitbonk

2
@Lukas esattamente come mostrato nello snippet di codice XAML sopra. Basta sostituire {Binding SelectedItem, Mode=TwoWay}con{Binding MyViewModelField, Mode=TwoWay}
Steve Greatrex l'

4
@Pascal èxmlns:e="http://schemas.microsoft.com/expression/2010/interactivity"
Steve Greatrex l'

46

Questa proprietà esiste: TreeView.SelectedItem

Ma è di sola lettura, quindi non è possibile assegnarlo tramite un'associazione, solo recuperarlo


Accetto questa risposta, perché lì ho trovato questo link, che ha lasciato alla mia risposta: msdn.microsoft.com/en-us/library/ms788714.aspx
Natrium

1
Quindi posso avere questo TreeView.SelectedItemeffetto su una proprietà sul modello quando l'utente seleziona un oggetto (aka OneWayToSource)?
Shimmy Weitzhandler,

43

Rispondi con le proprietà associate e senza dipendenze esterne, se mai dovesse sorgere la necessità!

È possibile creare una proprietà collegata che è associabile e ha un getter e 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;
        }
    }
}

Aggiungi la dichiarazione dello spazio dei nomi contenente quella classe al tuo XAML e associa come segue (locale è come ho chiamato la dichiarazione dello spazio dei nomi):

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

    </TreeView>

Ora è possibile associare l'elemento selezionato e anche impostarlo nel modello di visualizzazione per modificarlo a livello di codice, qualora tale requisito dovesse sorgere. Questo ovviamente presuppone che INotifyPropertyChanged sia implementato su quella particolare proprietà.


4
+1, la migliore risposta in questa discussione. Nessuna dipendenza da System.Windows.Interactivity e consente l'associazione bidirezionale (impostazione a livello di programmazione in un ambiente MVVM). Perfetto.
Chris Ray,

5
Un problema con questo approccio è che il comportamento inizierà a funzionare solo dopo che l'elemento selezionato è stato impostato una volta tramite l'associazione (ovvero dal ViewModel). Se il valore iniziale nella VM è nullo, l'associazione non aggiornerà il valore DP e il comportamento non verrà attivato. È possibile risolvere questo problema utilizzando un altro elemento predefinito selezionato (ad esempio un articolo non valido).
Segna il

6
@Mark: utilizzare semplicemente new object () anziché il null sopra quando si crea un'istanza di UIPropertyMetadata della proprietà collegata. Il problema dovrebbe essere risolto, allora ...
Barnacleboy,

2
Il cast su TreeViewItem non riesce per me suppongo perché sto usando un HierarchicalDataTemplate applicato dalle risorse per tipo di dati. Ma se rimuovi ChangeSelectedItem, il collegamento a un modello di visualizzazione e il recupero dell'elemento funziona correttamente.
Casey Sebben,

1
Ho anche problemi con il cast di TreeViewItem. A quel punto, ItemContainerGenerator contiene solo riferimenti agli elementi radice, ma ho bisogno che sia in grado di ottenere anche elementi non root. Se si passa un riferimento a uno, il cast ha esito negativo e restituisce null. Non sei sicuro di come risolvere questo problema?
Bob Tway,

39

Bene, ho trovato una soluzione. Sposta il casino, in modo che MVVM funzioni.

Per prima cosa aggiungi questa classe:

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

e aggiungilo al tuo xaml:

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

3
Questa è l'unica cosa che mi ha avvicinato al lavoro finora. Mi piace molto questa soluzione.
Rachael

1
Non so perché ma non ha funzionato per me :( Sono riuscito a ottenere l'oggetto selezionato dall'albero ma non viceversa - per cambiare l'elemento selezionato dall'esterno dell'albero.
Erez

Sarebbe leggermente più ordinato impostare la proprietà di dipendenza come BindsTwoWayByDefault, quindi non sarà necessario specificare TwoWay in XAML
Stephen Holt,

Questo è l'approccio migliore. Non usa il riferimento di interattività, non usa il codice dietro, non ha una perdita di memoria come hanno alcuni comportamenti. Grazie.
Alexandru Dicu,

Come accennato, questa soluzione non funziona con l'associazione a 2 vie. Se si imposta il valore nel viewmodel, la modifica non si propaga a TreeView.
Richard Moore,

25

Risponde un po 'più di quanto si aspetti l'OP ... Ma spero che possa aiutare qualcuno almeno.

Se si desidera eseguire un ICommandogni volta che viene SelectedItemmodificato, è possibile associare un comando a un evento e l'uso di una proprietà SelectedItemin ViewModelnon è più necessario.

Fare così:

1- Aggiungi riferimento a System.Windows.Interactivity

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

2- Associare il comando all'evento 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
Il riferimento System.Windows.Interactivitypuò essere installato da NuGet: nuget.org/packages/System.Windows.Interactivity.WPF
Li

Ho provato a risolvere questo problema per ore, l'ho implementato ma il mio comando non funziona, per favore potresti aiutarmi?
Alfie,

1
Alla fine del 2018 sono stati introdotti XAML Behaviors for WPF da parte di Microsoft. Può essere utilizzato al posto di System.Windows.Interactivity. Ha funzionato per me (provato con il progetto .NET Core). Per impostare le cose, basta aggiungere il pacchetto nuget Microsoft.Xaml.Behaviors.Wpf , cambiare lo spazio dei nomi in xmlns:i="http://schemas.microsoft.com/xaml/behaviors". Per ulteriori informazioni, consultare il blog
rychlmoj,

19

Ciò può essere realizzato in modo "più gradevole" utilizzando solo il binding e il EventToCommand della libreria GalaSoft MVVM Light. Nella tua macchina virtuale aggiungi un comando che verrà chiamato quando l'elemento selezionato viene modificato e inizializza il comando per eseguire qualsiasi azione sia necessaria. In questo esempio ho usato un RelayCommand e imposterò la proprietà 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; }
}

Quindi aggiungi il comportamento EventToCommand nel tuo xaml. Questo è davvero facile usando la fusione.

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

Questa è una buona soluzione soprattutto se stai già utilizzando MvvmLight toolkit. Tuttavia, non risolve il problema di impostazione del nodo selezionato e il treeview aggiorna la selezione.
keft

12

Tutto complicato ... Vai con Caliburn Micro (http://caliburnmicro.codeplex.com/)

Visualizza:

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

ViewModel:

public void SetSelectedItem(YourNodeViewModel item) {}; 

5
Sì ... e dov'è la parte che imposta SelectedItem su TreeView ?
mnn

Caliburn è carino ed elegante. Funziona abbastanza facilmente per le gerarchie nidificate
Purusartha,

8

Mi sono imbattuto in questa pagina cercando la stessa risposta dell'autore originale e dimostrando che esiste sempre più di un modo per farlo, la soluzione per me è stata persino più semplice delle risposte fornite finora, quindi ho pensato che avrei potuto aggiungere alla pila.

La motivazione per l'associazione è di mantenerlo piacevole e MVVM. Il probabile utilizzo di ViewModel è avere una proprietà con un nome come "CurrentThingy", e da qualche altra parte, DataContext su qualcos'altro è legato a "CurrentThingy".

Piuttosto che passare attraverso ulteriori passaggi necessari (ad esempio: comportamento personalizzato, controllo di terze parti) per supportare un bel legame da TreeView al mio modello, e poi da qualcos'altro al mio modello, la mia soluzione era quella di utilizzare un semplice elemento che lega l'altro elemento a TreeView.SelectedItem, piuttosto che associare l'altra cosa al mio ViewModel, saltando così il lavoro extra richiesto.

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

Naturalmente, questo è ottimo per leggere l'elemento attualmente selezionato, ma non per impostarlo, che è tutto ciò di cui avevo bisogno.


1
Cosa è locale: MyThingyDetailsView? Ottengo quel locale: MyThingyDetailsView contiene l'elemento selezionato, ma come fa il tuo modello di visualizzazione a ottenere queste informazioni? Sembra un modo carino e pulito per farlo, ma ho bisogno solo di qualche informazione in più ...
Bob Horn

local: MyThingyDetailsView è semplicemente un UserControl pieno di XAML che crea una vista dei dettagli su un'istanza "cosa". È incorporato nel mezzo di un'altra vista come contenuto, con DataContext di questa vista è l'elemento della vista ad albero attualmente selezionato, usando l'associazione elemento.
Wes,

6

Potresti anche essere in grado di utilizzare la proprietà TreeViewItem.IsSelected


Penso che questa potrebbe essere la risposta corretta. Ma vorrei vedere un esempio o una raccomandazione delle migliori pratiche su come la proprietà IsSelected degli Articoli viene passata a TreeView.
anhoppe,

3

C'è anche un modo per creare la proprietà SelectedItem associabile XAML senza usare Interaction.Behaviors.

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
}

È quindi possibile utilizzare questo nel tuo XAML come:

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

3

Ho provato tutte le soluzioni di questa domanda. Nessuno ha risolto completamente il mio problema. Quindi penso che sia meglio usare tale classe ereditata con la proprietà ridefinita SelectedItem. Funzionerà perfettamente se scegli l'elemento tree dalla GUI e se imposti questo valore di proprietà nel tuo codice

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

Sarebbe molto più veloce se UpdateLayout () e IsExpanded non fossero chiamati per alcuni nodi. Quando non è necessario chiamare UpdateLayout () ed IsExpanded? Quando l'oggetto dell'albero è stato visitato in precedenza. Come saperlo? ContainerFromItem () restituisce null per i nodi non visitati. Quindi possiamo espandere il nodo padre solo quando ContainerFromItem () restituisce null per i bambini.
CoperNick,

3

Il mio requisito era per la soluzione basata su PRISM-MVVM in cui era necessario un TreeView e l'oggetto associato è del tipo Collection <> e quindi ha bisogno di HierarchicalDataTemplate. Il BindableSelectedItemBehavior predefinito non sarà in grado di identificare TreeViewItem figlio. Per farlo funzionare in questo scenario.

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

Ciò consente di scorrere tutti gli elementi indipendentemente dal livello.


Grazie! Questo è stato l'unico che funziona per il mio scenario che non è diverso dal tuo.
Robert,

Funziona molto bene e non fa confondere le associazioni selezionate / espanse .
Rusty

2

Suggerisco un'aggiunta al comportamento fornito da Steve Greatrex. Il suo comportamento non riflette le modifiche dall'origine perché potrebbe non essere una raccolta di TreeViewItems. Quindi si tratta di trovare TreeViewItem nell'albero quale datacontext è il valore selezionato dalla fonte. TreeView ha una proprietà protetta denominata "ItemsHost", che contiene la raccolta TreeViewItem. Possiamo ottenere attraverso la riflessione e camminare sull'albero alla ricerca dell'elemento selezionato.

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

In questo modo il comportamento funziona per i collegamenti bidirezionali. In alternativa, è possibile spostare l'acquisizione ItemsHost sul metodo OnAttached del comportamento, risparmiando il sovraccarico dell'uso della riflessione ogni volta che si aggiorna l'associazione.


2

WPF MVVM TreeView SelectedItem

... è una risposta migliore, ma non menziona un modo per ottenere / impostare SelectedItem in ViewModel.

  1. Aggiungi una proprietà booleana IsSelected a ItemViewModel e associala ad un setter di stile per TreeViewItem.
  2. Aggiungi una proprietà SelectedItem a ViewModel utilizzata come DataContext per TreeView. Questo è il pezzo mancante nella soluzione sopra.
    'ItemVM ...
    La proprietà pubblica è selezionata come booleana
        Ottenere
            Restituisce _func.SelectedNode Is Me
        End Get
        Imposta (valore come booleano)
            Se valore IsSelected Quindi
                _func.SelectedNode = If (value, Me, Nothing)
            Finisci se
            RaisePropertyChange ()
        Set finale
    Proprietà finale
    'TreeVM ...
    Proprietà pubblica SelectedItem come ItemVM
        Ottenere
            Restituisce _selectedItem
        End Get
        Imposta (valore come ItemVM)
            Se _selectedItem è valore Quindi
                Ritorno
            Finisci se
            Dim prev = _selectedItem
            _selectedItem = value
            Se prev IsNot Nothing Quindi
                prev.IsSelected = False
            Finisci se
            Se _selectedItem non è niente, allora
                _selectedItem.IsSelected = True
            Finisci se
        Set finale
    Proprietà finale
<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

Dopo aver studiato Internet per un giorno, ho trovato la mia soluzione per selezionare un elemento dopo aver creato un normale treeview in un normale ambiente WPF / C #

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

Può anche essere fatto usando la proprietà IsSelected dell'elemento TreeView. Ecco come l'ho gestito,

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

Quindi nel ViewModel che contiene i dati a cui è associato TreeView, basta iscriversi all'evento nella classe TreeViewItem.

TreeViewItem.OnItemSelected += TreeViewItemSelected;

E infine, implementa questo gestore nello stesso ViewModel,

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

E il legame ovviamente,

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

Questa è in realtà una soluzione sottovalutata. Modificando il tuo modo di pensare e vincolando la proprietà IsSelected di ciascun elemento treeview e diffondendo gli eventi IsSelected, puoi utilizzare la funzionalità integrata che funziona bene con l'associazione bidirezionale. Ho provato molte soluzioni proposte a questo problema, e questo è il primo che ha funzionato. Solo un po 'complesso da collegare. Grazie.
Richard Moore,

1

So che questa discussione ha 10 anni ma il problema esiste ancora ....

La domanda originale era "recuperare" l'elemento selezionato. Avevo anche bisogno di "ottenere" l'elemento selezionato nel mio viewmodel (non impostarlo). Di tutte le risposte in questo thread, quella di "Wes" è l'unica che affronta il problema in modo diverso: se puoi usare "Item selezionato" come target per il database, usalo come sorgente per il database. Lo abbiamo fatto in un'altra proprietà della vista, lo farò in una proprietà del modello vista:

Abbiamo bisogno di due cose:

  • Creare una proprietà di dipendenza nel viewmodel (nel mio caso di tipo "MyObject" poiché il mio treeview è associato all'oggetto del tipo "MyObject")
  • Associa da Treeview.SelectedItem a questa proprietà nel costruttore della vista (sì, è il codice dietro ma, è probabile che inizierai il tuo datacontext anche lì)

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

Visualizza costruttore:

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

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

0

(Siamo tutti d'accordo sul fatto che TreeView è ovviamente sballato rispetto a questo problema. Il binding a SelectedItem sarebbe stato ovvio. Sigh )

Avevo bisogno della soluzione per interagire correttamente con la proprietà IsSelected di TreeViewItem, quindi ecco come l'ho fatto:

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

Con questo XAML:

<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

Ti porto la mia soluzione che offre le seguenti funzionalità:

  • Supporta l'associazione in 2 modi

  • Aggiorna automaticamente le proprietà TreeViewItem.IsSelected (in base a SelectedItem)

  • Nessuna sottoclasse di TreeView

  • Gli elementi associati a ViewModel possono essere di qualsiasi tipo (anche null)

1 / Incolla il seguente codice nel tuo CS:

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 / Esempio di utilizzo nel tuo file XAML

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

0

Propongo questa soluzione (che considero la più semplice e senza perdite di memoria) che funziona perfettamente per aggiornare l'elemento selezionato del ViewModel dall'elemento selezionato della vista.

Si noti che la modifica dell'elemento selezionato da ViewModel non aggiorna l'elemento selezionato della vista.

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

Utilizzo XAML

<l:TreeViewEx ItemsSource="{Binding Path=Items}" SelectedItemEx="{Binding Path=SelectedItem}" >
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.