Enlace de datos a SelectedItem en una vista de árbol de WPF


241

¿Cómo puedo recuperar el elemento seleccionado en una vista de árbol WPF? Quiero hacer esto en XAML, porque quiero vincularlo.

Puede pensar que es, SelectedItempero aparentemente eso no existe, es de solo lectura y, por lo tanto, inutilizable.

Esto es lo que quiero hacer:

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

Quiero vincular SelectedItema una propiedad en mi modelo.

Pero esto me da el error:

La propiedad 'SelectedItem' es de solo lectura y no se puede establecer desde el marcado.

Editar: Ok, esta es la forma en que resolví esto:

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

y en el código detrás del archivo de mi xaml:

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

51
Hombre, esto apesta. También me golpeó a mí también. Vine aquí con la esperanza de encontrar que hay una manera decente y que soy un idiota. Esta es la primera vez que estoy triste porque no soy un idiota ..
Andrei Rînea

66
esto realmente apesta y arruina el concepto vinculante
Delta

Espero que esto pueda ayudar a alguien a vincularse a un elemento de vista de árbol seleccionado. Cambie la llamada en Icommand jacobaloysious.wordpress.com/2012/02/19/…
jacob aloysious

99
En términos de enlace y MVVM, el código subyacente no está "prohibido", sino que el código subyacente debería admitir la vista. En mi opinión de todas las otras soluciones que he visto, el código subyacente es una opción mucho mejor, ya que todavía se trata de "vincular" la vista al modelo de vista. Lo único negativo es que si tienes un equipo con un diseñador trabajando solo en XAML, el código podría romperse / descuidarse. Es un pequeño precio a pagar por una solución que tarda 10 segundos en implementarse.
nrjohnstone

Una de las soluciones más fáciles probablemente: stackoverflow.com/questions/1238304/…
JoanComasFdz

Respuestas:


240

Me doy cuenta de que ya se ha aceptado una respuesta, pero la armé para resolver el problema. Utiliza una idea similar a la solución de Delta, pero sin la necesidad de subclasificar 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;
    }
}

Luego puede usar esto en su XAML como:

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

¡Ojalá ayude a alguien!


55
Como Brent señaló, también necesitaba agregar Mode = TwoWay al enlace. No soy un "Blender", así que no estaba familiarizado con la clase Behavior <> de System.Windows.Interactivity. El ensamblaje es parte de Expression Blend. Para aquellos que no desean comprar / instalar la versión de prueba para obtener este ensamblaje, pueden descargar el BlendSDK que incluye System.Windows.Interactivity. BlendSDK 3 para 3.5 ... Creo que es BlendSDK 4 para 4.0. Nota: Esto solo le permite obtener el elemento seleccionado, no le permite establecer el elemento seleccionado
Mike Rowley

44
También puede reemplazar UIPropertyMetadata por FrameworkPropertyMetadata (nulo, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnSelectedItemChanged));
Filimindji

3
Este sería un enfoque para resolver el problema: stackoverflow.com/a/18700099/4227
bitbonk

2
@Lukas exactamente como se muestra en el fragmento de código XAML anterior. Simplemente reemplace {Binding SelectedItem, Mode=TwoWay}con{Binding MyViewModelField, Mode=TwoWay}
Steve Greatrex

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

46

Esta propiedad existe: TreeView.SelectedItem

Pero es de solo lectura, por lo que no puede asignarlo a través de un enlace, solo recuperarlo


Acepto esta respuesta, porque allí encontré este enlace, que dejó mi propia respuesta: msdn.microsoft.com/en-us/library/ms788714.aspx
Natrium

1
Entonces, ¿puedo hacer que esto TreeView.SelectedItemafecte una propiedad en el modelo cuando el usuario selecciona un elemento (también conocido como OneWayToSource)?
Shimmy Weitzhandler

43

Responda con propiedades adjuntas y sin dependencias externas, en caso de que surja la necesidad.

Puede crear una propiedad adjunta que sea enlazable y tenga un captador y definidor:

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

Agregue la declaración de espacio de nombres que contiene esa clase a su XAML y enlace de la siguiente manera (local es como denominé la declaración de espacio de nombres):

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

    </TreeView>

Ahora puede vincular el elemento seleccionado y también configurarlo en su modelo de vista para cambiarlo programáticamente, en caso de que surja ese requisito. Esto es, por supuesto, suponiendo que implemente INotifyPropertyChanged en esa propiedad en particular.


44
+1, la mejor respuesta en este hilo en mi humilde opinión. No depende de System.Windows.Interactivity, y permite el enlace bidireccional (configuración programática en un entorno MVVM). Perfecto.
Chris Ray

55
Un problema con este enfoque es que el comportamiento solo comenzará a funcionar una vez que el elemento seleccionado se haya configurado una vez a través del enlace (es decir, desde ViewModel). Si el valor inicial en la VM es nulo, el enlace no actualizará el valor DP y el comportamiento no se activará. Puede solucionar esto utilizando un elemento seleccionado predeterminado diferente (por ejemplo, un elemento no válido).
Mark

66
@Mark: Simplemente use un nuevo objeto () en lugar del nulo anterior al crear una instancia de UIPropertyMetadata de la propiedad adjunta. El problema debería desaparecer entonces ...
barnacleboy

2
Supongo que la conversión a TreeViewItem falla porque supongo que estoy usando un HierarchicalDataTemplate aplicado desde los recursos por tipo de datos. Pero si elimina ChangeSelectedItem, vincular a un modelo de vista y recuperar el elemento funciona bien.
Casey Sebben

1
También tengo problemas con el reparto a TreeViewItem. En ese momento, ItemContainerGenerator solo contiene referencias a los elementos raíz, pero necesito que también pueda obtener elementos no raíz. Si pasa una referencia a uno, el lanzamiento falla y devuelve nulo. ¿No está seguro de cómo se puede solucionar esto?
Bob Tway

39

Bueno, encontré una solución. Mueve el desorden, por lo que MVVM funciona.

Primero agregue esta clase:

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

y agregue esto a su xaml:

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

3
Esto es lo ÚNICO que me ha funcionado hasta ahora. Realmente me gusta esta solución.
Rachael

1
No sé por qué, pero no funcionó para mí :( Logré obtener el elemento seleccionado del árbol pero no al revés - para cambiar el elemento seleccionado desde fuera del árbol.
Erez

Sería un poco más ordenado establecer la propiedad de dependencia como BindsTwoWayByDefault, entonces no necesitaría especificar TwoWay en el XAML
Stephen Holt

Este es el mejor enfoque. No usa la referencia de interactividad, no usa código detrás, no tiene una pérdida de memoria como tienen algunos comportamientos. Gracias.
Alexandru Dicu

Como se mencionó, esta solución no funciona con enlace bidireccional. Si establece el valor en el modelo de vista, el cambio no se propaga a TreeView.
Richard Moore

25

Responde un poco más de lo que espera el OP ... Pero espero que al menos pueda ayudar a alguien.

Si desea ejecutar un comando ICommandcada vez que se SelectedItemmodifica, puede vincular un comando en un evento y ya no es necesario usar una propiedad SelectedItemen elViewModel

Para hacerlo:

1- Añadir referencia a System.Windows.Interactivity

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

2- Vincula el comando al 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
La referencia System.Windows.Interactivityse puede instalar desde NuGet: nuget.org/packages/System.Windows.Interactivity.WPF
Junle Li

He estado tratando de resolver este problema durante horas, lo he implementado pero mi comando no funciona, ¿podrían ayudarme?
Alfie

1
Microsoft introdujo los comportamientos XAML para WPF a fines de 2018. Se puede usar en lugar de System.Windows.Interactivity. Me funcionó (probé con el proyecto .NET Core). Para configurar las cosas, simplemente agregue el paquete nuget Microsoft.Xaml.Behaviors.Wpf , cambie el espacio de nombres a xmlns:i="http://schemas.microsoft.com/xaml/behaviors". Para obtener más información, consulte el blog
rychlmoj

19

Esto se puede lograr de una manera 'más agradable' usando solo el enlace y EventToCommand de la biblioteca de GalaSoft MVVM Light. En su VM, agregue un comando que se llamará cuando se cambie el elemento seleccionado, e inicialice el comando para realizar cualquier acción que sea necesaria. En este ejemplo, utilicé un RelayCommand y solo estableceré la propiedad 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; }
}

Luego agregue el comportamiento EventToCommand en su xaml. Esto es realmente fácil usando la mezcla.

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

Esta es una buena solución, especialmente si ya está utilizando el kit de herramientas MvvmLight. Sin embargo, no resuelve el problema de configurar el nodo seleccionado y hacer que la vista de árbol actualice la selección.
keft

12

Todo complicado ... Ve con Caliburn Micro (http://caliburnmicro.codeplex.com/)

Ver:

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

ViewModel:

public void SetSelectedItem(YourNodeViewModel item) {}; 

55
Sí ... y ¿dónde está la parte que establece SelectedItem en TreeView ?
Mnn

Caliburn es agradable y elegante. Funciona con bastante facilidad para jerarquías anidadas
Purusartha

8

Encontré esta página buscando la misma respuesta que el autor original, y demostrando que siempre hay más de una forma de hacerlo, la solución para mí fue aún más fácil que las respuestas proporcionadas aquí hasta ahora, así que pensé que también podría agregar a la pila

La motivación para el enlace es mantenerlo agradable y MVVM. El uso probable de ViewModel es tener una propiedad con un nombre como "CurrentThingy", y en otro lugar, el DataContext en otra cosa está vinculado a "CurrentThingy".

En lugar de seguir los pasos adicionales requeridos (por ejemplo: comportamiento personalizado, control de terceros) para admitir un enlace agradable desde TreeView a mi Modelo, y luego de otra cosa a mi Modelo, mi solución fue usar el enlace de Elemento simple. TreeView.SelectedItem, en lugar de vincular la otra cosa a mi ViewModel, omitiendo así el trabajo adicional requerido.

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

Por supuesto, esto es excelente para leer el elemento seleccionado actualmente, pero no para configurarlo, que es todo lo que necesitaba.


1
¿Qué es local: MyThingyDetailsView? Obtengo ese local: MyThingyDetailsView contiene el elemento seleccionado, pero ¿cómo obtiene su modelo de vista esta información? Esto parece una manera agradable y limpia de hacer esto, pero solo necesito un poco más de información ...
Bob Horn

local: MyThingyDetailsView es simplemente un UserControl lleno de XAML que crea una vista de detalles sobre una instancia "cosita". Está incrustado en el medio de otra vista como contenido, con el DataContext de esta vista es el elemento de vista de árbol seleccionado actualmente, utilizando el enlace de Elemento.
Wes

6

También puede usar la propiedad TreeViewItem.IsSelected


Creo que esta podría ser la respuesta correcta. Pero me gustaría ver un ejemplo o una recomendación de mejores prácticas sobre cómo se pasa la propiedad IsSelected de los Artículos a TreeView.
anhoppe

3

También hay una manera de crear la propiedad XAML bindable SelectedItem sin usar 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
}

Luego puede usar esto en su XAML como:

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

3

Intenté todas las soluciones de estas preguntas. Nadie resolvió mi problema por completo. Así que creo que es mejor usar una clase heredada con la propiedad redefinida SelectedItem. Funcionará perfectamente si elige el elemento de árbol de la GUI y si establece el valor de esta propiedad en su código

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

Sería mucho más rápido si no se llama a UpdateLayout () e IsExpanded para algunos nodos. ¿Cuándo no es necesario llamar a UpdateLayout () e IsExpanded? Cuando el elemento del árbol fue visitado previamente. ¿Cómo saber eso? ContainerFromItem () devuelve un valor nulo para los nodos no visitados. Por lo tanto, podemos expandir el nodo principal solo cuando ContainerFromItem () devuelve un valor nulo para los hijos.
CoperNick

3

Mi requisito era una solución basada en PRISM-MVVM donde se necesitaba un TreeView y el objeto vinculado es del tipo Colección <> y, por lo tanto, necesita HierarchicalDataTemplate. El BindableSelectedItemBehavior predeterminado no podrá identificar el TreeViewItem secundario. Para que funcione en este escenario.

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

Esto permite recorrer todos los elementos independientemente del nivel.


¡Gracias! Este fue el único que funciona para mi escenario, que no es diferente al suyo.
Robert

Funciona muy bien y no hace que los enlaces seleccionados / expandidos se confundan .
Rusty

2

Sugiero una adición al comportamiento proporcionado por Steve Greatrex. Su comportamiento no refleja los cambios de la fuente porque puede no ser una colección de TreeViewItems. Por lo tanto, se trata de encontrar el TreeViewItem en el árbol cuyo datacontext es el Valor seleccionado de la fuente. TreeView tiene una propiedad protegida llamada "ItemsHost", que contiene la colección TreeViewItem. Podemos obtenerlo a través de la reflexión y caminar por el árbol buscando el elemento seleccionado.

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 esta manera, el comportamiento funciona para enlaces bidireccionales. Alternativamente, es posible mover la adquisición de ItemsHost al método OnAttached de Behavior, ahorrando la sobrecarga de usar la reflexión cada vez que se actualiza el enlace.


2

WPF MVVM TreeView SelectedItem

... es una mejor respuesta, pero no menciona una manera de obtener / establecer el SelectedItem en ViewModel.

  1. Agregue una propiedad booleana IsSelected a su ItemViewModel y vincúlela en un Style Setter para TreeViewItem.
  2. Agregue una propiedad SelectedItem a su ViewModel usado como DataContext para TreeView. Esta es la pieza que falta en la solución anterior.
    'ItemVM ...
    La propiedad pública se selecciona como booleana
        Obtener
            Volver _func.SelectedNode Is Me
        End Get
        Establecer (valor como booleano)
            Si el valor IsSelected entonces
                _func.SelectedNode = If (valor, Yo, Nada)
            Terminara si
            RaisePropertyChange ()
        Conjunto final
    Terminar propiedad
    'TreeVM ...
    Propiedad pública seleccionada Artículo como artículo VM
        Obtener
            Devuelve _selectedItem
        End Get
        Establecer (valor como elemento VM)
            Si _selectedItem es valor entonces
                Regreso
            Terminara si
            Dim anterior = _selectedItem
            _selectedItem = valor
            Si anterior no es nada entonces
                prev.IsSelected = False
            Terminara si
            Si _selectedItem isNot Nothing Nothing entonces
                _selectedItem.IsSelected = True
            Terminara si
        Conjunto final
    Terminar propiedad
<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

Después de estudiar Internet por un día, encontré mi propia solución para seleccionar un elemento después de crear una vista de árbol normal en un entorno 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

También se puede hacer usando la propiedad IsSelected del elemento TreeView. Así es como lo logré,

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

Luego, en el ViewModel que contiene los datos a los que está vinculado su TreeView, simplemente suscríbase al evento en la clase TreeViewItem.

TreeViewItem.OnItemSelected += TreeViewItemSelected;

Y finalmente, implemente este controlador en el mismo ViewModel,

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

Y la encuadernación, por supuesto,

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

Esta es en realidad una solución subestimada. Al cambiar su forma de pensar y vincular la propiedad IsSelected de cada elemento de vista de árbol, y aumentar los eventos IsSelected, puede utilizar la funcionalidad integrada que funciona bien con el enlace bidireccional. He intentado muchas soluciones propuestas para este problema, y ​​esta es la primera que ha funcionado. Solo un pequeño complejo para cablear. Gracias.
Richard Moore

1

Sé que este hilo tiene 10 años, pero el problema aún existe ...

La pregunta original era "recuperar" el elemento seleccionado. También necesitaba "obtener" el elemento seleccionado en mi modelo de vista (no configurarlo). De todas las respuestas en este hilo, la de 'Wes' es la única que aborda el problema de manera diferente: si puede usar el 'Elemento seleccionado' como objetivo para el enlace de datos, úselo como fuente para el enlace de datos. Wes lo hizo en otra propiedad de vista, lo haré en una propiedad de modelo de vista:

Necesitamos dos cosas:

  • Crear una propiedad de dependencia en el modelo de vista (en mi caso de tipo 'MyObject' ya que mi vista de árbol está vinculada al objeto del tipo 'MyObject')
  • Enlace desde Treeview.SelectedItem a esta propiedad en el constructor de la Vista (sí, ese es el código detrás, pero es probable que también inicie su contexto de datos allí)

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

Ver constructor:

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

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

0

(Solo aceptemos que TreeView obviamente está roto con respecto a este problema. La unión a SelectedItem hubiera sido obvia. Suspiro )

Necesitaba la solución para interactuar correctamente con la propiedad IsSelected de TreeViewItem, así que así es como lo hice:

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

Te traigo mi solución que ofrece las siguientes características:

  • Soporta 2 formas vinculantes

  • Actualiza automáticamente las propiedades TreeViewItem.IsSelected (de acuerdo con SelectedItem)

  • No hay subclases TreeView

  • Los elementos vinculados a ViewModel pueden ser de cualquier tipo (incluso nulos)

1 / Pegue el siguiente código en su 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 / Ejemplo de uso en su archivo XAML

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

0

Propongo esta solución (que considero la más fácil y sin pérdidas de memoria) que funciona perfectamente para actualizar el elemento seleccionado de ViewModel desde el elemento seleccionado de View.

Tenga en cuenta que cambiar el elemento seleccionado de ViewModel no actualizará el elemento seleccionado de la 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;
    }
}

Uso de XAML

<l:TreeViewEx ItemsSource="{Binding Path=Items}" SelectedItemEx="{Binding Path=SelectedItem}" >
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.