使用数据模板(WPF)在ListBox中进行文本块的内联编辑

12

使用WPF,我有一个ListBox控件,内部包含一个DataTemplate。下面是相关的XAML代码:

<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>
我想做的就是使响应(双)击事件,并将其变成。用户可以编辑描述,按回车或改变焦点以进行更改。
我已经尝试在与TextBlock相同的位置添加一个元素,并将其可见性设置为Collapsed,但我不知道当用户单击时如何导航到正确的。也就是说,我知道用户已单击某个,现在要显示哪个?
任何帮助都将非常感激,
-Ko9

作为提示,您可以直接将XAML粘贴到编辑器中,并使用101010按钮将其格式化为代码,而不是使用“pre”标签并明确转义尖括号。 - itowlson
谢谢您以一种基本而全面的方式展示了ListBox中DataTemplate的工作原理! - z33k
4个回答

16

参考 Nathan Wheeler 的代码片段,以下代码是我昨天编写的完整的 UserControl 源代码。 特别是解决了绑定问题。 Nathan 的代码易于理解,但需要一些帮助才能与数据绑定的文本一起使用。

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

点击以编辑文本框控件.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>

最后,您可以在XAML中使用以下控件:

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

请注意,已设置Mode=TwoWay,UpdateSourceTrigger=PropertyChanged。它允许在每种类型中更改绑定的值。


谢谢您提供的解决方案,我通过DataTemplate在我的ListBox中使用它,但新值没有在源中更新。ItemsSource的类型是ObservableCollection<string>。有什么想法吗? - Nicolas

16

我在这些情况下所做的是使用XAML层次结构来确定要显示/隐藏的元素。 大致如下:

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

使用以下代码:

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

我通常会将我要重复使用的这种东西变成UserControl,这样我可以添加额外的错误处理,并保证Grid只包含两个项目,它们的顺序永远不会改变。

编辑:此外,将其转换为UserControl还允许您为每个实例创建一个Text属性,因此您可以对每个实例进行命名并直接引用文本,而无需通过((TextBox)myGrid.Children[1]).Text的强制类型转换查找当前值。这将使您的代码更加高效和简洁。如果将其转换为UserControl,您还可以为TextBlockTextBox元素命名,因此根本不需要强制类型转换。


这可能听起来有些奇怪,你能编辑一下你的回答吗?我之前过早地点了赞,然后又取消了,因为我不确定这是否是我需要的。后来我又试图再次点赞,因为这正是我需要的,但 Stack Overflow 表示除非被编辑,否则我无法对此帖子投票。因此请求编辑。谢谢,结果证明非常有用。 - Luke Duddridge

4
最理想的方法是创建一个ClickEditableTextBlock控件,默认呈现为TextBlock,但当用户单击它时会显示TextBox。因为每个ClickEditableTextBlock只有一个TextBlock和一个TextBox,所以不会出现匹配问题。然后,在DataTemplate中使用ClickEditableTextBlock而不是分开使用TextBlocks和TextBoxes。
这样做的副作用是将功能封装在控件中,使得您的主窗口代码不会被编辑行为污染,另外,您可以轻松地在其他模板中重复使用它。
如果这听起来太麻烦,您可以使用标签或附加属性将每个TextBlock与一个TextBox关联起来:
<DataTemplate>
  <StackPanel>
    <TextBlock Text="whatever"
               MouseDown="TextBlock_MouseDown"
               Tag="{Binding ElementName=tb}" />
    <TextBox Name="tb" />
  </StackPanel>
</DataTemplate>

请注意在标记中使用{Binding ElementName=tb}来引用名为tb的 TextBox。

另外,在您的代码后台中:

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
}

为了避免使用糟糕的Tag属性,你可以定义一个自定义的附加属性来携带TextBox绑定,但为了简洁起见,我不会展示这个方法。

1
如果我可以补充一下,为了涵盖原问题中(双重)的部分,在Youngjae的回复中,您需要在xaml文件中进行以下替换:
<TextBlock Name="textBlockName" Text="{Binding ElementName=root, Path=Text}" VerticalAlignment="Center" MouseDown="textBlockName_MouseDown" />

"

被替换为

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

同时在UserControl.Resources中添加正确的RoutedCommand

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

在UserControl.CommandBindings中添加一个CommandBinding。

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

同样在代码后台文件中:

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

如果有些人想将左双击作为输入手势,就像我一样。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接