Ligação de dados ao SelectedItem em uma Treeview WPF

240

Como recuperar o item selecionado em uma exibição em árvore do WPF? Eu quero fazer isso em XAML, porque eu quero vinculá-lo.

Você pode pensar que é, SelectedItemmas aparentemente o que não existe, é somente leitura e, portanto, inutilizável.

Isto é o que eu quero fazer:

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

Eu quero vincular a SelectedItemuma propriedade no meu Model.

Mas isso me dá o erro:

A propriedade 'SelectedItem' é somente leitura e não pode ser configurada a partir da marcação.

Edit: Ok, esta é a maneira que eu resolvi isso:

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

e no codebehindfile do meu xaml:

private void TreeView_OnSelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
    Model.SelectedCluster = (Cluster)e.NewValue;
}
Natrium
fonte
50
Cara, isso é péssimo. Isso também me atingiu. Eu vim aqui esperando descobrir que existe um jeito decente e sou apenas um idiota. Esta é a primeira vez que estou triste que eu não sou um idiota ..
Andrei Rînea
6
este realmente é uma porcaria e atrapalhar o conceito de ligação
Delta
Espero que isso poderia ajudar alguém para ligar a uma volta exibição de árvore item selecionado mudou chamada em iCommand jacobaloysious.wordpress.com/2012/02/19/...
jacob Aloysious
9
Em termos de ligação e MVVM, o code behind não é "banido", mas o code behind deve suportar a exibição. Na minha opinião, de todas as outras soluções que já vi, o código por trás é uma opção muito melhor, pois ainda está lidando com "vincular" a visualização ao modelo de visualização. O único aspecto negativo é que, se você tiver uma equipe com um designer trabalhando apenas em XAML, o código por trás poderá ser quebrado / negligenciado. É um preço pequeno a pagar por uma solução que leva 10 segundos para ser implementada.
Nrjohnstone
Uma das soluções mais fáceis, provavelmente: stackoverflow.com/questions/1238304/...
JoanComasFdz

Respostas:

240

Sei que isso já teve uma resposta aceita, mas reuni isso para resolver o problema. Ele usa uma idéia semelhante à solução da Delta, mas sem a necessidade de subclassificar o 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;
    }
}

Você pode usar isso no seu XAML como:

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

Espero que ajude alguém!

Steve Greatrex
fonte
5
Como Brent apontou, eu também precisei adicionar Mode = TwoWay à encadernação. Eu não sou um "Blender", então não estava familiarizado com a classe Behavior <> do System.Windows.Interactivity. A montagem faz parte do Expression Blend. Para aqueles que não desejam comprar / instalar uma avaliação para obter essa montagem, você pode baixar o BlendSDK, que inclui System.Windows.Interactivity. BlendSDK 3 para 3.5 ... Eu acho que é o BlendSDK 4 para 4.0. Nota: Isso permite que você obtenha apenas o item selecionado, não permite definir o item selecionado.
Mike Rowley
4
Você também pode substituir UIPropertyMetadata por FrameworkPropertyMetadata (null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnSelectedItemChanged));
Filimindji
3
Esta seria uma abordagem para resolver o problema: stackoverflow.com/a/18700099/4227
bitbonk
2
@ Lucas exatamente como mostrado no snippet de código XAML acima. Basta substituir {Binding SelectedItem, Mode=TwoWay}por{Binding MyViewModelField, Mode=TwoWay}
Steve Greatrex
4
@Pascal it'sxmlns:e="http://schemas.microsoft.com/expression/2010/interactivity"
Steve Greatrex
46

Esta propriedade existe: TreeView.SelectedItem

Mas é somente leitura, portanto, você não pode atribuí-lo por meio de uma ligação, apenas recuperá-lo

Thomas Levesque
fonte
Eu aceito esta resposta, porque lá eu encontrei este link, que deixou a minha própria resposta: msdn.microsoft.com/en-us/library/ms788714.aspx
Natrium
1
Então, isso pode TreeView.SelectedItemafetar uma propriedade no modelo quando o usuário seleciona um item (aka OneWayToSource)?
Shimmy Weitzhandler
43

Responda com propriedades anexadas e sem dependências externas, caso seja necessário!

Você pode criar uma propriedade anexada que seja vinculável e tenha um getter e um 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;
        }
    }
}

Adicione a declaração do espaço para nome que contém essa classe ao seu XAML e ligue-a da seguinte forma (local é como nomeei a declaração do espaço para nome):

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

    </TreeView>

Agora você pode vincular o item selecionado e também configurá-lo no seu modelo de exibição para alterá-lo programaticamente, caso esse requisito ocorra. Obviamente, isso pressupõe que você implementa INotifyPropertyChanged nessa propriedade específica.

Bas
fonte
4
+1, a melhor resposta neste tópico imho. Não há dependência do System.Windows.Interactivity e permite ligação bidirecional (configuração programaticamente em um ambiente MVVM). Perfeito.
Chris Ray
5
Um problema com essa abordagem é que o comportamento só começará a funcionar quando o item selecionado tiver sido definido uma vez por meio da ligação (ou seja, do ViewModel). Se o valor inicial na VM for nulo, a ligação não atualizará o valor DP e o comportamento não será ativado. Você pode corrigir isso usando um item selecionado padrão diferente (por exemplo, um item inválido).
Mark
6
@ Mark: Basta usar o novo object () em vez do nulo acima ao instanciar o UIPropertyMetadata da propriedade anexada. O problema deve ser ido então ...
Mexilhãozinho
2
A conversão para TreeViewItem falha para mim, suponho que estou usando um HierarchicalDataTemplate aplicado a partir de recursos por tipo de dados. Mas se você remover ChangeSelectedItem, a ligação a um viewmodel e a recuperação do item funcionarão bem.
Casey Sebben
1
Também estou tendo problemas com a transmissão para TreeViewItem. Nesse ponto, o ItemContainerGenerator contém apenas referências aos itens raiz, mas eu preciso que seja possível obter itens não raiz também. Se você passar uma referência a uma, a conversão falhará e retornará nulo. Não sabe ao certo como isso pode ser corrigido?
Bob Tway #
39

Bem, eu encontrei uma solução. Move a bagunça, para que o MVVM funcione.

Primeiro adicione esta 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 adicione isso ao seu xaml:

 <local:ExtendedTreeView ItemsSource="{Binding Items}" SelectedItem_="{Binding Item, Mode=TwoWay}">
 .....
 </local:ExtendedTreeView>
Delta
fonte
3
Esta é a ÚNICA coisa que chegou perto de trabalhar para mim até agora. Eu realmente gosto desta solução.
Rachael
1
Não sei porquê, mas não funcionou para mim :( I conseguiu para obter o item selecionado da árvore, mas não vice-versa - para alterar o item selecionado de fora da árvore.
Erez
Seria um pouco mais limpa para definir a propriedade de dependência como BindsTwoWayByDefault então você não precisa especificar TwoWay no XAML
Stephen Holt
Essa é a melhor abordagem. Ele não usa referência de interatividade, não usa código por trás, não possui vazamento de memória, como alguns comportamentos. Obrigado.
Alexandru Dicu 17/02
Como mencionado, esta solução não funciona com ligação bidirecional. Se você definir o valor no modelo de exibição, a alteração não será propagada para o TreeView.
Richard Moore
25

Responde um pouco mais do que o OP está esperando ... Mas espero que possa ajudar alguém pelo menos.

Se você deseja executar um ICommandsempre que SelectedItemalterado, é possível vincular um comando em um evento e o uso de uma propriedade SelectedItemnoViewModel não será mais necessário.

Para fazer isso:

1- Adicione referência a System.Windows.Interactivity

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

2- Vincule o comando ao 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>
JiBéDoublevé
fonte
3
A referência System.Windows.Interactivitypode ser instalado a partir NuGet: nuget.org/packages/System.Windows.Interactivity.WPF
Junle Li
Estou tentando resolver esses problemas há horas, eu implementei isso, mas meu comando não está funcionando, por favor, você poderia me ajudar?
Alfie
1
Foram introduzidos os comportamentos XAML para WPF pela Microsoft no final de 2018. Ele pode ser usado em vez de System.Windows.Interactivity. Foi trabalhado para mim (tentei com o projeto .NET Core). Para definir as coisas, basta adicionar o pacote de nuget Microsoft.Xaml.Behaviors.Wpf , altere o espaço para nome xmlns:i="http://schemas.microsoft.com/xaml/behaviors". Para obter mais informações - consulte o blog
rychlmoj
19

Isso pode ser feito de uma maneira 'melhor', usando apenas a ligação e o EventToCommand da biblioteca GalaSoft MVVM Light. Na sua VM, adicione um comando que será chamado quando o item selecionado for alterado e inicialize o comando para executar qualquer ação necessária. Neste exemplo, usei um RelayCommand e defina apenas a propriedade 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; }
}

Em seguida, adicione o comportamento EventToCommand no seu xaml. Isso é realmente fácil usando a mistura.

<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>
bstoney
fonte
Esta é uma boa solução, especialmente se você já estiver usando o kit de ferramentas MvvmLight. No entanto, isso não resolve o problema de configurar o nó selecionado e faz com que a árvore atualize a seleção.
Keft
12

Tudo complicado ... Vá com o Caliburn Micro (http://caliburnmicro.codeplex.com/)

Visão:

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

ViewModel:

public void SetSelectedItem(YourNodeViewModel item) {}; 
Devgig
fonte
5
Sim ... e onde está a parte que define o SelectedItem no TreeView ?
Mnn
Caliburn é agradável e elegante. Funciona com bastante facilidade para hierarquias aninhadas
purusartha
8

Encontrei esta página procurando a mesma resposta que o autor original e, provando que sempre há mais de uma maneira de fazê-lo, a solução para mim foi ainda mais fácil do que as respostas fornecidas aqui até agora, então achei que seria melhor adicionar para a pilha.

A motivação para a ligação é mantê-la agradável e MVVM. O uso provável do ViewModel é ter uma propriedade com um nome como "CurrentThingy" e, em outro lugar, o DataContext em alguma outra coisa está vinculado a "CurrentThingy".

Em vez de passar por etapas adicionais necessárias (por exemplo: comportamento personalizado, controle de terceiros) para oferecer suporte a uma boa ligação do TreeView ao meu modelo e, depois, de outra coisa ao meu modelo, minha solução foi usar o simples elemento de ligação à outra coisa para TreeView.SelectedItem, em vez de vincular a outra coisa ao meu ViewModel, ignorando o trabalho extra necessário.

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

Obviamente, isso é ótimo para ler o item atualmente selecionado, mas não defini-lo, o que é tudo o que eu precisava.

Wes
fonte
1
O que é local: MyThingyDetailsView? Recebo o local: MyThingyDetailsView mantém o item selecionado, mas como o seu modelo de exibição obtém essas informações? Isto parece um bom, maneira limpa para fazer isso, mas eu preciso de um pouco mais info ...
Bob Horn
local: MyThingyDetailsView é simplesmente um UserControl cheio de XAML, criando uma visualização de detalhes sobre uma instância "coisinha". Ele é incorporado no meio de outra visualização como conteúdo, com o DataContext dessa visualização é o item de visualização em árvore atualmente selecionado, usando a Ligação de elemento.
14373 Wes
6

Você também pode usar a propriedade TreeViewItem.IsSelected

nabeelfarid
fonte
Eu acho que essa pode ser a resposta correta. Mas eu gostaria de ver um exemplo ou uma recomendação de práticas recomendadas sobre como a propriedade IsSelected dos Items é passada para o TreeView.
Anhoppe 26/09/16
3

Também há uma maneira de criar a propriedade SelectedItem vinculável por XAML sem 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
}

Você pode usar isso no seu XAML como:

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

Eu tentei todas as soluções dessas perguntas. Ninguém resolveu meu problema completamente. Então, acho melhor usar essa classe herdada com a propriedade redefinida SelectedItem. Funcionará perfeitamente se você escolher o elemento de árvore da GUI e se definir esse valor da propriedade no seu 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;
    }
} 
Evgeny Bechkalo
fonte
Seria muito mais rápido se UpdateLayout () e IsExpanded não fossem chamados para alguns nós. Quando não é necessário chamar UpdateLayout () e IsExpanded? Quando o item da árvore foi visitado anteriormente. Como saber isso? ContainerFromItem () retorna nulo para nós não visitados. Portanto, podemos expandir o nó pai apenas quando ContainerFromItem () retorna nulo para filhos.
precisa saber é o seguinte
3

Meu requisito era a solução baseada em PRISM-MVVM, em que um TreeView era necessário e o objeto vinculado é do tipo Collection <> e, portanto, precisa de HierarchicalDataTemplate. O padrão BindableSelectedItemBehavior não poderá identificar o filho TreeViewItem. Para fazê-lo funcionar neste cenário.

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

Isso permite percorrer todos os elementos, independentemente do nível.

Chaitanya Kadamati
fonte
Obrigado! Este foi o único que funciona para o meu cenário que não é diferente do seu.
Robert
Funciona muito bem e não causa confusão nas ligações selecionadas / expandidas .
Rusty
2

Sugiro uma adição ao comportamento fornecido por Steve Greatrex. Seu comportamento não reflete as alterações da fonte porque pode não ser uma coleção de TreeViewItems. Portanto, é uma questão de encontrar o TreeViewItem na árvore em que datacontext é o selectedValue da origem. O TreeView tem uma propriedade protegida chamada "ItemsHost", que contém a coleção TreeViewItem. Podemos refleti-lo e percorrer a árvore procurando o item selecionado.

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

Dessa forma, o comportamento funciona para ligações bidirecionais. Como alternativa, é possível mover a aquisição de ItemsHost para o método OnAttached do Behavior, economizando a sobrecarga de usar reflexão sempre que a ligação for atualizada.

Arthur Nunes
fonte
2

WPF MVVM TreeView SelectedItem

... é uma resposta melhor, mas não menciona uma maneira de obter / definir o SelectedItem no ViewModel.

  1. Adicione uma propriedade booleana IsSelected ao ItemViewModel e vincule a ela em um Style Setter para o TreeViewItem.
  2. Adicione uma propriedade SelectedItem ao seu ViewModel usado como DataContext para o TreeView. Esta é a peça que falta na solução acima.
    'ItemVM ...
    Propriedade pública IsSelected como Boolean
        Obter
            Retornar _func.SelectedNode Sou Eu
        End Get
        Definir (valor como booleano)
            Se o valor IsSelected for
                _func.SelectedNode = If (valor, Eu, Nada)
            Fim se
            RaisePropertyChange ()
        Finalizar conjunto
    Propriedade final
    'TreeVM ...
    Propriedade pública SelectedItem As ItemVM
        Obter
            Retornar _selectedItem
        End Get
        Definir (valor como ItemVM)
            Se _selectedItem Is value Então
                Retorna
            Fim se
            Dim anterior = _selectedItem
            _selectedItem = value
            Se prev Não é nada, então
                prev.IsSelected = False
            Fim se
            Se _selectedItem não for nada, então
                _selectedItem.IsSelected = True
            Fim se
        Finalizar conjunto
    Propriedade final
<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>
JustinMichel
fonte
1

Depois de estudar na Internet por um dia, encontrei minha própria solução para selecionar um item depois de criar uma visualização em árvore normal em um ambiente 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 ;
            }
        }
carma
fonte
1

Isso também pode ser feito usando a propriedade IsSelected do item TreeView. Aqui está como eu consegui,

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

Em seguida, no ViewModel que contém os dados aos quais o TreeView está vinculado, basta se inscrever no evento na classe TreeViewItem.

TreeViewItem.OnItemSelected += TreeViewItemSelected;

E, finalmente, implemente esse manipulador no mesmo ViewModel,

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

E a ligação, é claro,

<Setter Property="IsSelected" Value="{Binding IsSelected}" />    
Fahad Owais
fonte
Esta é realmente uma solução subestimada. Alterando sua maneira de pensar e vinculando a propriedade IsSelected de cada elemento da árvore de visualização e desenvolvendo os eventos IsSelected, você utiliza a funcionalidade incorporada que funciona bem com a vinculação bidirecional. Eu tentei muitas soluções propostas para esse problema, e esta é a primeira que funcionou. Apenas um pouco complexo para ligar. Obrigado.
Richard Moore
1

Eu sei que esta discussão tem 10 anos, mas o problema ainda existe ....

A pergunta original era 'recuperar' o item selecionado. Eu também precisava "obter" o item selecionado no meu modelo de exibição (não configurá-lo). De todas as respostas neste tópico, a de 'Wes' é a única que aborda o problema de maneira diferente: se você pode usar o 'Item selecionado' como um destino para a ligação de dados, use-o como uma fonte para a ligação de dados. Wes fez isso em outra propriedade view, farei isso em uma propriedade viewmodel:

Precisamos de duas coisas:

  • Crie uma propriedade de dependência no viewmodel (no meu caso do tipo 'MyObject', pois minha treeview está vinculada ao objeto do tipo 'MyObject')
  • Associe-se a Treeview.SelectedItem a essa propriedade no construtor da View (sim, isso é código por trás, mas é provável que você inicie o seu datacontext também)

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

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

        BindingOperations.SetBinding(DataContext, MyViewModel.SelectedTreeViewItemProperty, binding);
Nils
fonte
0

(Vamos todos concordar que o TreeView está obviamente bloqueado em relação a esse problema. A ligação ao SelectedItem teria sido óbvia. Suspiro )

Eu precisava da solução para interagir corretamente com a propriedade IsSelected de TreeViewItem, então veja como eu fiz isso:

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

Com 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>
Eric Jorgensen
fonte
0

Trago a você minha solução, que oferece os seguintes recursos:

  • Suporta 2 maneiras de ligação

  • Atualiza automaticamente as propriedades TreeViewItem.IsSelected (de acordo com o SelectedItem)

  • Nenhuma subclasse de TreeView

  • Os itens vinculados ao ViewModel podem ser de qualquer tipo (até nulos)

1 / Cole o seguinte código no seu 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 / Exemplo de uso no seu arquivo XAML

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

Proponho esta solução (que considero mais fácil e sem vazamentos de memória), que funciona perfeitamente para atualizar o item selecionado do ViewModel a partir do item selecionado do View.

Observe que a alteração do item selecionado no ViewModel não atualizará o item selecionado da View.

public class TreeViewEx : TreeView
{
    public static readonly DependencyProperty SelectedItemExProperty = DependencyProperty.Register("SelectedItemEx", typeof(object), typeof(TreeViewEx), new FrameworkPropertyMetadata(default(object))
    {
        BindsTwoWayByDefault = true // Required in order to avoid setting the "BindingMode" from the XAML
    });

    public object SelectedItemEx
    {
        get => GetValue(SelectedItemExProperty);
        set => SetValue(SelectedItemExProperty, value);
    }

    protected override void OnSelectedItemChanged(RoutedPropertyChangedEventArgs<object> e)
    {
        SelectedItemEx = e.NewValue;
    }
}

Uso de XAML

<l:TreeViewEx ItemsSource="{Binding Path=Items}" SelectedItemEx="{Binding Path=SelectedItem}" >
Kino101
fonte