Tuesday, May 26, 2009

Programmatically changing TreeViewItem IsSelected fails to set IsFocused

I was having an odd problem with the IsSelected property of TreeViewItems. After adding an item and setting its IsSelected property, the originally selected item is no longer selectable.

I am using the Model-View-ViewModel pattern and a HierarchicalDataTemplate to bind a TreeView. The ItemsSource is an ObservableCollection. I noticed that when I added a new item to the collection (making it IsExpanded and IsSelected) it was added fine and was selected, but the parent item was no longer selectable. Any other item in the TreeView was selectable, and if you select one then the parent was again selectable, but right after the Add on the collection it was broken.

Seems to be a known problem: the IsFocused property is not updated when you change IsSelected via code. I am a little surprised that it is also not kept up to date when you set it via a binding… but I guess in the end binding is just code that you didn’t have to write yourself. Hopefully someone, somewhere will decide that this is indeed a bug so I can remove the event handler that I have put in place as a workaround*

Sample reproduction code:

<Window x:Class="WpfApplication1.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:WpfApplication1;assembly="
    Title="Window1" Height="300" Width="300">
    <Window.CommandBindings>
        <CommandBinding Command="{x:Static local:Window1.AddChildItem}" 
                            CanExecute="AddChildItem_CanExecute"
                            Executed="AddChildItem_Executed" />
    </Window.CommandBindings>
    <Window.Resources>
        <HierarchicalDataTemplate DataType="{x:Type local:Item}"
                                  ItemsSource="{Binding Children}">
            <TextBlock>
                <TextBlock Text="Name -> " />
                <TextBlock Text="{Binding ItemName}" />
                <TextBox></TextBox>
            </TextBlock>
        </HierarchicalDataTemplate>
        <Style TargetType="{x:Type TreeViewItem}">
            <!-- property bindings -->
            <Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
            <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
            <!-- Workaround: Keep the IsFocused property up to date when the IsSelected property changes -->
            <EventSetter Event="Selected" Handler="TreeViewItem_Selected" />
        </Style>
    </Window.Resources>
    <DockPanel LastChildFill="True">
        <ToolBarTray DockPanel.Dock="Top" Height="21">
            <ToolBar>
                <Button Command="{x:Static local:Window1.AddChildItem}">
                    <TextBlock Text="Add" />
                </Button>
            </ToolBar>
        </ToolBarTray>
        <TreeView Name="tv1" ItemsSource="{Binding Items}">
        </TreeView>
    </DockPanel>
</Window>

using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows;
using System.Windows.Input;
using System.Windows.Controls;
 
namespace WpfApplication1 {
    public partial class Window1 : Window {
        private ObservableCollection<Item> _items = new ObservableCollection<Item>();
        public ObservableCollection<Item> Items {
            get { return _items; }
        }
 
        public static readonly RoutedCommand AddChildItem = new RoutedCommand("AddChildItem", typeof(Window1));
 
        public Window1() {
            InitializeComponent();
 
            _items.Add(new Item { ItemName = "One" });
            _items.Add(new Item { ItemName = "Two" });
            _items.Add(new Item { ItemName = "Three" });
 
            this.DataContext = this;
        }
 
        private void AddChildItem_CanExecute(object sender, CanExecuteRoutedEventArgs e) {
            if(tv1 != null && tv1.SelectedItem != null) {
                e.CanExecute = true;
            }
        }
 
        private void AddChildItem_Executed(object sender, ExecutedRoutedEventArgs e) {
            if(tv1.SelectedItem != null) {
                (tv1.SelectedItem as Item).AddChildItem();
            }
        }
 
        // Without this event handler you will see the problem
        private void TreeViewItem_Selected(object sender, RoutedEventArgs e){
            TreeViewItem tvi = e.OriginalSource as TreeViewItem;
            if(tvi != null && !tvi.IsFocused) {
                tvi.Focus();
            }
        }
    }
 
    public class Item : INotifyPropertyChanged {
        private string _itemName = "New Item";
        public string ItemName {
            get { return _itemName; }
            set { _itemName = value; }
        }
        private ObservableCollection<Item> _children = new ObservableCollection<Item>();
        public ObservableCollection<Item> Children {
            get { return _children; }
        }
        private Item _parent = null;
        public Item Parent {
            get { return _parent; }
        }
 
        public Item() : this(null) { }
        public Item(Item parent) {
            _parent = parent;
        }
 
        public void AddChildItem() {
            Item i = new Item(this);
            _children.Add(i);
            i.IsSelected = true;
        }
 
        private bool _isSelected;
        public bool IsSelected {
            get { return _isSelected; }
            set {
                if(_isSelected != value) {
                    _isSelected = value;
                    OnPropertyChanged("IsSelected");
                }
            }
        }
 
        private bool _isExpanded = true;
        public bool IsExpanded {
            get { return _isExpanded; }
            set {
                if(_isExpanded != value) {
                    _isExpanded = value;
                    if(_isExpanded && _parent != null) {
                        _parent.IsExpanded = true;
                    }
                    OnPropertyChanged("IsExpanded");
                }
            }
        }
 
        #region INotifyPropertyChanged Members
 
        public event PropertyChangedEventHandler PropertyChanged;
 
        #endregion
 
        protected virtual void OnPropertyChanged(string propertyName) {
            if(this.PropertyChanged != null) {
                this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }
}

Just Comment out the call to Focus to see the problem.

Of note also is the fact that you must set IsSelected after the new item is inserted into the visual tree. For me that meant a small refactor (seen in the example in AddChildItem) to create the item, add it to the collection then set its IsSelected property rather than creating it with IsSelected already set to true. Otherwise it seems that the call to Focus fails.


* I.e. my code was all nice and separated, I was not handling any TreeView or TreeViewItem events, it was all done with smoke and mirrors** in a layer between the UI and the BO

** Data binding

1 comment:

  1. Have the same. Thanks for problem issue description. I have modify this solution base on attached property...

    ReplyDelete