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?