0

Working with WPF I'm trying to produce a TextBlock which, upon double click, "transforms" into a TextBox ready for editing. After that, an Enter key, Esc key or losing focus causes the editing to end, and the TextBox to revert to a TextBlock.

The solution I found is mostly working. My problem is that I'm not able to focus the TextBox upon the "transformation", thus forcing the user to explicitly click once more on the element to focus it and start editing.

Explanation of the code

The way I chose to implement this behaviour is to work with templates and styles' DataTriggers in order to change the template of the element. In the example I show, the element is a simple ContentControl, althought the real use case in which I'm trying to do this is slighly more complicated (I have a ListBox, where each element is editable through this behaviour, one at a time).

The idea is the following:

  • The ContentControl has an associated Style
  • The associated Style defines the ContentTemplate as a TextBlock.
  • The model object has a property, InEditing, which turns to true when I want to edit the control
  • Through a MouseBinding on the TextBlock I set the InEditing property of the model to True
  • The Style has a DataTrigger which listens to InEditing, and in this case sets the ContentTemplate to a TextBox
  • Through EventSetters I catch Enter, Esc and LostFocus in order to revert save the changes and revert back to the previous style. Note well: I can't directly attach events to the TextBox, otherwise I get a The event cannot be specified on a Target tag in a Style. Use an EventSetter instead.

Although not optimal (there's a certain mix of view and model behaviours - especially in the InEditing property - and I don't like to substantially re-implement the commit logic of changes to the model of the TextBox through the various handlers for KeyDown and LostFocus), the system actually works without problems.

Failed implementation idea

At first I thought of connecting to the IsVisibleChanged event of the TextBox, and set the focus in there. Cannot do it, because of the beforementioned The event cannot be specified on a Target tag in a Style. Use an EventSetter instead.

The solution suggested by the error cannot be used, because such an event is not a routed event, and thus cannot be used in an EventSetter.

The code

The code is split in four files.

Model.cs:

using System.Windows;

namespace LeFocus
{
    public class Model: DependencyObject
    {
        public bool InEditing
        {
            get { return (bool)GetValue(InEditingProperty); }
            set { SetValue(InEditingProperty, value); }
        }

        // Using a DependencyProperty as the backing store for InEditing.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty InEditingProperty =
            DependencyProperty.Register("InEditing", typeof(bool), typeof(Model), new UIPropertyMetadata(false));


        public string Name
        {
            get { return (string)GetValue(NameProperty); }
            set { SetValue(NameProperty, value); }
        }

        // Using a DependencyProperty as the backing store for Name.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty NameProperty =
            DependencyProperty.Register("Name", typeof(string), typeof(Model), new UIPropertyMetadata("Hello!"));
    }
}

App.xaml:

<Application x:Class="LeFocus.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:lefocus="clr-namespace:LeFocus"
             StartupUri="MainWindow.xaml">
    <Application.Resources>
        <lefocus:Model x:Key="Model"/>
    </Application.Resources>
</Application>

MainWindow.xaml:

<Window x:Class="LeFocus.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:lefocus="clr-namespace:LeFocus"
        Title="MainWindow" Height="350" Width="525"
        DataContext="{Binding Source={StaticResource Model}}" 
        Name="mainWindow">
    <Window.Resources>
        <Style x:Key="SwitchingStyle"
               TargetType="{x:Type ContentControl}">
            <Setter Property="ContentTemplate">
                <Setter.Value>
                    <DataTemplate DataType="{x:Type lefocus:Model}">
                        <TextBlock Text="{Binding Path=Name}">
                            <TextBlock.InputBindings>
                                <MouseBinding MouseAction="LeftDoubleClick"
                                              Command="lefocus:MainWindow.EditName"
                                              CommandParameter="{Binding}"/>
                            </TextBlock.InputBindings>
                        </TextBlock>
                    </DataTemplate>
                </Setter.Value>
            </Setter>
            <EventSetter Event="TextBox.KeyDown" Handler="TextBox_KeyDown"/>
            <EventSetter Event="TextBox.LostFocus" Handler="TextBox_LostFocus"/>
            <Style.Triggers>
                <DataTrigger Binding="{Binding Path=InEditing}" Value="True">
                    <Setter Property="ContentTemplate">
                        <Setter.Value>
                            <DataTemplate DataType="{x:Type lefocus:Model}">
                                <TextBox Text="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=lefocus:MainWindow, AncestorLevel=1}, Path=NameInEditing, UpdateSourceTrigger=PropertyChanged}" TextChanged="TextBox_TextChanged" KeyDown="TextBox_KeyDown_1" />
                            </DataTemplate>
                        </Setter.Value>
                    </Setter>
                </DataTrigger>
            </Style.Triggers>
        </Style>
    </Window.Resources>
    <Window.CommandBindings>
        <CommandBinding Command="lefocus:MainWindow.EditName" Executed="setInEditing"/>
    </Window.CommandBindings>
    <Grid>
        <ContentControl Style="{StaticResource SwitchingStyle}" Content="{Binding}"/>
    </Grid>
</Window>

MainWindow.xaml.cs

using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

namespace LeFocus
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }


        public string NameInEditing
        {
            get { return (string)GetValue(NameInEditingProperty); }
            set { SetValue(NameInEditingProperty, value); }
        }

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


        public static readonly RoutedUICommand EditName =
            new RoutedUICommand("EditName", "EditName", typeof(MainWindow));


        private void setInEditing(object sender, ExecutedRoutedEventArgs e)
        {
            var model = ((Model)e.Parameter);
            NameInEditing = model.Name;
            model.InEditing = true;
        }


        private void TextBox_KeyDown(object sender, KeyEventArgs e)
        {
            if (e.Key == Key.Enter)
            {
                var model = getModelFromSender(sender);
                model.Name = NameInEditing;
                NameInEditing = null;
                model.InEditing = false;
            }
            else if (e.Key == Key.Escape)
            {
                var model = getModelFromSender(sender);
                model.InEditing = false;
            }
        }

        private void TextBox_LostFocus(object sender, RoutedEventArgs e)
        {
            var model = getModelFromSender(sender);
            model.Name = NameInEditing;
            NameInEditing = null;
            model.InEditing = false;
        }

        private static Model getModelFromSender(object sender)
        {
            var contentControl = (ContentControl)sender;
            var model = (Model)contentControl.DataContext;
            return model;
        }
    }
}
RedGlow
  • 773
  • 7
  • 13
  • I answered a similar question http://stackoverflow.com/questions/9438396/change-listbox-template-on-lost-focus-event-in-wpf/9439336#9439336. It may be of some help. The idea is to style a textbox so it looks like a textblock, until you select it. – Phil Mar 10 '12 at 15:23
  • This seems promising; also the problem the original user had is similar to mine. The only problem I see is that I wanted the control to be editable only upon double click (single click: select; double click: edit), and I don't see an easy way to obtain that with this styling. – RedGlow Mar 11 '12 at 19:18

2 Answers2

0

One method which may work with this setup is handling Loaded on the TextBox and then to Keyboard.Focus it (the sender).

H.B.
  • 166,899
  • 29
  • 327
  • 400
  • Doesn't seem to work; it's called only once, at program startup, with the ContentControl as argument (weird: I thought that specifying "TextBox.Loaded" as event forced it only on TextBox). – RedGlow Mar 11 '12 at 19:12
  • @RedGlow: I said **on** the `TextBox` (like the `TextChanged`), not in the style, you need it to fire every time the template is instatiated. The `Loaded` event exists on the `ContentControl` as well, so using it in an `EventSetter` with the `"TextBox."` prefix does nothing. – H.B. Mar 11 '12 at 20:12
0

I think the code in Change listbox template on lost focus event in WPF already does what you want. Here's a modification that hides the listbox selection rectangle so that the behaviour is more apparent. When a listboxitem is selected (with a single click) the textbox border becomes visible, but only when the mouse is over it, and it's not yet editable (try typing). When you click it again, or do a double click to select the item in the first place, then can edit it.

<Page.Resources>
    <ResourceDictionary>

        <Style x:Key="NullSelectionStyle" TargetType="ListBoxItem">
            <Style.Resources>
                <!-- SelectedItem with focus -->
                <SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}" Color="Transparent" />
                <!-- SelectedItem without focus -->
                <SolidColorBrush x:Key="{x:Static SystemColors.ControlBrushKey}" Color="Transparent" />
                <!-- SelectedItem text foreground -->
                <SolidColorBrush x:Key="{x:Static SystemColors.HighlightTextBrushKey}" Color="{DynamicResource {x:Static SystemColors.ControlTextColorKey}}" />
            </Style.Resources>
            <Setter Property="FocusVisualStyle" Value="{x:Null}" />
        </Style>

        <Style x:Key="ListBoxSelectableTextBox" TargetType="{x:Type TextBox}">
            <Setter Property="IsHitTestVisible" Value="False" />
            <Style.Triggers>
                <DataTrigger Binding="{Binding IsSelected, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ListBoxItem}, AncestorLevel=1}}" Value="True">
                    <Setter Property="IsHitTestVisible" Value="True" />
                </DataTrigger>
            </Style.Triggers>
        </Style>
    </ResourceDictionary>
</Page.Resources>
<Grid>
    <ListBox ItemsSource="{Binding Departments}" HorizontalContentAlignment="Stretch">
        <ListBox.ItemTemplate>
            <DataTemplate>
                <TextBox Margin="5" Style="{StaticResource ListBoxSelectableTextBox}" Text="{Binding Name}" BorderBrush="{x:Null}"/>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
</Grid>
Community
  • 1
  • 1
Phil
  • 42,255
  • 9
  • 100
  • 100