简体   繁体   中英

Focusing element produced in a style trigger via ContentTemplate

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

一种适用于此设置的方法是处理LoadedTextBox ,然后处理到Keyboard.Focus上( sender )。

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>

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM