Search code examples
c#wpfmvvmtreeviewitem

TreeViewItem MVVM IsSelected in Xaml doesn't send value to View-Model


I have a problem with IsSelected property. It doesn't send values from view to view-model. I posted my code below ViewModel:

public class Viewmodel : INotifyPropertyChanged
{
    private ObservableCollection<int> seznam;
    public ObservableCollection<int> Seznam
    {
        get { return seznam; }
        set
        {
            seznam = value;
        }
    }

    public Viewmodel()
    {
        Seznam = new ObservableCollection<int>();
        for (int i = 0; i < 3; i++)
        {
            Seznam.Add(i);
        }
    }

    bool isSelected;
    public bool IsSelected
    {
        get { return isSelected; }
        set
        {
            isSelected = value;
        }
    }
    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged(string propertyName)
    {
        if (this.PropertyChanged != null)
            this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
}

View:

        <TreeView ItemsSource="{Binding Seznam}">
        <TreeView.ItemContainerStyle>
            <Style TargetType="{x:Type TreeViewItem}">
                <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}"/>
            </Style>
        </TreeView.ItemContainerStyle>
    </TreeView>

It still doesn't stop at breakpoint that I put on get { return isSelected; }


Solution

  • With your updated post, it is clear that you haven't implemented your view models correctly. In particular, your TreeView.ItemsSource is bound to the Seznam property of your only view model. This is a collection of int values.

    This means that the data context for each item container in the TreeView, where you attempt to bind to the IsSelected property, is an int value. And of course, int values don't even have an IsSelected property.

    (By the way, I am skeptical about your claim that "There are no binding errors". If you looked at the debug output, you certainly should have seen a binding error, where attempting to bind to the non-existent IsSelected property.)

    And think about this for a moment: supposing the item container did manage to bind to the Viewmodel.IsSelected property. How many item containers do you think there are? And how many instances of Viewmodel do you think there are? You should believe that there are many item containers, i.e. one for each item in your collection. And that there is just one instance of Viewmodel. So, how would all those items' selection state even map to the single Viewmodel.IsSelected property?

    The right way to do this would be to create a separate view model object for the collection, with a property for your int value, as well as properties for the IsSelected and IsExpanded states (since you originally had mentioned wanting both).

    Here is the example I wrote earlier just to prove to myself that the usual approach would work as expected. You should not have any trouble adapting it to suit your needs…

    Per-item view model:

    class TreeItemViewModel : NotifyPropertyChangedBase
    {
        public ObservableCollection<TreeItemViewModel> Items { get; }
            = new ObservableCollection<TreeItemViewModel>();
    
        private bool _isSelected;
        public bool IsSelected
        {
            get { return _isSelected; }
            set { _UpdateField(ref _isSelected, value, _OnBoolPropertyChanged); }
        }
    
        private bool _isExpanded;
        public bool IsExpanded
        {
            get { return _isExpanded; }
            set { _UpdateField(ref _isExpanded, value, _OnBoolPropertyChanged); }
        }
    
        private void _OnBoolPropertyChanged(bool obj)
        {
            _RaisePropertyChanged(nameof(FullText));
        }
    
        private string _text;
        public string Text
        {
            get { return _text; }
            set { _UpdateField(ref _text, value, _OnTextChanged); }
        }
    
        private void _OnTextChanged(string obj)
        {
            _RaisePropertyChanged(nameof(FullText));
        }
    
        public string FullText
        {
            get { return $"{Text} (IsSelected: {IsSelected}, IsExpanded: {IsExpanded})"; }
        }
    }
    

    Main view model for window:

    class MainViewModel : NotifyPropertyChangedBase
    {
        public ObservableCollection<TreeItemViewModel> Items { get; }
            = new ObservableCollection<TreeItemViewModel>();
    
        public ICommand ClearSelection { get; }
    
        public MainViewModel()
        {
            ClearSelection = new ClearSelectionCommand(this);
        }
    
        class ClearSelectionCommand : ICommand
        {
            private readonly MainViewModel _parent;
    
            public ClearSelectionCommand(MainViewModel parent)
            {
                _parent = parent;
            }
    
    #pragma warning disable 67
            public event EventHandler CanExecuteChanged;
    #pragma warning restore 67
    
            public bool CanExecute(object parameter)
            {
                return true;
            }
    
            public void Execute(object parameter)
            {
                _parent._ClearSelection();
            }
        }
    
        private void _ClearSelection()
        {
            _ClearSelection(Items);
        }
    
        private static void _ClearSelection(IEnumerable<TreeItemViewModel> collection)
        {
            foreach (TreeItemViewModel item in collection)
            {
                _ClearSelection(item.Items);
                item.IsSelected = false;
                item.IsExpanded = false;
            }
        }
    }
    

    XAML for window:

    <Window x:Class="TestSO44513864TreeViewIsSelected.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:p="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
            xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
            xmlns:l="clr-namespace:TestSO44513864TreeViewIsSelected"
            mc:Ignorable="d"
            Title="MainWindow" Height="350" Width="525">
      <Window.DataContext>
        <l:MainViewModel>
          <l:MainViewModel.Items>
            <l:TreeItemViewModel Text="One">
              <l:TreeItemViewModel.Items>
                <l:TreeItemViewModel Text="One A"/>
                <l:TreeItemViewModel Text="One B"/>
              </l:TreeItemViewModel.Items>
            </l:TreeItemViewModel>
            <l:TreeItemViewModel Text="Two"/>
            <l:TreeItemViewModel Text="Three"/>
          </l:MainViewModel.Items>
        </l:MainViewModel>
      </Window.DataContext>
    
      <Grid>
        <Grid.RowDefinitions>
          <RowDefinition Height="Auto"/>
          <RowDefinition/>
        </Grid.RowDefinitions>
        <Button Content="Clear Selection" Command="{Binding ClearSelection}"
                HorizontalAlignment="Left"/>
        <TreeView ItemsSource="{Binding Items}" Grid.Row="1">
          <TreeView.ItemContainerStyle>
            <p:Style TargetType="TreeViewItem">
              <Setter Property="IsSelected" Value="{Binding IsSelected}"/>
              <Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}"/>
            </p:Style>
          </TreeView.ItemContainerStyle>
          <TreeView.ItemTemplate>
            <HierarchicalDataTemplate DataType="l:TreeItemViewModel"
                                      ItemsSource="{Binding Items}">
              <TextBlock Text="{Binding FullText}"/>
            </HierarchicalDataTemplate>
          </TreeView.ItemTemplate>
        </TreeView>
      </Grid>
    </Window>
    

    And for completeness…

    The boilerplate base class for INotifyPropertyChanged implementation:

    class NotifyPropertyChangedBase : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
    
        protected void _UpdateField<T>(ref T field, T newValue,
            Action<T> onChangedCallback = null,
            [CallerMemberName] string propertyName = null)
        {
            if (EqualityComparer<T>.Default.Equals(field, newValue))
            {
                return;
            }
    
            T oldValue = field;
    
            field = newValue;
            onChangedCallback?.Invoke(oldValue);
            _RaisePropertyChanged(propertyName);
        }
    
        protected void _RaisePropertyChanged(string propertyName)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }