Click here to Skip to main content
15,905,590 members
Articles / Desktop Programming / WPF

WPF TreeView with in-place editing

Rate me:
Please Sign up or sign in to vote.
4.33/5 (10 votes)
7 Apr 2015CPOL5 min read 42.7K   1.9K   19   4
This simple WPF user control allows in-place editing headers of particular items in a TreeView control using HierarchicalDataTemplate

Introduction

Designing a user interface, I came across a need of editing the headers of items in a tree-like hierarchical data structure, like this:

Certainly, it should be easy when one has WPF at his disposal! But it took me surprisingly long time to achieve this goal, and that is why I decided to share my findings with the colleagues here. The solution appears to be simple and does the expected job.

Description of the problem

Suppose that we have a tree-like data structure, which is presented to the user in a TreeView control:

C#
class TreeViewDocument : ObservableCollection<TreeViewParentItem>
{
    public TreeViewDocument()
    {
        Add(new TreeViewParentItem("First parent item"));
        Add(new TreeViewParentItem("Second parent item"));
        Add(new TreeViewParentItem("Third parent item"));
    }
}
class TreeViewParentItem : NotifyPropertyChanged
{
    // this is a name for the parent item - shall be displayed as a header and be editable
    string name;
    public string Name
    {
        get { return name; }
        set { SetField(ref name, value); }
    }
    // child items are just strings
    public ObservableCollection<string> TreeViewChildrenItems { get; set; }
    public TreeViewParentItem(string name)
    {
        Name = name;
        TreeViewChildrenItems = new ObservableCollection<string>();
        TreeViewChildrenItems.Add("first child");
        TreeViewChildrenItems.Add("second child");
    }
}

A document is an ObservableCollection, which features several parent items with editable names. Parents have children, which are just fixed strings in this simple example. Our aim is to show this hierarchical collection in a TreeView such, that the user may edit the names of the parent items as in the figure above. The edit mode shall be activated by clicking a selected item with the mouse or by pressing F2, which I regard as being the expected behavior of the control. And last but not least (MVVM!!!), the data structure itself shall have nothing to do with the user interface besides the standard notifications system, implemented in ObservableCollection and a simple base class NotifyPropertyChange, which is based on the ideas presented here.

First simple solution

Having seen this simple example of exposing a hierarchical collection in a TreeView using HierarchicalDataTemplate for objects of different data types, I quickly handcrafted a simple solution:

XML
<UserControl x:Class="WpfTreeViewInPlaceEdit.WpfTreeViewInPlaceEditControl"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WpfTreeViewInPlaceEdit"
        x:Name="wpfTreeViewInPlaceEditControl">
    <TreeView x:Name="treeView" ItemsSource="{Binding}">
        <TreeView.Resources>
            <HierarchicalDataTemplate DataType="{x:Type local:TreeViewParentItem}" ItemsSource="{Binding TreeViewChildrenItems}">
                <TextBox Text="{Binding Name, UpdateSourceTrigger=LostFocus}"/>
            </HierarchicalDataTemplate>
        </TreeView.Resources>
    </TreeView>
</UserControl> 

All instances of TreeViewParentItem use the field TreeViewChildrenItems as an items source for the subsequent level of the hierarchy, and a TextBox is created and bound to the Name property. Certainly, the data context of the control shall be set to an instance of TreeViewDocument. It works fantastic, the user can expand/collapse the items and edit the names:

But selecting the parent items and navigating through the tree using the keyboard is impossible. One wishes a possibility to switch between the viewing mode and editing the headers.

Existing solutions

The problem is discussed for example here. A general suggestion is to use data triggers and switch the data presentation in a HierarchicalDataTemplate from static text to an editable text box based on the IsSelected property of the corresponding TreeViewItem. As many poing out, this does not solve the problem, as editing starts as soon as the user selects the item. Googling, I found something. There is a solution in an "essential objects" bundle of WPF controls, which is, however, not available for free. I have also checked this solution, this control and this project, and was not satisfied: one needs to link to external libraries, and the resulting behavior was not quite as expected. A simpler solution is presented below.

Basic idea

We again use HierarchicalDataTemplate, which switches the presentation of parent items to the editing mode when two conditions are met:

1. the item is selected,

2. and the control is in edit mode.

The control switches to edit mode when the user clicks on a selected item (remember, that only particular data types are covered by the data template, such that nothing would happen if an item is not editable), or the user presses F2. The flag IsInEditMode is a property of the control, and is set/reset depending on certain events in the code behind. MultiDataTrigger needs to be used, as several switching conditions are involved. The implementation below also features the possibility to roll back and undo the changes to the item when the user presses Escape during the editing process.

Implementation: XAML

Now, the XAML code of the control reads

XML
<UserControl x:Class="WpfTreeViewInPlaceEdit.WpfTreeViewInPlaceEditControl"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WpfTreeViewInPlaceEdit"
        x:Name="wpfTreeViewInPlaceEditControl">
    <TreeView x:Name="treeView" ItemsSource="{Binding}"
              KeyDown="treeView_KeyDown"
              SelectedItemChanged="treeView_SelectedItemChanged">
        <TreeView.Resources>
            <HierarchicalDataTemplate DataType="{x:Type local:TreeViewParentItem}" ItemsSource="{Binding TreeViewChildrenItems}">
                <Grid>
                    <!-- Normal state of the header -->
                    <TextBlock x:Name="textBlockHeader" Text="{Binding Name}" Margin="3,0" MouseLeftButtonDown="textBlockHeaderSelected_MouseLeftButtonDown"/>
                    <!-- This state is active in the edit mode -->
                    <TextBox x:Name="editableTextBoxHeader" Visibility="Hidden" MinWidth="100"
                             Text="{Binding Name, UpdateSourceTrigger=LostFocus}"
                             LostFocus="editableTextBoxHeader_LostFocus"
                             IsVisibleChanged="editableTextBoxHeader_IsVisibleChanged"
                             KeyDown="editableTextBoxHeader_KeyDown"/>
                </Grid>
                <!-- With triggers we switch between the three states of the header depending on its focused property and the control-level property "IsInEditMode" -->
                <HierarchicalDataTemplate.Triggers>
                    <MultiDataTrigger>
                        <!-- Selected, editing is generally active - the text box is displayed -->
                        <MultiDataTrigger.Conditions>
                            <Condition Binding="{Binding IsSelected, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type TreeViewItem}}}" Value="True"/>
                            <Condition Binding="{Binding IsInEditMode, ElementName=wpfTreeViewInPlaceEditControl}" Value="True"/>
                        </MultiDataTrigger.Conditions>
                        <Setter TargetName="editableTextBoxHeader" Property="Visibility" Value="Visible" />
                    </MultiDataTrigger>
                </HierarchicalDataTemplate.Triggers>
            </HierarchicalDataTemplate>
        </TreeView.Resources>
    </TreeView>
</UserControl>

Instances of TreeViewParentItem data type are presented using a static text block and an editable text box, all of which are bound to the Name property. The text box becomes visible (and hides the text block behind it) only when two flags are set true: IsSelected property of the tree view item (referred to using "FindAncestor") and IsInEditMode property of the control itself. Various event handlers in the code behind serve to the purpose of initiating and finishing editing of the headers by setting/resetting the flag IsInEditMode. Binding to TreeViewChildrenItems allows the TreeView traversing the hierarchical data structure.

Implementation: code behind

C#
public partial class WpfTreeViewInPlaceEditControl : UserControl, INotifyPropertyChanged
{
    // INotifyPropertyChanged
    public event PropertyChangedEventHandler PropertyChanged;

    // This flag indicates whether the tree view items shall (if possible) open in edit mode
    bool isInEditMode = false;
    public bool IsInEditMode
    {
        get { return isInEditMode; }
        set
        {
            isInEditMode = value;
            PropertyChangedEventHandler handler = PropertyChanged;
            if(handler != null)
                handler(this, new PropertyChangedEventArgs("IsInEditMode"));
        }
    }

    public WpfTreeViewInPlaceEditControl()
    {
        InitializeComponent();
    }

    // text in a text box before editing - to enable cancelling changes
    string oldText;

    // if a text box has just become visible, we give it the keyboard input focus and select contents
    private void editableTextBoxHeader_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
    {
        var tb = sender as TextBox;
        if(tb.IsVisible)
        {
            tb.Focus();
            tb.SelectAll();
            oldText = tb.Text;      // back up - for possible cancelling
        }
    }

    // stop editing on Enter or Escape (then with cancel)
    private void editableTextBoxHeader_KeyDown(object sender, KeyEventArgs e)
    {
        if(e.Key == Key.Enter)
            IsInEditMode = false;
        if(e.Key == Key.Escape)
        {
            var tb = sender as TextBox;
            tb.Text = oldText;
            IsInEditMode = false;
        }
    }

    // stop editing on lost focus
    private void editableTextBoxHeader_LostFocus(object sender, RoutedEventArgs e)
    {
        IsInEditMode = false;
    }

    // it might happen, that the user pressed F2 while a non-editable item was selected
    private void treeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
    {
        IsInEditMode = false;
    }

    // we (possibly) switch to edit mode when the user presses F2
    private void treeView_KeyDown(object sender, KeyEventArgs e)
    {
        if(e.Key == Key.F2)
            IsInEditMode = true;
    }

    // the user has clicked a header - proceed with editing if it was selected
    private void textBlockHeader_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
        if(FindTreeItem(e.OriginalSource as DependencyObject).IsSelected)
        {
            IsInEditMode = true;
            e.Handled = true;       // otherwise the newly activated control will immediately loose focus
        }
    }

    // searches for the corresponding TreeViewItem,
    // based on http://stackoverflow.com/questions/592373/select-treeview-node-on-right-click-before-displaying-contextmenu
    static TreeViewItem FindTreeItem(DependencyObject source)
    {
        while(source != null && !(source is TreeViewItem))
            source = VisualTreeHelper.GetParent(source);
        return source as TreeViewItem;
    }
}

The class of the user control is equipped with the property IsInEditMode using the standard notification system, which allows binding the data triggers to it. Changing its value, we automatically affect the graphical state of the user control. The flag is set to true when the user presses F2 on the keyboard or clicks a selected item. This makes the text box visible through the data triggers, and in the corresponding event handler we back up the old value of the name of the item for possible roll back and set the keyboard input focus to the text box. Edit mode finishes by pressing Enter or Escape (in the latter case, the old value of the header is restored), or when the text box loses the keyboard input focus. By setting IsInEditMode to false when the selected item changes, we ensure that hitting F2 on a child item will not result in opening a text box when the selection goes to a parent one.

And that's all I have to say about that!

Summary

The small sample project is attached. Clearly, the technique may be applied for more complicated and general data structures. It is easy to initiate editing not by pressing F2, but on pressing any alphanumeric key, which would be particularly user-friendly in certain contexts.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Austria Austria
I am a researcher at Vienna University of Technology.

Comments and Discussions

 
GeneralMy vote of 5 Pin
User 74293386-Jul-18 5:28
professionalUser 74293386-Jul-18 5:28 
GeneralGreat example Pin
1337Architect29-Jun-18 11:48
professional1337Architect29-Jun-18 11:48 
Easy to understand, had my stuff working the same way in a couple of hours... thanks!
QuestionMake the subItems editable Pin
Member 1239909128-Apr-17 3:09
Member 1239909128-Apr-17 3:09 
PraiseGreat article Pin
samhall114-Mar-17 15:54
samhall114-Mar-17 15:54 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.