Asked  6 Months ago    Answers:  5   Viewed   27 times

How can I retrieve the item that is selected in a WPF-treeview? I want to do this in XAML, because I want to bind it.

You might think that it is SelectedItem but apparently that does not exist is readonly and therefore unusable.

This is what I want to do:

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

I want to bind the SelectedItem to a property on my Model.

But this gives me the error:

'SelectedItem' property is read-only and cannot be set from markup.

Edit: Ok, this is the way that I solved this:

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

and in the codebehindfile of my xaml:

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

 Answers

65

I realise this has already had an answer accepted, but I put this together to solve the problem. It uses a similar idea to Delta's solution, but without the need to subclass the 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;
    }
}

You can then use this in your XAML as:

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

Hopefully it will help someone!

Tuesday, June 1, 2021
 
Sufi
answered 6 Months ago
62

You should not really need to deal with the SelectedItem property directly, bind IsSelected to a property on your viewmodel and keep track of the selected item there.

A sketch:

<TreeView ItemsSource="{Binding TreeData}">
    <TreeView.ItemContainerStyle>
        <Style TargetType="{x:Type TreeViewItem}">
            <Setter Property="IsSelected" Value="{Binding IsSelected}" />
        </Style>
    </TreeView.ItemContainerStyle>
</TreeView>
public class TViewModel : INotifyPropertyChanged
{
    private static object _selectedItem = null;
    // This is public get-only here but you could implement a public setter which
    // also selects the item.
    // Also this should be moved to an instance property on a VM for the whole tree, 
    // otherwise there will be conflicts for more than one tree.
    public static object SelectedItem
    {
        get { return _selectedItem; }
        private set
        {
            if (_selectedItem != value)
            {
                _selectedItem = value;
                OnSelectedItemChanged();
            }
        }
    }

    static virtual void OnSelectedItemChanged()
    {
        // Raise event / do other things
    }

    private bool _isSelected;
    public bool IsSelected
    {
        get { return _isSelected; }
        set
        {
            if (_isSelected != value)
            {
                _isSelected = value;
                OnPropertyChanged("IsSelected");
                if (_isSelected)
                {
                    SelectedItem = this;
                }
            }
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
    protected virtual void OnPropertyChanged(string propertyName)
    {
        var handler = this.PropertyChanged;
        if (handler != null)
            handler(this, new PropertyChangedEventArgs(propertyName));
    }
}
Thursday, June 3, 2021
 
fret
answered 6 Months ago
45

Here is an improved version of the above mentioned attached behavior. It fully supports twoway binding and also works with HeriarchicalDataTemplate and TreeViews where its items are virtualized. Please note though that to find the 'TreeViewItem' that needs to be selected, it will realize (i.e. create) the virtualized TreeViewItems until it finds the right one. This could potentially be a performance problem with big virtualized trees.

/// <summary>
///     Behavior that makes the <see cref="System.Windows.Controls.TreeView.SelectedItem" /> bindable.
/// </summary>
public class BindableSelectedItemBehavior : Behavior<TreeView>
{
    /// <summary>
    ///     Identifies the <see cref="SelectedItem" /> dependency property.
    /// </summary>
    public static readonly DependencyProperty SelectedItemProperty =
        DependencyProperty.Register(
            "SelectedItem",
            typeof(object),
            typeof(BindableSelectedItemBehavior),
            new UIPropertyMetadata(null, OnSelectedItemChanged));

    /// <summary>
    ///     Gets or sets the selected item of the <see cref="TreeView" /> that this behavior is attached
    ///     to.
    /// </summary>
    public object SelectedItem
    {
        get
        {
            return this.GetValue(SelectedItemProperty);
        }

        set
        {
            this.SetValue(SelectedItemProperty, value);
        }
    }

    /// <summary>
    ///     Called after the behavior is attached to an AssociatedObject.
    /// </summary>
    /// <remarks>
    ///     Override this to hook up functionality to the AssociatedObject.
    /// </remarks>
    protected override void OnAttached()
    {
        base.OnAttached();
        this.AssociatedObject.SelectedItemChanged += this.OnTreeViewSelectedItemChanged;
    }

    /// <summary>
    ///     Called when the behavior is being detached from its AssociatedObject, but before it has
    ///     actually occurred.
    /// </summary>
    /// <remarks>
    ///     Override this to unhook functionality from the AssociatedObject.
    /// </remarks>
    protected override void OnDetaching()
    {
        base.OnDetaching();
        if (this.AssociatedObject != null)
        {
            this.AssociatedObject.SelectedItemChanged -= this.OnTreeViewSelectedItemChanged;
        }
    }

    private static Action<int> GetBringIndexIntoView(Panel itemsHostPanel)
    {
        var virtualizingPanel = itemsHostPanel as VirtualizingStackPanel;
        if (virtualizingPanel == null)
        {
            return null;
        }

        var method = virtualizingPanel.GetType().GetMethod(
            "BringIndexIntoView",
            BindingFlags.Instance | BindingFlags.NonPublic,
            Type.DefaultBinder,
            new[] { typeof(int) },
            null);
        if (method == null)
        {
            return null;
        }

        return i => method.Invoke(virtualizingPanel, new object[] { i });
    }

    /// <summary>
    /// Recursively search for an item in this subtree.
    /// </summary>
    /// <param name="container">
    /// The parent ItemsControl. This can be a TreeView or a TreeViewItem.
    /// </param>
    /// <param name="item">
    /// The item to search for.
    /// </param>
    /// <returns>
    /// The TreeViewItem that contains the specified item.
    /// </returns>
    private static TreeViewItem GetTreeViewItem(ItemsControl container, object item)
    {
        if (container != null)
        {
            if (container.DataContext == item)
            {
                return container as TreeViewItem;
            }

            // Expand the current container
            if (container is TreeViewItem && !((TreeViewItem)container).IsExpanded)
            {
                container.SetValue(TreeViewItem.IsExpandedProperty, true);
            }

            // Try to generate the ItemsPresenter and the ItemsPanel.
            // by calling ApplyTemplate.  Note that in the 
            // virtualizing case even if the item is marked 
            // expanded we still need to do this step in order to 
            // regenerate the visuals because they may have been virtualized away.
            container.ApplyTemplate();
            var itemsPresenter =
                (ItemsPresenter)container.Template.FindName("ItemsHost", container);
            if (itemsPresenter != null)
            {
                itemsPresenter.ApplyTemplate();
            }
            else
            {
                // The Tree template has not named the ItemsPresenter, 
                // so walk the descendents and find the child.
                itemsPresenter = container.GetVisualDescendant<ItemsPresenter>();
                if (itemsPresenter == null)
                {
                    container.UpdateLayout();
                    itemsPresenter = container.GetVisualDescendant<ItemsPresenter>();
                }
            }

            var itemsHostPanel = (Panel)VisualTreeHelper.GetChild(itemsPresenter, 0);

            // Ensure that the generator for this panel has been created.
#pragma warning disable 168
            var children = itemsHostPanel.Children;
#pragma warning restore 168

            var bringIndexIntoView = GetBringIndexIntoView(itemsHostPanel);
            for (int i = 0, count = container.Items.Count; i < count; i++)
            {
                TreeViewItem subContainer;
                if (bringIndexIntoView != null)
                {
                    // Bring the item into view so 
                    // that the container will be generated.
                    bringIndexIntoView(i);
                    subContainer =
                        (TreeViewItem)container.ItemContainerGenerator.
                                                ContainerFromIndex(i);
                }
                else
                {
                    subContainer =
                        (TreeViewItem)container.ItemContainerGenerator.
                                                ContainerFromIndex(i);

                    // Bring the item into view to maintain the 
                    // same behavior as with a virtualizing panel.
                    subContainer.BringIntoView();
                }

                if (subContainer == null)
                {
                    continue;
                }

                // Search the next level for the object.
                var resultContainer = GetTreeViewItem(subContainer, item);
                if (resultContainer != null)
                {
                    return resultContainer;
                }

                // The object is not under this TreeViewItem
                // so collapse it.
                subContainer.IsExpanded = false;
            }
        }

        return null;
    }

    private static void OnSelectedItemChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
        var item = e.NewValue as TreeViewItem;
        if (item != null)
        {
            item.SetValue(TreeViewItem.IsSelectedProperty, true);
            return;
        }

        var behavior = (BindableSelectedItemBehavior)sender;
        var treeView = behavior.AssociatedObject;
        if (treeView == null)
        {
            // at designtime the AssociatedObject sometimes seems to be null
            return;
        }

        item = GetTreeViewItem(treeView, e.NewValue);
        if (item != null)
        {
            item.IsSelected = true;
        }
    }

    private void OnTreeViewSelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
    {
        this.SelectedItem = e.NewValue;
    }
}

And for the sake of completeness hier is the implementation of GetVisualDescentants:

/// <summary>
///     Extension methods for the <see cref="DependencyObject" /> type.
/// </summary>
public static class DependencyObjectExtensions
{
    /// <summary>
    ///     Gets the first child of the specified visual that is of tyoe <typeparamref name="T" />
    ///     in the visual tree recursively.
    /// </summary>
    /// <param name="visual">The visual to get the visual children for.</param>
    /// <returns>
    ///     The first child of the specified visual that is of tyoe <typeparamref name="T" /> of the
    ///     specified visual in the visual tree recursively or <c>null</c> if none was found.
    /// </returns>
    public static T GetVisualDescendant<T>(this DependencyObject visual) where T : DependencyObject
    {
        return (T)visual.GetVisualDescendants().FirstOrDefault(d => d is T);
    }

    /// <summary>
    ///     Gets all children of the specified visual in the visual tree recursively.
    /// </summary>
    /// <param name="visual">The visual to get the visual children for.</param>
    /// <returns>All children of the specified visual in the visual tree recursively.</returns>
    public static IEnumerable<DependencyObject> GetVisualDescendants(this DependencyObject visual)
    {
        if (visual == null)
        {
            yield break;
        }

        for (var i = 0; i < VisualTreeHelper.GetChildrenCount(visual); i++)
        {
            var child = VisualTreeHelper.GetChild(visual, i);
            yield return child;
            foreach (var subChild in GetVisualDescendants(child))
            {
                yield return subChild;
            }
        }
    }
}
Sunday, August 1, 2021
 
ala
answered 4 Months ago
ala
82

The core issue is to have a Focus() change within an event handler. Postpone the Focus by calling it within a BeginInvoke.

Something like:

delegate void voidDelegate();

private void treeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
    var tree = (TreeView)sender;
    var selectedItem = tree.SelectedItem as Child;
    if (selectedItem != null)
    {
        int selectionStart = scriptTextBox.SelectionStart;
        string selectedText = selectedItem.Name;
        voidDelegate giveFocusDelegate = new  voidDelegate(giveFocus);  
        Dispatcher.BeginInvoke(giveFocusDelegate, new object[] { });
        scriptTextBox.SelectedText = selectedText;         
    }
}

private void giveFocus()
{
    scriptTextBox.Focus();
}    

Should get you closer from your goal.

Edit : How do we know this will work ?

As the documentation for Dispatcher.BeginInvoke says :

The operation is added to the event queue of the Dispatcher at the specified DispatcherPriority.

So whatever the priority of the task where you call beginInvoke, the nearest time when the call can happen is right after the execution of current operation ended : the beginInvoked operation is 'pushed' somewhere on the queue of the dispatcher, which works on a single thread.

Thursday, August 19, 2021
 
Amitai Fensterheim
answered 4 Months ago
40

If I recall correctly, at the conclusion of our last episode, we were using some whimsical WPF control that doesn't let you bind SelectedItems properly, so that's out. But if you can do it, it's by far the best way:

<NonWhimsicalListBox
    ItemsSource="{Binding VNodes}"
    SelectedItems="{Binding SelectedVNodes}"
    />

But if you're using System.Windows.Controls.ListBox, you have to write it yourself using an attached property, which is actually not so bad. There's a lot of code here, but it's almost entirely boilerplate (most of the C# code in this attached property was created by a VS IDE code snippet). Nice thing here is it's general and any random passerby can use it on any ListBox that's got anything in it.

public static class AttachedProperties
{
    #region AttachedProperties.SelectedItems Attached Property
    public static IList GetSelectedItems(ListBox obj)
    {
        return (IList)obj.GetValue(SelectedItemsProperty);
    }

    public static void SetSelectedItems(ListBox obj, IList value)
    {
        obj.SetValue(SelectedItemsProperty, value);
    }

    public static readonly DependencyProperty 
        SelectedItemsProperty =
            DependencyProperty.RegisterAttached(
                "SelectedItems", 
                typeof(IList), 
                typeof(AttachedProperties),
                new PropertyMetadata(null, 
                    SelectedItems_PropertyChanged));

    private static void SelectedItems_PropertyChanged(
        DependencyObject d, 
        DependencyPropertyChangedEventArgs e)
    {
        var lb = d as ListBox;
        IList coll = e.NewValue as IList;

        //  If you want to go both ways and have changes to 
        //  this collection reflected back into the listbox...
        if (coll is INotifyCollectionChanged)
        {
            (coll as INotifyCollectionChanged)
                .CollectionChanged += (s, e3) =>
            {
                //  Haven't tested this branch -- good luck!
                if (null != e3.OldItems)
                    foreach (var item in e3.OldItems)
                        lb.SelectedItems.Remove(item);
                if (null != e3.NewItems)
                    foreach (var item in e3.NewItems)
                        lb.SelectedItems.Add(item);
            };
        }

        if (null != coll)
        {
            if (coll.Count > 0)
            {
                //  Minor problem here: This doesn't work for initializing a 
                //  selection on control creation. 
                //  When I get here, it's because I've initialized the selected 
                //  items collection that I'm binding. But at that point, lb.Items 
                //  isn't populated yet, so adding these items to lb.SelectedItems 
                //  always fails. 
                //  Haven't tested this otherwise -- good luck!
                lb.SelectedItems.Clear();
                foreach (var item in coll)
                    lb.SelectedItems.Add(item);
            }

            lb.SelectionChanged += (s, e2) =>
            {
                if (null != e2.RemovedItems)
                    foreach (var item in e2.RemovedItems)
                        coll.Remove(item);
                if (null != e2.AddedItems)
                    foreach (var item in e2.AddedItems)
                        coll.Add(item);
            };
        }
    }
    #endregion AttachedProperties.SelectedItems Attached Property
}

Assuming AttachedProperties is defined in whatever the "local:" namespace is in your XAML...

<ListBox 
    ItemsSource="{Binding VNodes}" 
    SelectionMode="Extended"
    local:AttachedProperties.SelectedItems="{Binding SelectedVNodes}"
    />

ViewModel:

private ObservableCollection<Node> _selectedVNodes 
    = new ObservableCollection<Node>();
public ObservableCollection<Node> SelectedVNodes
{
    get
    {
        return _selectedVNodes;
    }
}

If you don't want to go there, I can think of threethree and a half straightforward ways of doing this offhand:

  1. When the parent viewmodel creates a VNode, it adds a handler to the new VNode's PropertyChanged event. In the handler, it adds/removes sender from SelectedVNodes according to (bool)e.NewValue

    var newvnode = new VNode();
    newvnode.PropertyChanged += (s,e) => {
        if (e.PropertyName == "IsSelected") {
            if ((bool)e.NewValue) {
                //  If not in SelectedVNodes, add it.
            } else {
                //  If in SelectedVNodes, remove it.
            }
        }
    };
    
    //  blah blah blah
    
  2. Do that event, but instead of adding/removing, just recreate SelectedVNodes:

    var newvnode = new VNode();
    newvnode.PropertyChanged += (s,e) => {
        if (e.PropertyName == "IsSelected") {
            //  Make sure OnPropertyChanged("SelectedVNodes") is happening!
            SelectedVNodes = new ObservableCollection<VNode>(
                    VNodes.Where(vn => vn.IsSelected)
                );
        }
    };
    
  3. Do that event, but don't make SelectedVNodes Observable at all:

    var newvnode = new VNode();
    newvnode.PropertyChanged += (s,e) => {
        if (e.PropertyName == "IsSelected") {
            OnPropertyChanged("SelectedVNodes");
        }
    };
    
    //  blah blah blah much else blah blah
    
    public IEnumerable<VNode> SelectedVNodes {
        get { return VNodes.Where(vn => vn.IsSelected); }
    }
    
  4. Give VNode a Parent property. When the parent viewmodel creates a VNode, it gives each VNode a Parent reference to the owner of SelectedVNodes (presumably itself). In VNode.IsSelected.set, the VNode does the add or remove on Parent.SelectedVNodes.

    //  In class VNode
    private bool _isSelected = false;
    public bool IsSelected {
        get { return _isSelected; }
        set {
            _isSelected = value;
            OnPropertyChanged("IsSelected");
            // Elided: much boilerplate checking for redundancy, null parent, etc.
            if (IsSelected)
                Parent.SelectedVNodes.Add(this);
            else
                Parent.SelectedVNodes.Remove(this);
         }
     }
    

None of the above is a work of art. Version 1 is least bad maybe.

Don't use the IEnumerable one if you've got a very large number of items. On the other hand, it relieves you of the responsibility to make this two-way, i.e. if some consumer messes with SelectedVNodes directly, you should really be handling its CollectionChanged event and updating the VNodes in question. Of course then you have to make sure you don't accidentally recurse: Don't add one to the collection that's already there, and don't set vn.IsSelected = true if vn.IsSelected is true already. If your eyes are glazing over like mine right now and you're starting to feel the walls closing in, allow me to recommend option #3.

Maybe SelectedVNodes should publicly expose ReadOnlyObservableCollection<VNode>, to get you off that hook. In that case number 1 is your best bet, because the VNodes won't have access to the VM's private mutable ObservableCollection<VNode>.

But take your pick.

Sunday, November 21, 2021
 
Santi
answered 1 Week ago
Only authorized users can answer the question. Please sign in first, or register a free account.
Not the answer you're looking for? Browse other questions tagged :
 
Share