1

I’m trying to bind the SelectedItem of a WPF TreeView to a property in my MainViewModel in a pure MVVM setup (no code-behind).

Since TreeView.SelectedItem is not a dependency property, I created an attached property to make it bindable:

public static class TreeViewSelectedItemBehavior
{
    public static readonly DependencyProperty SelectedItemProperty =
        DependencyProperty.RegisterAttached(
            "SelectedItem",
            typeof(object),
            typeof(TreeViewSelectedItemBehavior),
            new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnSelectedItemChanged));

    public static object GetSelectedItem(DependencyObject obj) =>
        obj.GetValue(SelectedItemProperty);

    public static void SetSelectedItem(DependencyObject obj, object value) =>
        obj.SetValue(SelectedItemProperty, value);

    private static void OnSelectedItemChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
        if (sender is TreeView treeView)
        {
            treeView.SelectedItemChanged -= TreeView_SelectedItemChanged;
            treeView.SelectedItemChanged += TreeView_SelectedItemChanged;
        }
    }

    private static void TreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
    {
        if (sender is TreeView treeView)
        {
            SetSelectedItem(treeView, e.NewValue);
        }
    }
}
<TreeView ItemsSource="{Binding tvTodoItems}"
          helpers:TreeViewSelectedItemBehavior.SelectedItem="{Binding SelectedTodoItem, Mode=TwoWay}">
    <TreeView.ItemTemplate>
        <DataTemplate>
            <TextBlock Text="{Binding Name}" />
        </DataTemplate>
    </TreeView.ItemTemplate>
</TreeView>
public class MainViewModel : BaseViewModel
{
    public List<TodoItemViewModel> tvTodoItems { get; set; }

    private TodoItemViewModel _selectedTodoItem;
    public TodoItemViewModel SelectedTodoItem
    {
        get => _selectedTodoItem;
        set
        {
            if (_selectedTodoItem != value)
            {
                _selectedTodoItem = value;
                OnPropertyChanged();
            }
        }
    }
}

public abstract class BaseViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;  
  
        public void OnPropertyChanged(string propertyName)  
        {  
            if (PropertyChanged != null)  
            {  
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));  
            }  
        }  
    }

The problem: When I select a different item in the TreeView, the set accessor of SelectedTodoItem in my ViewModel is never called. The binding does not seem to update from the TreeView to the ViewModel.

Question: How can I bind TreeView.SelectedItem to my ViewModel so that the ViewModel property is updated when the user changes the selection — purely in MVVM, without any code-behind?

Note: This is just the minimal test version. The real TreeView is hierarchical and looks like this:

<TreeView x:Name="NavTree"
          Grid.Column="0" 
          Grid.Row="1"
          ItemsSource="{Binding Spaces}"
          Margin="8" 
          helpers:TreeViewSelectedItemBehavior.SelectedItem="{Binding SelectedItem, Mode=TwoWay}">
    <TreeView.ItemTemplate>
        <HierarchicalDataTemplate ItemsSource="{Binding Folders}">
            <TextBlock Text="{Binding Name}" />
            <HierarchicalDataTemplate.ItemTemplate>
                <HierarchicalDataTemplate ItemsSource="{Binding Lists}">
                    <TextBlock Text="{Binding Name}" />
                </HierarchicalDataTemplate>
            </HierarchicalDataTemplate.ItemTemplate>
        </HierarchicalDataTemplate>
    </TreeView.ItemTemplate>
</TreeView>
2

1 Answer 1

3

In your implementation, the method will not be called, since the initial value of the null binding does not change. And, accordingly, all the other logic does not work.
For testing, you can assign some initial value to the VM property and then your Attached Property option will start working.

    public class MainViewModel : ViewModelBase
    {
        public List<TodoItemViewModel> tvTodoItems { get;  } =
            "WPF strict MVVM: Basic TreeView SelectedItem ViewModel Problem"
            .Split()
            .Select(x => new TodoItemViewModel() { Name =x })
            .ToList();      

        private TodoItemViewModel? _selectedTodoItem;
        public TodoItemViewModel? SelectedTodoItem
        {
            get => _selectedTodoItem;
            set
            {
                if (_selectedTodoItem != value)
                {
                    _selectedTodoItem = value;
                    RaisePropertyChanged();
                }
            }
        }

        public MainViewModel()
        {
            SelectedTodoItem = tvTodoItems[3];
        }
    }

In the working Solution, of course, I do not recommend doing this, since this will lead to the fact that the values of TreeView.SelectedItem and MainViewModel.SelectedTodoItem will be different at the initial moment.

It can often help to initialize the initial value of the Target Property to some closed type that is guaranteed not to be in the source property. Example:

    public static class TreeViewSelectedItemBehavior
    {
        private sealed class NoSelected
        {
            public override string ToString() => "No selected";
        }
        public static readonly DependencyProperty SelectedItemProperty =
            DependencyProperty.RegisterAttached(
                "SelectedItem",
                typeof(object),
                typeof(TreeViewSelectedItemBehavior),
                new FrameworkPropertyMetadata(new NoSelected(),
                                              FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                                              OnSelectedItemChanged));
    public class MainViewModel : ViewModelBase
    {
        public List<TodoItemViewModel> tvTodoItems { get;  } =
            "WPF strict MVVM: Basic TreeView SelectedItem ViewModel Problem"
            .Split()
            .Select(x => new TodoItemViewModel() { Name =x })
            .ToList();      

        private TodoItemViewModel? _selectedTodoItem;
        public TodoItemViewModel? SelectedTodoItem
        {
            get => _selectedTodoItem;
            set
            {
                if (_selectedTodoItem != value)
                {
                    _selectedTodoItem = value;
                    RaisePropertyChanged();
                }
            }
        }

        //public MainViewModel()
        //{
        //    SelectedTodoItem = tvTodoItems[3];
        //}
    }

But in general, for such logic I would recommend using Interaction Behavior.

    public class TreeViewSelectedItemBehavior : Behavior<TreeView>
    {
        public object SelectedItem
        {
            get { return GetValue(SelectedItemProperty); }
            set { SetValue(SelectedItemProperty, value); }
        }

        public static readonly DependencyProperty SelectedItemProperty =
            DependencyProperty.Register(
                "SelectedItem",
                typeof(object),
                typeof(TreeViewSelectedItemBehavior),
                new UIPropertyMetadata(null));

        protected override void OnAttached()
        {
            base.OnAttached();
            AssociatedObject.SelectedItemChanged += OnTreeViewSelectedItemChanged;
            SelectedItem = AssociatedObject.SelectedItem;
        }

        protected override void OnDetaching()
        {
            base.OnDetaching();
            if (AssociatedObject != null)
            {
                AssociatedObject.SelectedItemChanged -= OnTreeViewSelectedItemChanged;
            }
        }
        private void OnTreeViewSelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
        {
            SelectedItem = e.NewValue;
        }
    }
    <TreeView ItemsSource="{Binding tvTodoItems}">
        <i:Interaction.Behaviors>
            <local:TreeViewSelectedItemBehavior SelectedItem="{Binding SelectedTodoItem, Mode=OneWayToSource}"/>
        </i:Interaction.Behaviors>
Sign up to request clarification or add additional context in comments.

1 Comment

Thanks a lot! The Interaction Behavior approach works perfectly for me, even with my HierarchicalDataTemplate setup. Much appreciated!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.