Datenbindung an SelectedItem in einer WPF-Baumansicht


240

Wie kann ich das in einer WPF-Baumansicht ausgewählte Element abrufen? Ich möchte dies in XAML tun, weil ich es binden möchte.

Sie könnten denken, dass es SelectedItemaber anscheinend nicht existiert, ist schreibgeschützt und daher unbrauchbar.

Folgendes möchte ich tun:

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

Ich möchte das SelectedIteman eine Eigenschaft in meinem Modell binden .

Aber das gibt mir den Fehler:

Die Eigenschaft 'SelectedItem' ist schreibgeschützt und kann nicht über das Markup festgelegt werden.

Edit: Ok, so habe ich das gelöst:

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

und in der codebehindfile meiner xaml:

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

50
Mann das ist scheiße. Es hat mich auch getroffen. Ich bin hierher gekommen in der Hoffnung herauszufinden, dass es einen anständigen Weg gibt und ich bin nur ein Idiot. Dies ist das erste Mal, dass ich traurig bin, dass ich kein Idiot bin.
Andrei Rînea

6
das ist wirklich scheiße und bringt das Bindungskonzept durcheinander
Delta

Hoffe, dies könnte jemandem helfen
jacob aloysious

9
In Bezug auf Bindung und MVVM ist der Code dahinter nicht "verboten", sondern der Code dahinter sollte die Ansicht unterstützen. Meiner Meinung nach ist der Code dahinter bei all den anderen Lösungen, die ich gesehen habe, eine weitaus bessere Option, da es immer noch darum geht, die Ansicht an das Ansichtsmodell zu "binden". Der einzige Nachteil ist, dass wenn Sie ein Team mit einem Designer haben, der nur in XAML arbeitet, der Code dahinter beschädigt / vernachlässigt werden kann. Es ist ein kleiner Preis für eine Lösung, deren Implementierung 10 Sekunden dauert.
Nrjohnstone

Eine der einfachsten Lösungen wahrscheinlich: stackoverflow.com/questions/1238304/…
JoanComasFdz

Antworten:


240

Mir ist klar, dass dies bereits eine Antwort akzeptiert hat, aber ich habe dies zusammengestellt, um das Problem zu lösen. Es verwendet eine ähnliche Idee wie die Lösung von Delta, ohne dass die TreeView in Unterklassen unterteilt werden muss:

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

Sie können dies dann in Ihrer XAML verwenden als:

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

Hoffentlich hilft es jemandem!


5
Wie Brent betonte, musste ich der Bindung auch Mode = TwoWay hinzufügen. Ich bin kein "Blender", daher war ich mit der Behavior <> -Klasse von System.Windows.Interactivity nicht vertraut. Die Assembly ist Teil von Expression Blend. Für diejenigen, die keine Testversion kaufen / installieren möchten, um diese Assembly zu erhalten, können Sie das BlendSDK herunterladen, das System.Windows.Interactivity enthält. BlendSDK 3 für 3.5 ... Ich denke, es ist BlendSDK 4 für 4.0. Hinweis: Auf diese Weise können Sie nur das ausgewählte Element
Mike Rowley,

4
Sie können UIPropertyMetadata auch durch FrameworkPropertyMetadata ersetzen (null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnSelectedItemChanged));
Filimindji

3
Dies wäre ein Ansatz, um das Problem zu lösen: stackoverflow.com/a/18700099/4227
bitbonk

2
@Lukas genau wie im obigen XAML-Code-Snippet gezeigt. Ersetzen Sie einfach {Binding SelectedItem, Mode=TwoWay}mit{Binding MyViewModelField, Mode=TwoWay}
Steve Greatrex

4
@Pascal es istxmlns:e="http://schemas.microsoft.com/expression/2010/interactivity"
Steve Greatrex

46

Diese Eigenschaft ist vorhanden: TreeView.SelectedItem

Es ist jedoch schreibgeschützt, sodass Sie es nicht über eine Bindung zuweisen, sondern nur abrufen können


Ich akzeptiere diese Antwort, weil ich dort diesen Link gefunden habe, der zu meiner eigenen Antwort führte: msdn.microsoft.com/en-us/library/ms788714.aspx
Natrium

1
Kann sich dies TreeView.SelectedItemauf eine Eigenschaft des Modells auswirken, wenn der Benutzer ein Element (auch bekannt als OneWayToSource) auswählt ?
Shimmy Weitzhandler

43

Antworten Sie mit angehängten Eigenschaften und ohne externe Abhängigkeiten, falls dies jemals erforderlich sein sollte!

Sie können eine angehängte Eigenschaft erstellen, die bindbar ist und einen Getter und einen Setter enthält:

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

Fügen Sie die Namespace-Deklaration, die diese Klasse enthält, zu Ihrer XAML hinzu und binden Sie sie wie folgt (lokal habe ich die Namespace-Deklaration benannt):

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

    </TreeView>

Jetzt können Sie das ausgewählte Element binden und es in Ihrem Ansichtsmodell so einstellen, dass es programmgesteuert geändert wird, falls diese Anforderung jemals auftritt. Dies setzt natürlich voraus, dass Sie INotifyPropertyChanged für diese bestimmte Eigenschaft implementieren.


4
+1, beste Antwort in diesem Thread imho. Keine Abhängigkeit von System.Windows.Interactivity und ermöglicht bidirektionale Bindung (programmgesteuerte Einstellung in einer MVVM-Umgebung). Perfekt.
Chris Ray

5
Ein Problem bei diesem Ansatz ist, dass das Verhalten erst dann funktioniert, wenn das ausgewählte Element einmal über die Bindung festgelegt wurde (dh über das ViewModel). Wenn der Anfangswert in der VM null ist, aktualisiert die Bindung den DP-Wert nicht und das Verhalten wird nicht aktiviert. Sie können dies mit einem anderen ausgewählten Standardelement (z. B. einem ungültigen Element) beheben.
Mark

6
@Mark: Verwenden Sie einfach das neue Objekt () anstelle der obigen Null, wenn Sie die UIPropertyMetadata der angehängten Eigenschaft instanziieren. Das Problem sollte dann weg sein ...
barnacleboy

2
Die Umwandlung in TreeViewItem schlägt für mich fehl, da ich eine HierarchicalDataTemplate verwende, die von Ressourcen nach Datentyp angewendet wird. Wenn Sie jedoch ChangeSelectedItem entfernen, funktioniert das Binden an ein Ansichtsmodell und das Abrufen des Elements einwandfrei.
Casey Sebben

1
Ich habe auch Probleme mit der Besetzung von TreeViewItem. Zu diesem Zeitpunkt enthält ItemContainerGenerator nur Verweise auf die Stammelemente, aber ich benötige es, um auch Nicht-Stammelemente abrufen zu können. Wenn Sie einen Verweis auf einen übergeben, schlägt die Umwandlung fehl und gibt null zurück. Nicht sicher, wie dies behoben werden könnte?
Bob Tway

39

Nun, ich habe eine Lösung gefunden. Es bewegt das Chaos, so dass MVVM funktioniert.

Fügen Sie zuerst diese Klasse hinzu:

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

und füge dies deinem xaml hinzu:

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

3
Dies ist die EINZIGE Sache, die bisher fast für mich gearbeitet hat. Diese Lösung gefällt mir sehr gut.
Rachael

1
Ich weiß nicht warum, aber es hat bei mir nicht funktioniert :( Es ist mir gelungen, das ausgewählte Element aus dem Baum zu holen, aber nicht umgekehrt - um das ausgewählte Element von außerhalb des Baums zu ändern.
Erez

Es wäre etwas ordentlicher, die Abhängigkeitseigenschaft als BindsTwoWayByDefault festzulegen, dann müssten Sie TwoWay nicht in der XAML angeben
Stephen Holt

Dies ist der beste Ansatz. Es wird keine Interaktivitätsreferenz verwendet, es wird kein Code dahinter verwendet, es gibt keinen Speicherverlust wie bei einigen Verhaltensweisen. Danke dir.
Alexandru Dicu

Wie bereits erwähnt, funktioniert diese Lösung nicht mit 2-Wege-Bindung. Wenn Sie den Wert im Ansichtsmodell festlegen, wird die Änderung nicht in die Baumansicht übertragen.
Richard Moore

25

Es antwortet etwas mehr als das OP erwartet ... Aber ich hoffe, es könnte zumindest jemandem helfen.

Wenn Sie a ausführen möchten, ICommandwann immer dies SelectedItemgeändert wurde, können Sie einen Befehl an ein Ereignis binden, und die Verwendung einer Eigenschaft SelectedItemin der ViewModelwird nicht mehr benötigt.

Um dies zu tun:

1- Verweis auf hinzufügen System.Windows.Interactivity

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

2- Binden Sie den Befehl an das Ereignis 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
Die Referenz System.Windows.Interactivitykann von NuGet installiert werden: nuget.org/packages/System.Windows.Interactivity.WPF
Junle Li

Ich habe stundenlang versucht, dieses Problem zu lösen. Ich habe dies implementiert, aber mein Befehl funktioniert nicht. Könnten Sie mir bitte helfen?
Alfie

1
Ende 2018 wurde von Microsoft das XAML-Verhalten für WPF eingeführt. Es kann anstelle des verwendet werden System.Windows.Interactivity. Es wurde für mich gearbeitet (versucht mit dem .NET Core-Projekt). Um die Dinge einzurichten, fügen Sie einfach das Microsoft.Xaml.Behaviors.Wpf- Nuget-Paket hinzu und ändern Sie den Namespace in xmlns:i="http://schemas.microsoft.com/xaml/behaviors". Weitere Informationen finden Sie im Blog
rychlmoj

19

Dies kann auf eine "schönere" Weise erreicht werden, indem nur die Bindung und der EventToCommand der GalaSoft MVVM Light-Bibliothek verwendet werden. Fügen Sie in Ihrer VM einen Befehl hinzu, der aufgerufen wird, wenn das ausgewählte Element geändert wird, und initialisieren Sie den Befehl, um die erforderlichen Aktionen auszuführen. In diesem Beispiel habe ich einen RelayCommand verwendet und werde nur die SelectedCluster-Eigenschaft festlegen.

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

Fügen Sie dann das EventToCommand-Verhalten in Ihre xaml ein. Dies ist wirklich einfach mit Blend.

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

Dies ist eine gute Lösung, insbesondere wenn Sie bereits das MvvmLight-Toolkit verwenden. Es löst jedoch nicht das Problem, den ausgewählten Knoten festzulegen und die Auswahl durch die Baumansicht zu aktualisieren.
Keft

12

Alles zu kompliziert ... Gehen Sie mit Caliburn Micro (http://caliburnmicro.codeplex.com/)

Aussicht:

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

ViewModel:

public void SetSelectedItem(YourNodeViewModel item) {}; 

5
Ja ... und wo ist der Teil , dass Sätze SelectedItem auf TreeView ?
Mnn

Caliburn ist schön und elegant. Funktioniert ganz einfach für verschachtelte Hierarchien
Purusartha

8

Ich bin auf diese Seite gestoßen, um nach der gleichen Antwort wie der ursprüngliche Autor zu suchen, und um zu beweisen, dass es immer mehr als einen Weg gibt, war die Lösung für mich noch einfacher als die Antworten, die hier bisher gegeben wurden, also dachte ich mir, ich könnte sie genauso gut hinzufügen auf den Stapel.

Die Motivation für die Bindung ist es, es schön zu halten & MVVM. Die wahrscheinliche Verwendung des ViewModel besteht darin, eine Eigenschaft mit einem Namen wie "CurrentThingy" zu haben, und an anderer Stelle ist der DataContext für eine andere Sache an "CurrentThingy" gebunden.

Anstatt zusätzliche Schritte durchzuführen (z. B. benutzerdefiniertes Verhalten, Steuerung durch Dritte), um eine nette Bindung von TreeView zu meinem Modell und dann von etwas anderem zu meinem Modell zu unterstützen, bestand meine Lösung darin, eine einfache Elementbindung zu verwenden, an die das andere Ding gebunden wird TreeView.SelectedItem, anstatt das andere Ding an mein ViewModel zu binden, wodurch die zusätzliche Arbeit übersprungen wird.

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

Dies ist natürlich großartig, um das aktuell ausgewählte Element zu lesen, aber nicht einzustellen, was alles ist, was ich brauche.


1
Was ist lokal: MyThingyDetailsView? Ich erhalte das Lokal: MyThingyDetailsView enthält das ausgewählte Element, aber wie erhält Ihr Ansichtsmodell diese Informationen? Das sieht nach einer schönen, sauberen Art aus, aber ich brauche nur ein bisschen mehr Infos ...
Bob Horn

local: MyThingyDetailsView ist einfach ein UserControl voller XAML, das eine Detailansicht über eine "thingy" -Instanz erstellt. Es ist in der Mitte einer anderen Ansicht als Inhalt eingebettet, wobei der Datenkontext dieser Ansicht das aktuell ausgewählte Baumansichtselement unter Verwendung der Elementbindung ist.
Wes

6

Möglicherweise können Sie auch die TreeViewItem.IsSelected-Eigenschaft verwenden


Ich denke, das könnte die richtige Antwort sein. Ich würde jedoch gerne ein Beispiel oder eine Best-Practice-Empfehlung sehen, wie die IsSelected-Eigenschaft der Elemente an TreeView übergeben wird.
Anhoppe

3

Es gibt auch eine Möglichkeit, eine XAML-bindbare SelectedItem-Eigenschaft zu erstellen, ohne Interaction.Behaviors zu verwenden.

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
}

Sie können dies dann in Ihrer XAML verwenden als:

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

3

Ich habe alle Lösungen dieser Fragen ausprobiert. Niemand hat mein Problem vollständig gelöst. Ich denke, es ist besser, eine solche geerbte Klasse mit der neu definierten Eigenschaft SelectedItem zu verwenden. Es funktioniert perfekt, wenn Sie ein Baumelement aus der GUI auswählen und diesen Eigenschaftswert in Ihrem Code festlegen

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

Es wäre viel schneller, wenn UpdateLayout () und IsExpanded für einige Knoten nicht aufgerufen würden. Wann muss UpdateLayout () und IsExpanded nicht aufgerufen werden? Wenn das Baumelement zuvor besucht wurde. Woher weiß ich das? ContainerFromItem () gibt null für nicht besuchte Knoten zurück. Daher können wir den übergeordneten Knoten nur erweitern, wenn ContainerFromItem () für untergeordnete Elemente null zurückgibt.
CoperNick

3

Meine Anforderung war eine PRISM-MVVM-basierte Lösung, bei der eine TreeView benötigt wurde und das gebundene Objekt vom Typ Collection <> ist und daher HierarchicalDataTemplate benötigt. Das Standard-BindableSelectedItemBehavior kann das untergeordnete TreeViewItem nicht identifizieren. Damit es in diesem Szenario funktioniert.

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

Dies ermöglicht es, alle Elemente unabhängig von der Ebene zu durchlaufen.


Danke dir! Dies war das einzige, das für mein Szenario funktioniert, das Ihrem nicht unähnlich ist.
Robert

Funktioniert sehr gut und führt nicht dazu, dass die ausgewählten / erweiterten Bindungen verwirrt werden .
Rusty

2

Ich schlage eine Ergänzung zu dem Verhalten von Steve Greatrex vor. Sein Verhalten spiegelt keine Änderungen gegenüber der Quelle wider, da es sich möglicherweise nicht um eine Sammlung von TreeViewItems handelt. Es geht also darum, das TreeViewItem im Baum zu finden, dessen Datenkontext der ausgewählte Wert aus der Quelle ist. Die TreeView verfügt über eine geschützte Eigenschaft namens "ItemsHost", die die TreeViewItem-Auflistung enthält. Wir können es durch Reflexion bekommen und über den Baum nach dem ausgewählten Gegenstand suchen.

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

Auf diese Weise funktioniert das Verhalten für bidirektionale Bindungen. Alternativ ist es möglich, die ItemsHost-Erfassung in die OnAttached-Methode des Verhaltens zu verschieben, wodurch der Aufwand für die Verwendung von Reflection bei jeder Aktualisierung der Bindung gespart wird.


2

WPF MVVM TreeView SelectedItem

... ist eine bessere Antwort, erwähnt jedoch keine Möglichkeit, das SelectedItem im ViewModel abzurufen / festzulegen.

  1. Fügen Sie Ihrem ItemViewModel eine boolesche IsSelected-Eigenschaft hinzu und binden Sie sie in einem Style Setter für das TreeViewItem daran.
  2. Fügen Sie Ihrem ViewModel eine SelectedItem-Eigenschaft hinzu, die als DataContext für die TreeView verwendet wird. Dies ist das fehlende Teil in der obigen Lösung.
    'ItemVM ...
    Öffentliches Eigentum wird als Boolescher Wert ausgewählt
        Bekommen
            Rückgabe _func.SelectedNode Is Me
        End Get
        Set (Wert als Boolescher Wert)
            Wenn IsSelected Wert Dann
                _func.SelectedNode = If (Wert, Me, Nothing)
            End If
            RaisePropertyChange ()
        End Set
    Endeigenschaft
    'TreeVM ...
    Öffentliches Eigentum SelectedItem As ItemVM
        Bekommen
            Geben Sie _selectedItem zurück
        End Get
        Set (Wert As ItemVM)
            Wenn _selectedItem Wert ist, dann
                Rückkehr
            End If
            Dim prev = _selectedItem
            _selectedItem = Wert
            Wenn prev IsNot Nothing Then
                prev.IsSelected = False
            End If
            Wenn _selectedItem nichts ist, dann
                _selectedItem.IsSelected = True
            End If
        End Set
    Endeigenschaft
<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

Nachdem ich einen Tag lang im Internet studiert hatte, fand ich meine eigene Lösung für die Auswahl eines Elements, nachdem ich eine normale Baumansicht in einer normalen WPF / C # -Umgebung erstellt hatte

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

Dies kann auch mit der IsSelected-Eigenschaft des TreeView-Elements erfolgen. So habe ich es geschafft:

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

Abonnieren Sie dann im ViewModel, das die Daten enthält, an die Ihre TreeView gebunden ist, einfach das Ereignis in der TreeViewItem-Klasse.

TreeViewItem.OnItemSelected += TreeViewItemSelected;

Implementieren Sie diesen Handler schließlich im selben ViewModel.

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

Und die Bindung natürlich,

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

Dies ist eigentlich eine unterbewertete Lösung. Indem Sie Ihre Denkweise ändern und die IsSelected-Eigenschaft jedes Treeview-Elements binden und die IsSelected-Ereignisse in die Luft sprudeln lassen, können Sie integrierte Funktionen verwenden, die gut mit bidirektionaler Bindung funktionieren. Ich habe viele Lösungsvorschläge für dieses Problem ausprobiert, und dies ist das erste, das funktioniert hat. Nur ein wenig komplex zu verkabeln. Vielen Dank.
Richard Moore

1

Ich weiß, dass dieser Thread 10 Jahre alt ist, aber das Problem besteht immer noch ....

Die ursprüngliche Frage lautete "Abrufen" des ausgewählten Elements. Ich musste auch das ausgewählte Element in meinem Ansichtsmodell "abrufen" (nicht festlegen). Von allen Antworten in diesem Thread ist die von 'Wes' die einzige, die das Problem anders angeht: Wenn Sie das 'Ausgewählte Element' als Ziel für die Datenbindung verwenden können, verwenden Sie es als Quelle für die Datenbindung. Wes hat es mit einer anderen view-Eigenschaft gemacht, ich werde es mit einer viewmodel-Eigenschaft machen:

Wir brauchen zwei Dinge:

  • Erstellen Sie eine Abhängigkeitseigenschaft im Ansichtsmodell (in meinem Fall vom Typ 'MyObject', da meine Baumansicht an ein Objekt vom Typ 'MyObject' gebunden ist).
  • Binden Sie vom Treeview.SelectedItem an diese Eigenschaft im Konstruktor der Ansicht (ja, das ist Code dahinter, aber es ist wahrscheinlich, dass Sie dort auch Ihren Datenkontext initiieren).

Ansichtsmodell:

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

Konstruktor anzeigen:

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

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

0

(Lassen Sie uns einfach alle zustimmen, dass TreeView in Bezug auf dieses Problem offensichtlich kaputt ist. Die Bindung an SelectedItem wäre offensichtlich gewesen. Seufz )

Ich brauchte die Lösung, um richtig mit der IsSelected-Eigenschaft von TreeViewItem zu interagieren. So habe ich es gemacht:

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

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

Ich bringe Ihnen meine Lösung, die folgende Funktionen bietet:

  • Unterstützt 2 Bindungsmöglichkeiten

  • Automatische Aktualisierung der TreeViewItem.IsSelected-Eigenschaften (gemäß SelectedItem)

  • Keine TreeView-Unterklasse

  • An ViewModel gebundene Elemente können von einem beliebigen Typ sein (sogar null).

1 / Fügen Sie den folgenden Code in Ihr CS ein:

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 / Anwendungsbeispiel in Ihrer XAML-Datei

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

0

Ich schlage diese Lösung vor (die meiner Meinung nach am einfachsten ist und Speicherlecks freisetzt), die perfekt zum Aktualisieren des ausgewählten Elements des ViewModel aus dem ausgewählten Element der Ansicht geeignet ist.

Beachten Sie, dass durch Ändern des ausgewählten Elements im ViewModel das ausgewählte Element der Ansicht nicht aktualisiert wird.

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

XAML-Nutzung

<l:TreeViewEx ItemsSource="{Binding Path=Items}" SelectedItemEx="{Binding Path=SelectedItem}" >
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.