WPF editable treeview with two editable elements per row

41 Views Asked by At

Based on this tutorial, I incorporated an editable treeview in my project following an MVVM structure. So far I can display a databound treeview, populated with elements from an uploaded xml file. The treeview has two levels: parents and children, both are editable, but I left the children out in this example, to make it simpler.

Xaml:

<UserControl x:Class="MyProject.MVVM.View.EditableTreeView"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
         xmlns:local="clr-namespace:MyProject.MVVM.View"
         xmlns:model="clr-namespace:MyProject.MVVM.Model"
         x:Name="editableTreeView"
         mc:Ignorable="d" 
         d:DesignHeight="450" d:DesignWidth="800">

<TreeView x:Name="treeView" ItemsSource="{Binding TRVItems}">
    <TreeView.Resources>

        <HierarchicalDataTemplate DataType="{x:Type model:ParentItem}" ItemsSource="{Binding Children}">
            <Grid>
                <!-- Normal state of the header -->
                <TextBlock x:Name="textBlockHeaderName" Text="{Binding Name}" Margin="3,0" MouseLeftButtonDown="textBlockHeaderSelected_MouseLeftButtonDown"/>
                <!-- This state is active in the edit mode -->
                <TextBox x:Name="editableTextBoxHeaderName" Visibility="Hidden" MinWidth="100"
                         Text="{Binding Name, UpdateSourceTrigger=LostFocus}"
                         LostFocus="editableTextBoxHeader_LostFocus"
                         IsVisibleChanged="editableTextBoxHeader_IsVisibleChanged"
                         KeyDown="editableTextBoxHeader_KeyDown"/>
            </Grid>
            
            <HierarchicalDataTemplate.Triggers>
                <MultiDataTrigger>
                    <MultiDataTrigger.Conditions>
                        <Condition Binding="{Binding IsSelected, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type TreeViewItem}}}" Value="True"/>
                        <Condition Binding="{Binding IsInEditMode, ElementName=editableTreeView}" Value="True"/>
                    </MultiDataTrigger.Conditions>
                    <Setter TargetName="editableTextBoxHeaderName" Property="Visibility" Value="Visible" />
                </MultiDataTrigger>
            </HierarchicalDataTemplate.Triggers>
        
        </HierarchicalDataTemplate>
        
    </TreeView.Resources>
</TreeView>

Code-behind:

    public partial class EditableTreeView : UserControl, INotifyPropertyChanged
{
    public EditableTreeView()
    {
        InitializeComponent();
    }

    // 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"));
        }
    }

    // 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;
    }

    // the user has clicked a header - proceed with editing if it was selected
    private void textBlockHeaderSelected_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,
    static TreeViewItem FindTreeItem(DependencyObject source)
    {
        while (source != null && !(source is TreeViewItem))
            source = VisualTreeHelper.GetParent(source);
        return source as TreeViewItem;
    }

    public event PropertyChangedEventHandler? PropertyChanged;

    protected void OnPropertyChanged(string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

Furthermore, in the treeview I want to display two properties of each element, its name and its ID. However, when I add another TextBlock, TextBox and Trigger, the edititing functionality stops working:

<HierarchicalDataTemplate DataType="{x:Type model:Parent}" ItemsSource="{Binding Children}">
<Grid>

    <Grid.ColumnDefinitions>
        <ColumnDefinition />
        <ColumnDefinition />
    </Grid.ColumnDefinitions>

    <!-- Normal state of the header -->
    <TextBlock x:Name="textBlockHeaderName" Text="{Binding Name}" Margin="3,0" Grid.Column="0" MouseLeftButtonDown="textBlockHeaderSelected_MouseLeftButtonDown"/>
    <TextBlock x:Name="textBlockHeaderId" Text="{Binding Id}" Margin="3,0" Grid.Column="1" MouseLeftButtonDown="textBlockHeaderSelected_MouseLeftButtonDown" />
    <!-- This state is active in the edit mode -->
    <TextBox x:Name="editableTextBoxHeaderName" Visibility="Hidden" MinWidth="100" Grid.Column="0"
             Text="{Binding Name, UpdateSourceTrigger=LostFocus}"
             LostFocus="editableTextBoxHeader_LostFocus"
             IsVisibleChanged="editableTextBoxHeader_IsVisibleChanged"
             KeyDown="editableTextBoxHeader_KeyDown"/>
    <TextBox x:Name="editableTextBoxHeaderId" Visibility="Hidden" MinWidth="100" Grid.Column="1"
            Text="{Binding Id, UpdateSourceTrigger=LostFocus}"
            LostFocus="editableTextBoxHeader_LostFocus"
            IsVisibleChanged="editableTextBoxHeader_IsVisibleChanged"
            KeyDown="editableTextBoxHeader_KeyDown"/>
    
</Grid>

<HierarchicalDataTemplate.Triggers>
    <MultiDataTrigger>
        <MultiDataTrigger.Conditions>
            <Condition Binding="{Binding IsSelected, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type TreeViewItem}}}" Value="True"/>
            <Condition Binding="{Binding IsInEditMode, ElementName=editableTreeView}" Value="True"/>
        </MultiDataTrigger.Conditions>
        <Setter TargetName="editableTextBoxHeaderName" Property="Visibility" Value="Visible" />
    </MultiDataTrigger>
    <MultiDataTrigger>
        <MultiDataTrigger.Conditions>
            <Condition Binding="{Binding IsSelected, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type TreeViewItem}}}" Value="True"/>
            <Condition Binding="{Binding IsInEditMode, ElementName=editableTreeView}" Value="True"/>
        </MultiDataTrigger.Conditions>
        <Setter TargetName="editableTextBoxHeaderId" Property="Visibility" Value="Visible" />
    </MultiDataTrigger>
</HierarchicalDataTemplate.Triggers>

When I now select and click the parent, neither trigger seems to be called. The row is selected, but clicking it again does nothing. I can imagine this doesn't work, because I basically have the same trigger conditions twice. How would I specificy that a trigger be called when the specific TextBlock is selected, and not just the whole TreeViewItem?

0

There are 0 best solutions below