Inline editing TextBlock in a ListBox with Data Template (WPF)

18,490

Solution 1

What I've done in these situations is used the XAML hierarchy to determine which element to show/hide. Something along the lines of:

<Grid>
  <TextBlock MouseDown="txtblk_MouseDown" />
  <TextBox LostFocus="txtbox_LostFocus" Visibility="Collapsed" />
</Grid>

with the code:

protected void txtblk_MouseDown(object sender, MouseButtonEventArgs e)
{
    TextBox txt = (TextBox)((Grid)((TextBlock)sender).Parent).Children[1];
    txt.Visibility = Visibility.Visible;
    ((TextBlock)sender).Visibility = Visibility.Collapsed;
}

protected void txtbox_LostFocus(object sender, RoutedEventArgs e)
{
    TextBlock tb = (TextBlock)((Grid)((TextBox)sender).Parent).Children[0];
    tb.Text = ((TextBox)sender).Text;
    tb.Visibility = Visibility.Visible;
    ((TextBox)sender).Visibility = Visibility.Collapsed;
}

I always turn stuff like this that I'm going to reuse into a UserControl, which I can add additional error handling to, and guarantee that the Grid will only contain two items, and the order of them will never change.

EDIT: Additionally, turning this into a UserControl allows you to create a Text property for each instantiation, so you can name each one and reference the text directly without fishing for the current value through the ((TextBox)myGrid.Children[1]).Text casting. This will make your code much more efficient and clean. If you make it into a UserControl, you can also name the TextBlock and TextBox elements, so no casting is needed at all.

Solution 2

Refer to the Nathan Wheeler's code snippet, the following codes are complete UserControl source that I coded yesterday. Especially, Binding issues are addressed. Nathan's code is easy to follow, but needs some aid in order to work with databound text.

ClickToEditTextboxControl.xaml.cs

public partial class ClickToEditTextboxControl : UserControl
{
    public ClickToEditTextboxControl()
    {
        InitializeComponent();
    }

    public string Text
    {
        get { return (string)GetValue(TextProperty); }
        set { SetValue(TextProperty, value); }
    }

    // Using a DependencyProperty as the backing store for Text.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty TextProperty =
        DependencyProperty.Register("Text", typeof(string), typeof(ClickToEditTextboxControl), new UIPropertyMetadata());

    private void textBoxName_LostFocus(object sender, RoutedEventArgs e)
    {
        var txtBlock = (TextBlock)((Grid)((TextBox)sender).Parent).Children[0];

        txtBlock.Visibility = Visibility.Visible;
        ((TextBox)sender).Visibility = Visibility.Collapsed;
    }

    private void textBlockName_MouseDown(object sender, MouseButtonEventArgs e)
    {
        var grid = ((Grid) ((TextBlock) sender).Parent);
        var tbx = (TextBox)grid.Children[1];
        ((TextBlock)sender).Visibility = Visibility.Collapsed;
        tbx.Visibility = Visibility.Visible;

        this.Dispatcher.BeginInvoke((Action)(() => Keyboard.Focus(tbx)), DispatcherPriority.Render);
    }

    private void TextBoxKeyDown(object sender, KeyEventArgs e)
    {
        if (e == null)
            return;

        if (e.Key == Key.Return)
        {
            textBoxName_LostFocus(sender, null);
        }
    }
}

ClickToEditTextboxControl.xaml

<UserControl x:Class="Template.ClickToEditTextboxControl"
         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" 
         mc:Ignorable="d" 
         Name="root"
         d:DesignHeight="30" d:DesignWidth="100">
<Grid>
    <TextBlock Name="textBlockName" Text="{Binding ElementName=root, Path=Text}" VerticalAlignment="Center" MouseDown="textBlockName_MouseDown" />
    <TextBox Name="textBoxName" Text="{Binding ElementName=root, Path=Text, UpdateSourceTrigger=PropertyChanged}" Visibility="Collapsed" LostFocus="textBoxName_LostFocus" KeyDown ="TextBoxKeyDown"/>
</Grid>
</UserControl>

And, finally, you can use this control in the XAML as below:

<Template1:ClickToEditTextboxControl Text="{Binding Path=Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" MinWidth="40" Height="23" />

Note that Mode=TwoWay, UpdateSourceTrigger=PropertyChanged is set. It enables to change the binded value in every type.

Solution 3

The ideal way to do this would be to create a ClickEditableTextBlock control, which by default renders as a TextBlock but shows a TextBox when the user clicks it. Because any given ClickEditableTextBlock has only one TextBlock and one TextBox, you don't have the matching issue. Then you use a ClickEditableTextBlock instead of separate TextBlocks and TextBoxes in your DataTemplate.

This has the side benefit of encapsulating the functionality in a control so you don't pollute your main window code-behind with the edit behaviour, plus you can easily reuse it in other templates.


If this sounds like too much effort, you can use Tag or an attached property to associate each TextBlock with a TextBox:

<DataTemplate>
  <StackPanel>
    <TextBlock Text="whatever"
               MouseDown="TextBlock_MouseDown"
               Tag="{Binding ElementName=tb}" />
    <TextBox Name="tb" />
  </StackPanel>
</DataTemplate>

Note the use of {Binding ElementName=tb} on the Tag to refer to the TextBox named tb.

And in your code-behind:

private void TextBlock_MouseDown(object sender, MouseButtonEventArgs e)
{
  FrameworkElement textBlock = (FrameworkElement)sender;
  TextBox editBox = (TextBox)(textBlock.Tag);
  editBox.Text = "Wow!";  // or set visible or whatever
}

(To avoid the use of the nasty Tag property, you could define a custom attached property to carry the TextBox binding, but for brevity I'm not showing that.)

Solution 4

If I may supplement, in order to cover the (double) part of the original question, in Youngjae's reply you make the following replacement in the xaml file:

<TextBlock Name="textBlockName" Text="{Binding ElementName=root, Path=Text}" VerticalAlignment="Center" MouseDown="textBlockName_MouseDown" />

is replaced with

<TextBlock Name="textBlockName" Text="{Binding ElementName=root, Path=Text}" VerticalAlignment="Center" >
    <TextBlock.InputBindings>
        <MouseBinding Gesture="LeftDoubleCLick" Command="{StaticResource cmdEditTextblock}"/>
    </TextBlock.InputBindings>
</TextBlock>

adding also the proper RoutedCommand in UserControl.Resources

<UserControl.Resources>
    <RoutedCommand x:Key="cmdEditTextblock"/>
</UserControl.Resources>

and a CommandBinding in UserControl.CommandBindings

<UserControl.CommandBindings>
    <CommandBinding Command="{StaticResource cmdEditTextblock}"
                    Executed="CmdEditTextblock_Executed"/>
</UserControl.CommandBindings>

Also in the code behind file:

private void textBlockName_MouseDown(object sender, MouseButtonEventArgs e)
{
    var grid = ((Grid) ((TextBlock) sender).Parent);
    var tbx = (TextBox)grid.Children[1];
    ((TextBlock)sender).Visibility = Visibility.Collapsed;
    tbx.Visibility = Visibility.Visible;
    this.Dispatcher.BeginInvoke((Action)(() => Keyboard.Focus(tbx)), DispatcherPriority.Render);
}

is replaced by

private void CmdEditTextblock_Executed(object sender, ExecutedRoutedEventArgs e)
    {
        var grid = ((Grid)((TextBlock)e.OriginalSource).Parent);
        var tbx = (TextBox)grid.Children[1];
        ((TextBlock)e.OriginalSource).Visibility = Visibility.Collapsed;
        tbx.Visibility = Visibility.Visible;
        this.Dispatcher.BeginInvoke((Action)(() => Keyboard.Focus(tbx)), DispatcherPriority.Render);
    }

In case some people want to have left doubleclick as input gesture, like I did...

Share:
18,490
Ko9
Author by

Ko9

Updated on June 03, 2022

Comments

  • Ko9
    Ko9 almost 2 years

    Using WPF, I have a ListBox control with a DataTemplate inside it. The relevant XAML code is shown below:

    <ListBox Name="_todoList" Grid.Row="1" BorderThickness="2"
         Drop="todoList_Drop" AllowDrop="True"
         HorizontalContentAlignment="Stretch"
         ScrollViewer.HorizontalScrollBarVisibility="Disabled"                 
         AlternationCount="2">
         <ListBox.ItemTemplate>
             <DataTemplate>
                 <Grid Margin="4">
                     <Grid.ColumnDefinitions>
                         <ColumnDefinition Width="Auto" />
                         <ColumnDefinition Width="*" />
                     </Grid.ColumnDefinitions>
                     <CheckBox Grid.Column="0" Checked="CheckBox_Check" />
                     <TextBlock Name="descriptionBlock"
                                Grid.Column="1"
                                Text="{Binding Description}"
                                Cursor="Hand" FontSize="14"
                                ToolTip="{Binding Description}"
                                MouseDown="TextBlock_MouseDown" />                      
                 </Grid>
             </DataTemplate>
         </ListBox.ItemTemplate>
    </ListBox>
    

    What I am trying to do is make the TextBlock respond to a (double)click which turns it into a TextBox. The user can then edit the description, and press return or change focus to make the change.

    I have tried adding a TextBox element in the same position as the TextBlock and making its visiblity Collapsed, but I don't know how to navigate to the right TextBox when the user has clicked on a TextBlock. That is, I know the user has clicked on a certain TextBlock, now which TextBox do I show?

    Any help would be appreciated greatly,

    -Ko9

  • Luke Duddridge
    Luke Duddridge almost 14 years
    this may sound odd, could you edit your answer... I clicked up prematurely, then removed my vote, because I wasnt sure if this was what I was after. I then tried to post an up vote again because it was what I needed and Stack Overflow says I cannot vote on this post unless it was edited. hence the request. Cheers, very useful post as it turned out.
  • Nicolas
    Nicolas over 5 years
    Thanks for this solution, I use it via DataTemplate for my ListBox, but the new value does not get updated in the source. The ItemsSource is of type ObservableCollection<string>. Any ideas?