Powiązanie danych z SelectedItem w widoku drzewa WPF

241

Jak mogę pobrać element wybrany w widoku drzewa WPF? Chcę to zrobić w XAML, ponieważ chcę to powiązać.

Możesz pomyśleć, że tak, SelectedItemale najwyraźniej nie istnieje, jest tylko do odczytu i dlatego nie nadaje się do użytku.

Oto co chcę zrobić:

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

Chcę powiązać SelectedItemwłaściwość z moim modelem.

Ale to daje mi błąd:

Właściwość „SelectedItem” jest tylko do odczytu i nie można jej ustawić na podstawie znaczników.

Edycja: Ok, oto jak to rozwiązałem:

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

oraz w pliku codebehindfile mojego xaml:

private void TreeView_OnSelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
    Model.SelectedCluster = (Cluster)e.NewValue;
}
Sód
źródło
51
Człowieku, to jest do bani. To też mnie uderzyło. Przybyłem tu z nadzieją, że jest dobry sposób, a ja jestem tylko idiotą. Po raz pierwszy jestem smutny, że nie jestem idiotą ..
Andrei Rînea
6
to naprawdę jest do bani i psuje wiążącą koncepcję
Delta
Mam nadzieję, że to może pomóc komuś powiązać z
jacob aloysious
9
Pod względem wiązania i MVVM, kod z tyłu nie jest „zbanowany”, raczej kod z tyłu powinien wspierać widok. Moim zdaniem ze wszystkich innych rozwiązań, które widziałem, kod jest znacznie lepszą opcją, ponieważ wciąż zajmuje się „wiązaniem” widoku z modelem widoku. Jedynym minusem jest to, że jeśli masz zespół z projektantem pracującym tylko w XAML, kod za nim może zostać zepsuty / zaniedbany. To niewielka cena za rozwiązanie, którego wdrożenie zajmuje 10 sekund.
nrjohnstone
Prawdopodobnie jedno z najłatwiejszych rozwiązań: stackoverflow.com/questions/1238304/…
JoanComasFdz

Odpowiedzi:

240

Zdaję sobie sprawę, że odpowiedź została już zaakceptowana, ale zebrałem ją, aby rozwiązać problem. Wykorzystuje podobny pomysł do rozwiązania Delta, ale bez potrzeby podklasy 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;
    }
}

Następnie możesz użyć tego w swojej XAML jako:

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

Mam nadzieję, że to komuś pomoże!

Steve Greatrex
źródło
5
Jak zauważył Brent, musiałem również dodać Mode = TwoWay do wiązania. Nie jestem „Blenderem”, więc nie znałem klasy Behaviour <> z System.Windows.Interactivity. Zespół jest częścią Expression Blend. Dla tych, którzy nie chcą kupować / instalować wersji próbnej, aby uzyskać ten zestaw, można pobrać pakiet BlendSDK zawierający System.Windows.Interactivity. BlendSDK 3 dla 3.5 ... Myślę, że to BlendSDK 4 dla 4.0. Uwaga: Pozwala to tylko uzyskać wybrany element, nie pozwala ustawić wybranego elementu
Mike Rowley,
4
Możesz także zastąpić UIPropertyMetadata przez FrameworkPropertyMetadata (null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnSelectedItemChanged));
Filimindji,
3
To byłoby podejście do rozwiązania problemu: stackoverflow.com/a/18700099/4227
bitbonk 9.09.2013
2
@Lukas dokładnie tak, jak pokazano we fragmencie kodu XAML powyżej. Wystarczy wymienić {Binding SelectedItem, Mode=TwoWay}z{Binding MyViewModelField, Mode=TwoWay}
Steve Greatrex
4
@Pascal It'sxmlns:e="http://schemas.microsoft.com/expression/2010/interactivity"
Steve Greatrex
46

Ta właściwość istnieje: TreeView.SelectedItem

Ale jest tylko do odczytu, więc nie można przypisać go przez powiązanie, a jedynie pobrać

Thomas Levesque
źródło
Akceptuję tę odpowiedź, ponieważ znalazłem ten link, który dał moją własną odpowiedź: msdn.microsoft.com/en-us/library/ms788714.aspx
Natrium
1
Czy mogę to TreeView.SelectedItemwpłynąć na właściwość modelu, gdy użytkownik wybierze element (alias OneWayToSource)?
Shimmy Weitzhandler
43

Odpowiedz z dołączonymi właściwościami i bez zewnętrznych zależności, jeśli zajdzie taka potrzeba!

Możesz utworzyć dołączoną właściwość, którą można powiązać i która zawiera getter i 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;
        }
    }
}

Dodaj deklarację przestrzeni nazw zawierającą tę klasę do swojej XAML i powiąż w następujący sposób (lokalny to sposób, w jaki nazwałem deklarację przestrzeni nazw):

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

    </TreeView>

Teraz możesz powiązać wybrany element, a także ustawić go w modelu widoku, aby zmienić go programowo, jeśli kiedykolwiek pojawi się to wymaganie. Zakłada się oczywiście, że zaimplementujesz INotifyPropertyChanged dla tej konkretnej właściwości.

Bas
źródło
4
+1, najlepsza odpowiedź w tym wątku imho. Brak zależności od System.Windows.Interactivity i pozwala na dwukierunkowe wiązanie (ustawienie programowo w środowisku MVVM). Idealny.
Chris Ray
5
Problem z tym podejściem polega na tym, że zachowanie zacznie działać dopiero, gdy wybrany element zostanie ustawiony raz za pomocą wiązania (tj. Z ViewModel). Jeśli wartość początkowa w maszynie wirtualnej jest równa null, powiązanie nie zaktualizuje wartości DP, a zachowanie nie zostanie aktywowane. Możesz to naprawić, używając innego domyślnie wybranego elementu (np. Nieprawidłowego elementu).
Mark
6
@Mark: Po prostu użyj nowego obiektu () zamiast null powyżej podczas tworzenia instancji UIPropertyMetadata właściwości dołączonej. Problem powinien zniknąć ...
Barnacleboy
2
Rzutowanie na TreeViewItem nie powiedzie się dla mnie, zakładam, ponieważ używam HierarchicalDataTemplate zastosowanego z zasobów według typu danych. Ale jeśli usuniesz ChangeSelectedItem, powiązanie z viewmodel i pobranie elementu będzie działać poprawnie.
Casey Sebben,
1
Mam również problemy z rzutowaniem na TreeViewItem. W tym momencie ItemContainerGenerator zawiera tylko odniesienia do elementów głównych, ale potrzebuję go, aby móc uzyskać także elementy inne niż root. Jeśli przekażesz odwołanie do jednego, rzutowanie zakończy się niepowodzeniem i zwróci null. Nie wiesz, jak to naprawić?
Bob Tway,
39

Cóż, znalazłem rozwiązanie. Porusza bałagan, dzięki czemu MVVM działa.

Najpierw dodaj tę klasę:

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

i dodaj to do swojego xaml:

 <local:ExtendedTreeView ItemsSource="{Binding Items}" SelectedItem_="{Binding Item, Mode=TwoWay}">
 .....
 </local:ExtendedTreeView>
Delta
źródło
3
To JEDYNA rzecz, która do tej pory była dla mnie bliska. Naprawdę podoba mi się to rozwiązanie.
Rachael
1
Nie wiem dlaczego, ale to nie zadziałało :( Udało mi się pobrać wybrany element z drzewa, ale nie odwrotnie - aby zmienić wybrany element spoza drzewa.
Erez
Byłoby nieco fajniej ustawić właściwość zależności jako BindsTwoWayByDefault, wtedy nie trzeba by określać TwoWay w XAML
Stephen Holt
To najlepsze podejście. Nie używa referencji interaktywności, nie używa kodu z tyłu, nie ma przecieku pamięci, jak niektóre zachowania. Dziękuję Ci.
Alexandru Dicu
Jak wspomniano, to rozwiązanie nie działa z wiązaniem dwukierunkowym. Jeśli ustawisz wartość w viewmodel, zmiana nie zostanie propagowana do TreeView.
Richard Moore
25

Odpowiada nieco więcej, niż się spodziewa PO ... Ale mam nadzieję, że to może komuś pomóc.

Jeśli chcesz wykonać ICommand, gdy SelectedItemzmianie, można powiązać komendę na zdarzenia i wykorzystania nieruchomości SelectedItemw ViewModelnie jest już potrzebny.

Aby to zrobić:

1- Dodaj odniesienie do System.Windows.Interactivity

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

2- Powiąż polecenie z wydarzeniem 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é
źródło
3
Odwołanie System.Windows.Interactivitymożna zainstalować z NuGet: nuget.org/packages/System.Windows.Interactivity.WPF
Junle Li
Próbowałem rozwiązać ten problem od wielu godzin, wdrożyłem to, ale moje polecenie nie działa, proszę, czy możesz mi pomóc?
Alfie,
1
Pod koniec 2018 r. Microsoft wprowadził zachowania XAML dla WPF przez Microsoft. Można go używać zamiast System.Windows.Interactivity. To zadziałało dla mnie (próbowałem z projektem .NET Core). Aby to skonfigurować, po prostu dodaj pakiet nuget Microsoft.Xaml.Behaviors.Wpf , zmień przestrzeń nazw na xmlns:i="http://schemas.microsoft.com/xaml/behaviors". Aby uzyskać więcej informacji - zobacz blog
rychlmoj
19

Można to osiągnąć w „ładniejszy” sposób, używając tylko wiązania i EventToCommand biblioteki GalaSoft MVVM Light. Na maszynie wirtualnej dodaj polecenie, które zostanie wywołane, gdy wybrany element zostanie zmieniony, i zainicjuj polecenie, aby wykonać niezbędne czynności. W tym przykładzie użyłem RelayCommand i po prostu ustawię właściwość 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; }
}

Następnie dodaj zachowanie EventToCommand do twojego xaml. Jest to naprawdę łatwe przy użyciu mieszanki.

<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
źródło
To dobre rozwiązanie, szczególnie jeśli już korzystasz z zestawu narzędzi MvvmLight. Nie rozwiązuje to jednak problemu ustawienia wybranego węzła i powoduje, że widok drzewa aktualizuje wybór.
keft
12

Wszystko skomplikowane ... Przejdź z Caliburn Micro (http://caliburnmicro.codeplex.com/)

Widok:

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

ViewModel:

public void SetSelectedItem(YourNodeViewModel item) {}; 
Devgig
źródło
5
Tak ... i gdzie jest część, która ustawia SelectedItem w TreeView ?
2013 r
Caliburn jest ładny i elegancki. Działa dość łatwo dla hierarchii zagnieżdżonych
Purusartha
8

Natknąłem się na tę stronę, szukając tej samej odpowiedzi, co autor, i udowadniając, że zawsze jest na to więcej niż jeden sposób, rozwiązanie było dla mnie jeszcze łatwiejsze niż odpowiedzi tutaj podane, więc pomyślałem, że równie dobrze mogę dodać na stos.

Motywacją do wiązania jest utrzymanie go w porządku i MVVM. Prawdopodobne użycie ViewModel to posiadanie właściwości o nazwie takiej jak „CurrentThingy”, a gdzie indziej DataContext w innej rzeczy jest powiązany z „CurrentThingy”.

Zamiast przejść dodatkowe kroki wymagane (np .: niestandardowe zachowanie, kontrola innej firmy) w celu obsługi ładnego powiązania z TreeView z moim modelem, a następnie z czegoś innego do mojego modelu, moim rozwiązaniem było użycie prostego elementu wiążącego inną rzecz z TreeView.SelectedItem, zamiast wiązania innej rzeczy z moim ViewModel, pomijając w ten sposób dodatkową wymaganą pracę.

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

Oczywiście, jest to świetne do czytania aktualnie wybranego elementu, ale bez ustawiania go, co jest wszystkim, czego potrzebowałem.

Wes
źródło
1
Co to jest lokalny: MyThingyDetailsView? Rozumiem lokalnie: MyThingyDetailsView przechowuje wybrany element, ale w jaki sposób model widoku uzyskuje te informacje? Wygląda to na ładny, czysty sposób na zrobienie tego, ale potrzebuję tylko trochę więcej informacji ...
Bob Horn
local: MyThingyDetailsView to po prostu UserControl pełen XAML, tworzący widok szczegółów na temat jednej „rzeczowej” instancji. Jest osadzony w środku innego widoku jako treść, w tym DataContext tego widoku jest aktualnie wybranym elementem widoku drzewa, z wykorzystaniem wiązania Element.
Wes
6

Może być również możliwe użycie właściwości TreeViewItem.IsSelected

nabeelfarid
źródło
Myślę, że to może być poprawna odpowiedź. Chciałbym jednak zobaczyć przykład lub zalecenie dotyczące najlepszych praktyk, w jaki sposób właściwość IsSelected elementów jest przekazywana do TreeView.
anhoppe
3

Istnieje również sposób na utworzenie powiązanej z XAML właściwości SelectedItem bez użycia 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
}

Następnie możesz użyć tego w swojej XAML jako:

<TreeView  helper:BindableSelectedItemHelper.Attach="True" 
           helper:BindableSelectedItemHelper.SelectedItem="{Binding SelectedItem, Mode=TwoWay}">
Paul Solomenchuk
źródło
3

Próbowałem wszystkich rozwiązań tych pytań. Nikt nie rozwiązał w pełni mojego problemu. Myślę więc, że lepiej użyć takiej odziedziczonej klasy z przedefiniowaną właściwością SelectedItem. Będzie działał idealnie, jeśli wybierzesz element drzewa z GUI i ustawisz tę wartość właściwości w swoim kodzie

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
źródło
Byłoby to znacznie szybsze, gdyby UpdateLayout () i IsExpanded nie były wywoływane dla niektórych węzłów. Kiedy nie trzeba wywoływać UpdateLayout () i IsExpanded? Kiedy element drzewa był wcześniej odwiedzany. Jak to wiedzieć? Funkcja ContainerFromItem () zwraca null dla nieodwiedzonych węzłów. Możemy więc rozwinąć węzeł nadrzędny tylko wtedy, gdy ContainerFromItem () zwraca wartość null dla dzieci.
CoperNick
3

Moje wymaganie dotyczyło rozwiązania opartego na PRISM-MVVM, w którym potrzebny był TreeView, a związany obiekt jest typu Collection <>, a zatem potrzebuje HierarchicalDataTemplate. Domyślny BindableSelectedItemBehavior nie będzie w stanie zidentyfikować potomnego TreeViewItem. Aby działało w tym scenariuszu.

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

Umożliwia to iterację wszystkich elementów niezależnie od poziomu.

Chaitanya Kadamati
źródło
Dziękuję Ci! To był jedyny, który działa w moim scenariuszu, który jest podobny do twojego.
Robert
Działa bardzo dobrze i nie powoduje dezorientacji wybranych / rozwiniętych powiązań .
Rusty
2

Sugeruję uzupełnienie zachowania zapewnionego przez Steve'a Greatrexa. Jego zachowanie nie odzwierciedla zmian ze źródła, ponieważ może nie być zbiorem TreeViewItems. Jest więc kwestią znalezienia TreeViewItem w drzewie, którego tekstem danych jest selectedValue ze źródła. TreeView ma chronioną właściwość o nazwie „ItemsHost”, która przechowuje kolekcję TreeViewItem. Możemy to zrobić przez odbicie i przejść się po drzewie w poszukiwaniu wybranego elementu.

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

W ten sposób zachowanie działa w przypadku powiązań dwukierunkowych. Alternatywnie możliwe jest przeniesienie akwizycji ItemsHost do metody OnAttached Behaviour, co pozwala zaoszczędzić na nakładach związanych z odbiciem przy każdej aktualizacji wiązania.

Arthur Nunes
źródło
2

WPF MVVM TreeView SelectedItem

... jest lepszą odpowiedzią, ale nie wspomina o sposobie uzyskania / ustawienia SelectedItem w ViewModel.

  1. Dodaj właściwość boolean IsSelected do ItemViewModel i powiąż ją w seterze stylów dla TreeViewItem.
  2. Dodaj właściwość SelectedItem do swojego ViewModel używanego jako DataContext dla TreeView. To brakujący element w powyższym rozwiązaniu.
    „ItemVM ...
    Właściwość publiczna jest wybrana jako wartość logiczna
        Dostać
            Zwróć _func.SelectedNode Is Me
        End Get
        Set (wartość As Boolean)
            Jeśli wartość IsSelected to
                _func.SelectedNode = If (wartość, Ja, Nic)
            End If
            RaisePropertyChange ()
        Zestaw końcowy
    Zakończ właściwość
    „TreeVM ...
    Właściwość publiczna SelectedItem As ItemVM
        Dostać
            Zwróć _selectedItem
        End Get
        Set (wartość As ItemVM)
            Jeśli _selectedItem jest wartością to
                Powrót
            End If
            Dim prev = _selectedItem
            _selectedItem = wartość
            Jeśli poprzedni IsNot Nothing Then
                prev.IsSelected = False
            End If
            Jeśli _selectedItem IsNot Nothing Then
                _selectedItem.IsSelected = True
            End If
        Zestaw końcowy
    Zakończ właściwość
<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
źródło
1

Po studiach przez jeden dzień w Internecie znalazłem własne rozwiązanie do wyboru elementu po utworzeniu normalnego widoku drzewa w normalnym środowisku WPF / C #

private void BuildSortTree(int sel)
        {
            MergeSort.Items.Clear();
            TreeViewItem itTemp = new TreeViewItem();
            itTemp.Header = SortList[0];
            MergeSort.Items.Add(itTemp);
            TreeViewItem prev;
            itTemp.IsExpanded = true;
            if (0 == sel) itTemp.IsSelected= true;
            prev = itTemp;
            for(int i = 1; i<SortList.Count; i++)
            {

                TreeViewItem itTempNEW = new TreeViewItem();
                itTempNEW.Header = SortList[i];
                prev.Items.Add(itTempNEW);
                itTempNEW.IsExpanded = true;
                if (i == sel) itTempNEW.IsSelected = true;
                prev = itTempNEW ;
            }
        }
karma
źródło
1

Można to również zrobić za pomocą właściwości IsSelected elementu TreeView. Oto jak to zrobiłem,

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

Następnie w ViewModel, który zawiera dane, z którymi związany jest Twój TreeView, po prostu subskrybuj zdarzenie w klasie TreeViewItem.

TreeViewItem.OnItemSelected += TreeViewItemSelected;

Na koniec zaimplementuj ten moduł obsługi w tym samym ViewModel,

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

I oczywiście wiązanie

<Setter Property="IsSelected" Value="{Binding IsSelected}" />    
Fahad Owais
źródło
To jest właściwie rozwiązanie niedoceniane. Zmieniając sposób myślenia i wiążąc właściwość IsSelected każdego elementu widoku drzewa, oraz przenosząc zdarzenia IsSelected, można korzystać z wbudowanej funkcjonalności, która działa dobrze z wiązaniem dwukierunkowym. Wypróbowałem wiele proponowanych rozwiązań tego problemu i jest to pierwszy, który zadziałał. Tylko trochę skomplikowany do połączenia. Dzięki.
Richard Moore
1

Wiem, że ten wątek ma 10 lat, ale problem nadal istnieje ....

Pierwotne pytanie brzmiało „odzyskać” wybrany element. Musiałem także „uzyskać” wybrany element w moim viewmodelu (nie ustawiać go). Spośród wszystkich odpowiedzi w tym wątku, odpowiedź „Wesa” jest jedyną, która podchodzi do problemu inaczej: Jeśli możesz użyć „Selected Item” jako celu do wiązania danych, użyj go jako źródła do wiązania danych. Wes zrobił to z inną właściwością view, zrobię to z właściwością viewmodel:

Potrzebujemy dwóch rzeczy:

  • Utwórz właściwość zależności w viewmodel (w moim przypadku typu „MyObject”, ponieważ mój widok drzewa jest powiązany z obiektem typu „MyObject”)
  • Powiąż z Treeview.SelectedItem z tą właściwością w konstruktorze View (tak, to jest kod za, ale prawdopodobnie zainicjujesz tam również swój tekst danych)

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

Wyświetl konstruktora:

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

        BindingOperations.SetBinding(DataContext, MyViewModel.SelectedTreeViewItemProperty, binding);
Nils
źródło
0

(Po prostu wszyscy zgódźmy się, że TreeView jest oczywiście zablokowany w związku z tym problemem. Powiązanie z SelectedItem byłoby oczywiste. Westchnienie )

Potrzebowałem rozwiązania, aby poprawnie współpracować z właściwością IsSelected TreeViewItem, więc oto jak to zrobiłem:

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

Z tym 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
źródło
0

Przedstawiam wam moje rozwiązanie, które oferuje następujące funkcje:

  • Obsługuje 2 sposoby wiązania

  • Automatycznie aktualizuje właściwości TreeViewItem.IsSelected (zgodnie z SelectedItem)

  • Brak podklasy TreeView

  • Elementy powiązane z ViewModel mogą być dowolnego typu (nawet zerowe)

1 / Wklej następujący kod w swoim 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 / Przykład użycia w pliku XAML

<TreeView myNS:BindableSelectedItem.SelectedItem="{Binding Path=SelectedItem, Mode=TwoWay}" />  
Kino101
źródło
0

Proponuję to rozwiązanie (które uważam za najłatwiejsze i wolne od wycieków pamięci), które działa idealnie do aktualizacji wybranego elementu ViewModel z wybranego elementu View.

Należy pamiętać, że zmiana wybranego elementu z ViewModel nie spowoduje aktualizacji wybranego elementu w 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;
    }
}

Wykorzystanie XAML

<l:TreeViewEx ItemsSource="{Binding Path=Items}" SelectedItemEx="{Binding Path=SelectedItem}" >
Kino101
źródło