Liaison de données à SelectedItem dans une arborescence WPF


241

Comment puis-je récupérer l'élément sélectionné dans une arborescence WPF? Je veux le faire en XAML, car je veux le lier.

Vous pourriez penser que c'est le cas SelectedItemmais apparemment cela n'existe pas est en lecture seule et donc inutilisable.

Voici ce que je veux faire:

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

Je veux lier le SelectedItemà une propriété de mon modèle.

Mais cela me donne l'erreur:

La propriété 'SelectedItem' est en lecture seule et ne peut pas être définie à partir du balisage.

Edit: Ok, c'est comme ça que j'ai résolu ceci:

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

et dans le codebehindfile de mon xaml:

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

51
Mec ça craint. Ça m'a juste frappé aussi. Je suis venu ici en espérant trouver une voie décente et je ne suis qu'un idiot. C'est la première fois que je suis triste de ne pas être un idiot ..
Andrei Rînea

6
cela suce vraiment et gâche le concept de reliure
Delta

Espérons que cela pourrait aider quelqu'un à se lier à un élément de l'arborescence sélectionné retour d'appel changé sur ICommand jacobaloysious.wordpress.com/2012/02/19/...
jacob Aloysious

9
En termes de liaison et de MVVM, le code derrière n'est pas "interdit", mais le code derrière devrait plutôt prendre en charge la vue. À mon avis, de toutes les autres solutions que j'ai vues, le code derrière est une bien meilleure option car il s'agit toujours de "lier" la vue au viewmodel. Le seul point négatif est que si vous avez une équipe avec un designer travaillant uniquement en XAML, le code derrière pourrait être cassé / négligé. C'est un petit prix à payer pour une solution qui prend 10 secondes à mettre en œuvre.
nrjohnstone

L'une des solutions les plus faciles probablement: stackoverflow.com/questions/1238304/…
JoanComasFdz

Réponses:


240

Je me rends compte que la réponse a déjà été acceptée, mais je l'ai mise en place pour résoudre le problème. Il utilise une idée similaire à la solution de Delta, mais sans avoir besoin de sous-classer 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;
    }
}

Vous pouvez ensuite l'utiliser dans votre XAML comme:

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

J'espère que cela aidera quelqu'un!


5
Comme l'a souligné Brent, j'avais également besoin d'ajouter Mode = TwoWay à la liaison. Je ne suis pas un "Blender" donc je ne connaissais pas la classe Behavior <> de System.Windows.Interactivity. L'assemblage fait partie de Expression Blend. Pour ceux qui ne veulent pas acheter / installer une version d'essai pour obtenir cet assemblage, vous pouvez télécharger le BlendSDK qui comprend System.Windows.Interactivity. BlendSDK 3 pour 3.5 ... Je pense que c'est BlendSDK 4 pour 4.0. Remarque: Cela vous permet uniquement d'obtenir quel élément est sélectionné, ne vous permet pas de définir l'élément sélectionné
Mike Rowley

4
Vous pouvez également remplacer UIPropertyMetadata par FrameworkPropertyMetadata (null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnSelectedItemChanged));
Filimindji

3
Ce serait une approche pour résoudre le problème: stackoverflow.com/a/18700099/4227
bitbonk

2
@Lukas exactement comme indiqué dans l'extrait de code XAML ci-dessus. Il suffit de remplacer {Binding SelectedItem, Mode=TwoWay}par{Binding MyViewModelField, Mode=TwoWay}
Steve Greatrex

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

46

Cette propriété existe: TreeView.SelectedItem

Mais il est en lecture seule, vous ne pouvez donc pas l'affecter via une liaison, il suffit de le récupérer


J'accepte cette réponse, car là j'ai trouvé ce lien, qui laisse à ma propre réponse: msdn.microsoft.com/en-us/library/ms788714.aspx
Natrium

1
Puis-je avoir cela TreeView.SelectedItemsur une propriété du modèle lorsque l'utilisateur sélectionne un élément (aka OneWayToSource)?
Shimmy Weitzhandler

43

Répondez avec des propriétés attachées et sans dépendances externes, si le besoin se fait sentir!

Vous pouvez créer une propriété attachée qui peut être liée et possède un getter et un 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;
        }
    }
}

Ajoutez la déclaration d'espace de noms contenant cette classe à votre XAML et liez comme suit (local est la façon dont j'ai nommé la déclaration d'espace de noms):

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

    </TreeView>

Vous pouvez désormais lier l'élément sélectionné et le définir également dans votre modèle de vue pour le modifier par programme, si cette exigence devait se produire. Ceci, bien sûr, en supposant que vous implémentez INotifyPropertyChanged sur cette propriété particulière.


4
+1, meilleure réponse dans ce fil à mon humble avis. Aucune dépendance à System.Windows.Interactivity et permet la liaison bidirectionnelle (paramétrage par programme dans un environnement MVVM). Parfait.
Chris Ray

5
Un problème avec cette approche est que le comportement ne commencera à fonctionner qu'une fois que l'élément sélectionné aura été défini une fois via la liaison (c'est-à-dire depuis le ViewModel). Si la valeur initiale dans la machine virtuelle est nulle, la liaison ne mettra pas à jour la valeur DP et le comportement ne sera pas activé. Vous pouvez résoudre ce problème en utilisant un autre élément sélectionné par défaut (par exemple, un élément non valide).
Mark

6
@Mark: utilisez simplement new object () au lieu de la valeur null ci-dessus lors de l'instanciation de l'UIPropertyMetadata de la propriété attachée. Le problème devrait avoir disparu alors ...
barnacleboy

2
La conversion en TreeViewItem échoue pour moi, je suppose, car j'utilise un HierarchicalDataTemplate appliqué à partir des ressources par type de données. Mais si vous supprimez ChangeSelectedItem, la liaison à un modèle de vue et la récupération de l'élément fonctionnent correctement.
Casey Sebben

1
J'ai également des problèmes avec la conversion en TreeViewItem. À ce stade, ItemContainerGenerator ne contient que des références aux éléments racine, mais j'en ai besoin pour pouvoir également obtenir des éléments non root. Si vous passez une référence à un, le transtypage échoue et retourne null. Vous ne savez pas comment cela pourrait être résolu?
Bob Tway

39

Eh bien, j'ai trouvé une solution. Il déplace le désordre, de sorte que MVVM fonctionne.

Ajoutez d'abord cette 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));
}

et ajoutez ceci à votre xaml:

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

3
C'est la SEULE chose qui a failli fonctionner pour moi jusqu'à présent. J'aime vraiment cette solution.
Rachael

1
Je ne sais pas pourquoi mais cela n'a pas fonctionné pour moi :( J'ai réussi à obtenir l'élément sélectionné de l'arborescence mais pas l'inverse - pour modifier l'élément sélectionné de l'extérieur de l'arborescence.
Erez

Il serait un peu plus simple de définir la propriété de dépendance comme BindsTwoWayByDefault, alors vous n'auriez pas besoin de spécifier TwoWay dans le XAML
Stephen Holt

C'est la meilleure approche. Il n'utilise pas de référence d'interactivité, il n'utilise pas de code derrière, il n'a pas de fuite de mémoire comme certains comportements. Je vous remercie.
Alexandru Dicu

Comme mentionné, cette solution ne fonctionne pas avec la liaison bidirectionnelle. Si vous définissez la valeur dans le viewmodel, la modification ne se propage pas à TreeView.
Richard Moore

25

Cela répond un peu plus que ce que le PO attend ... Mais j'espère que cela pourrait aider quelqu'un au moins.

Si vous souhaitez exécuter a ICommandchaque fois que vous le SelectedItemmodifiez, vous pouvez lier une commande à un événement et l'utilisation d'une propriété SelectedItemdans le ViewModeln'est plus nécessaire.

Faire cela:

1- Ajouter une référence à System.Windows.Interactivity

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

2- Lier la commande à l'événement 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
La référence System.Windows.Interactivitypeut être installée à partir de NuGet: nuget.org/packages/System.Windows.Interactivity.WPF
Junle Li

J'essaie de résoudre ce problème depuis des heures, je l'ai implémenté mais ma commande ne fonctionne pas, pourriez-vous m'aider?
Alfie

1
Fin 2018, Microsoft a introduit les comportements XAML pour WPF. Il peut être utilisé à la place de System.Windows.Interactivity. Cela a fonctionné pour moi (essayé avec le projet .NET Core). Pour configurer les choses, ajoutez simplement le package de nuget Microsoft.Xaml.Behaviors.Wpf , changez l'espace de noms en xmlns:i="http://schemas.microsoft.com/xaml/behaviors". Pour obtenir plus d'informations - veuillez consulter le blog
rychlmoj

19

Cela peut être accompli d'une manière «plus agréable» en utilisant uniquement la liaison et EventToCommand de la bibliothèque GalaSoft MVVM Light. Dans votre machine virtuelle, ajoutez une commande qui sera appelée lorsque l'élément sélectionné est modifié et initialisez la commande pour effectuer toute action nécessaire. Dans cet exemple, j'ai utilisé un RelayCommand et je vais juste définir la propriété 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; }
}

Ajoutez ensuite le comportement EventToCommand dans votre xaml. C'est vraiment facile d'utiliser le mélange.

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

C'est une bonne solution, surtout si vous utilisez déjà la boîte à outils MvvmLight. Cela ne résout cependant pas le problème de définition du nœud sélectionné et la mise à jour de la sélection par l'arborescence.
keft

12

Tout est compliqué ... Allez avec Caliburn Micro (http://caliburnmicro.codeplex.com/)

Vue:

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

ViewModel:

public void SetSelectedItem(YourNodeViewModel item) {}; 

5
Oui ... et où est la partie qui définit SelectedItem sur TreeView ?
2013

Caliburn est agréable et élégant. Fonctionne assez facilement pour les hiérarchies imbriquées
Purusartha

8

Je suis tombé sur cette page à la recherche de la même réponse que l'auteur original, et prouvant qu'il y a toujours plus d'une façon de le faire, la solution pour moi était encore plus facile que les réponses fournies jusqu'ici, donc je me suis dit que je pourrais aussi bien ajouter à la pile.

La motivation pour la liaison est de la garder agréable et MVVM. L'utilisation probable du ViewModel est d'avoir une propriété avec un nom tel que "CurrentThingy", et ailleurs, le DataContext sur une autre chose est lié à "CurrentThingy".

Plutôt que de passer par des étapes supplémentaires nécessaires (par exemple: comportement personnalisé, contrôle tiers) pour prendre en charge une liaison agréable de TreeView à mon modèle, puis de quelque chose d'autre à mon modèle, ma solution consistait à utiliser un élément simple liant l'autre chose à TreeView.SelectedItem, plutôt que de lier l'autre chose à mon ViewModel, sautant ainsi le travail supplémentaire requis.

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

Bien sûr, c'est idéal pour lire l'élément actuellement sélectionné, mais pas pour le définir, ce qui est tout ce dont j'avais besoin.


1
Qu'est-ce qui est local: MyThingyDetailsView? Je reçois cette information locale: MyThingyDetailsView contient l'élément sélectionné, mais comment votre modèle de vue obtient-il ces informations? Cela ressemble à une façon agréable et propre de le faire, mais j'ai besoin d'un peu plus d'informations ...
Bob Horn

local: MyThingyDetailsView est simplement un UserControl plein de XAML constituant une vue détaillée d'une instance "thingy". Il est intégré au milieu d'une autre vue en tant que contenu, avec le DataContext de cette vue est l'élément d'arborescence actuellement sélectionné, à l'aide de la liaison d'élément.
Wes

6

Vous pouvez également utiliser la propriété TreeViewItem.IsSelected


Je pense que cela pourrait être la bonne réponse. Mais j'aimerais voir un exemple ou une recommandation de meilleures pratiques sur la façon dont la propriété IsSelected des éléments est transmise à TreeView.
anhoppe

3

Il existe également un moyen de créer la propriété SelectedItem liable XAML sans utiliser 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
}

Vous pouvez ensuite l'utiliser dans votre XAML comme:

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

3

J'ai essayé toutes les solutions de ces questions. Personne n'a résolu mon problème complètement. Je pense donc qu'il est préférable d'utiliser une telle classe héritée avec la propriété redéfinie SelectedItem. Cela fonctionnera parfaitement si vous choisissez l'élément d'arbre dans l'interface graphique et si vous définissez cette valeur de propriété dans votre code

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

Ce serait beaucoup plus rapide si UpdateLayout () et IsExpanded n'étaient pas appelés pour certains nœuds. Lorsqu'il n'est pas nécessaire d'appeler UpdateLayout () et IsExpanded? Quand l'élément d'arbre a été visité précédemment. Comment savoir ça? ContainerFromItem () renvoie null pour les nœuds non visités. Nous pouvons donc développer le nœud parent uniquement lorsque ContainerFromItem () renvoie null pour les enfants.
CoperNick

3

J'avais besoin d'une solution basée sur PRISM-MVVM où un TreeView était nécessaire et l'objet lié est de type Collection <> et a donc besoin de HierarchicalDataTemplate. Le BindableSelectedItemBehavior par défaut ne pourra pas identifier le TreeViewItem enfant. Pour le faire fonctionner dans ce scénario.

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

Cela permet de parcourir tous les éléments quel que soit le niveau.


Je vous remercie! Ce fut le seul qui fonctionne pour mon scénario qui n'est pas différent du vôtre.
Robert

Fonctionne très bien et ne provoque pas de confusion dans les liaisons sélectionnées / développées .
Rusty

2

Je suggère un ajout au comportement fourni par Steve Greatrex. Son comportement ne reflète pas les modifications de la source car il ne s'agit peut-être pas d'une collection de TreeViewItems. Il s'agit donc de trouver le TreeViewItem dans l'arborescence dont le datacontext est le selectedValue de la source. TreeView possède une propriété protégée appelée "ItemsHost", qui contient la collection TreeViewItem. Nous pouvons l'obtenir à travers la réflexion et marcher dans l'arbre à la recherche de l'élément sélectionné.

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

De cette façon, le comportement fonctionne pour les liaisons bidirectionnelles. Il est également possible de déplacer l'acquisition ItemsHost vers la méthode OnAttached du comportement, ce qui évite les frais généraux liés à l'utilisation de la réflexion à chaque mise à jour de la liaison.


2

WPF MVVM TreeView SelectedItem

... est une meilleure réponse, mais ne mentionne pas un moyen d'obtenir / définir le SelectedItem dans le ViewModel.

  1. Ajoutez une propriété booléenne IsSelected à votre ItemViewModel et liez-la dans un sélecteur de style pour TreeViewItem.
  2. Ajoutez une propriété SelectedItem à votre ViewModel utilisé comme DataContext pour TreeView. C'est la pièce manquante dans la solution ci-dessus.
    «ItemVM ...
    La propriété publique est sélectionnée comme booléenne
        Avoir
            Renvoie _func.SelectedNode Is Me
        Fin Get
        Set (valeur As Boolean)
            Si la valeur IsSelected Alors
                _func.SelectedNode = If (valeur, moi, rien)
            Fin si
            RaisePropertyChange ()
        Ensemble final
    Propriété de fin
    'TreeVM ...
    Propriété publique SelectedItem As ItemVM
        Avoir
            Renvoyer _selectedItem
        Fin Get
        Set (valeur As ItemVM)
            Si _selectedItem est une valeur Then
                Revenir
            Fin si
            Dim prev = _selectedItem
            _selectedItem = valeur
            If prev IsNot Nothing Then
                prev.IsSelected = False
            Fin si
            Si _selectedItem n'est pas rien alors
                _selectedItem.IsSelected = True
            Fin si
        Ensemble final
    Propriété de fin
<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

Après avoir étudié Internet pendant une journée, j'ai trouvé ma propre solution pour sélectionner un élément après avoir créé une arborescence normale dans un environnement WPF / C # 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

Cela peut également être fait en utilisant la propriété IsSelected de l'élément TreeView. Voici comment je l'ai géré,

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

Ensuite, dans le ViewModel qui contient les données auxquelles votre TreeView est lié, abonnez-vous simplement à l'événement dans la classe TreeViewItem.

TreeViewItem.OnItemSelected += TreeViewItemSelected;

Et enfin, implémentez ce gestionnaire dans le même ViewModel,

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

Et la reliure bien sûr,

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

Il s'agit en fait d'une solution sous-évaluée. En modifiant votre façon de penser et de lier la propriété IsSelected de chaque élément de l'arborescence, et en agrandissant les événements IsSelected, vous pouvez utiliser des fonctionnalités intégrées qui fonctionnent bien avec la liaison bidirectionnelle. J'ai essayé de nombreuses solutions proposées à ce problème, et c'est la première qui a fonctionné. Juste un peu complexe à câbler. Merci.
Richard Moore

1

Je sais que ce fil a 10 ans mais le problème existe toujours ....

La question d'origine était de «récupérer» l'élément sélectionné. J'avais également besoin de «récupérer» l'élément sélectionné dans mon modèle de vue (pas de le définir). De toutes les réponses de ce fil, celle de «Wes» est la seule qui aborde le problème différemment: si vous pouvez utiliser «l'élément sélectionné» comme cible pour la liaison de données, utilisez-le comme source pour la liaison de données. Wes l'a fait dans une autre propriété view, je vais le faire dans une propriété viewmodel:

Nous avons besoin de deux choses:

  • Créer une propriété de dépendance dans le viewmodel (dans mon cas de type 'MyObject' car mon treeview est lié à un objet de type 'MyObject')
  • Lier à partir de Treeview.SelectedItem à cette propriété dans le constructeur de la vue (oui, c'est du code derrière, mais il est probable que vous y initierez également votre contexte de données)

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

Constructeur de vue:

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

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

0

(Soyons juste tous d'accord pour dire que TreeView est évidemment cassé en ce qui concerne ce problème. La liaison à SelectedItem aurait été évidente. Soupir )

J'avais besoin de la solution pour interagir correctement avec la propriété IsSelected de TreeViewItem, alors voici comment je l'ai fait:

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

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

Je vous apporte ma solution qui offre les fonctionnalités suivantes:

  • Prend en charge la liaison 2 voies

  • Met à jour automatiquement les propriétés TreeViewItem.IsSelected (selon le SelectedItem)

  • Pas de sous-classement TreeView

  • Les éléments liés à ViewModel peuvent être de tout type (même nul)

1 / Collez le code suivant dans votre 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 / Exemple d'utilisation dans votre fichier XAML

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

0

Je propose cette solution (que je considère comme la plus simple et sans fuite de mémoire) qui fonctionne parfaitement pour mettre à jour l'élément sélectionné de ViewModel à partir de l'élément sélectionné de View.

Veuillez noter que la modification de l'élément sélectionné dans le ViewModel ne mettra pas à jour l'élément sélectionné de la vue.

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

Utilisation de XAML

<l:TreeViewEx ItemsSource="{Binding Path=Items}" SelectedItemEx="{Binding Path=SelectedItem}" >
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.