Search code examples
wpfeventsdatagridtextboxcustom-controls

SelectAll text on Custom TextBox control in a DataGrid on CellEditingTemplate


The question says it all: I can use Events to select all the text in a DataGrid Custom TextBox, BUT it does not work when the TextBox is initially created (i.e. When the cell enters edit mode and create the TextBox).

IF I click in the TextBox after it is created the text is fully selected, but it should already be selected after the TextBox is displayed. This does not work. I tried setting Focus in Code, or using FocusManager in XAML but not helps.

Here is the Code (less the Dependency Properties):

<ResourceDictionary xmlns       = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x     = "http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:ccont = "clr-namespace:App.Controls">

    <!-- Default style for the Validation Buttons -->
    <Style TargetType="{x:Type ccont:vokDataGridEdit}">

        <Setter Property="SnapsToDevicePixels"  Value="true" />

        <Setter Property="Template">
            <Setter.Value>

                <ControlTemplate TargetType="{x:Type ccont:vokDataGridEdit}">

                    <TextBox Text                               = "{Binding Text, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, RelativeSource={RelativeSource AncestorType=ccont:vokDataGridEdit}}"
                             BorderThickness                    = "0"
                             ContextMenuService.Placement       = "Right"
                             ContextMenuService.PlacementTarget = "{Binding Path=., RelativeSource={RelativeSource Self}}"
                             x:Name                             = "TextBox">

                        <TextBox.ContextMenu>
                            <ContextMenu x:Name="Menu">
                                <ContextMenu.Template>
                                    <ControlTemplate>

                                        <Border CornerRadius    = "5"
                                                Background      = "LightGray"
                                                BorderThickness = "1" 
                                                BorderBrush     = "Gray"
                                                Padding         = "2">

                                            <StackPanel Orientation="Vertical">

                                                <!-- Title -->
                                                <TextBlock Text="Test" x:Name = "Test" />

                                                <!-- TODO: List of matches -->
                                                <TextBox Text               = "{Binding Text, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, RelativeSource={RelativeSource AncestorType=ccont:vokDataGridEdit}}" 
                                                         BorderThickness    = "0" />

                                            </StackPanel>

                                        </Border>

                                    </ControlTemplate>
                                </ContextMenu.Template>
                            </ContextMenu>
                        </TextBox.ContextMenu>

                    </TextBox>

                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

</ResourceDictionary>

And Code (Dependency Properties not shown):

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

namespace App.Controls
{
    /// <summary>
    /// DataGrid Edit control (see: https://www.c-sharpcorner.com/article/wpf-routed-events/ for RoutedEvents)
    /// </summary>
    public class vokDataGridEdit : Control
    {
        static vokDataGridEdit()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(vokDataGridEdit), new FrameworkPropertyMetadata(typeof(vokDataGridEdit)));
        }

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();

            // Demo purpose only, check for previous instances and remove the handler first  
            if (this.GetTemplateChild("TextBox") is TextBox button)
            {
                button.PreviewMouseLeftButtonDown   += this.SelectContentPreparation;
                button.GotKeyboardFocus             += this.SelectContent;
                button.MouseDoubleClick             += this.SelectContent;
                //button.GotFocus                     += this.SelectContent;
            }
        }

        /// <summary>
        /// Prepare the Control to ensure it has focus before subsequent event fire
        /// </summary>
        private void SelectContentPreparation(object sender, MouseButtonEventArgs e)
        {
            if (sender is TextBox tb)
            {
                if (!tb.IsKeyboardFocusWithin)
                {
                    e.Handled = true;
                    tb.Focus();
                }
            }
        }

        private void SelectContent(object sender, RoutedEventArgs e)
        {
            if (sender is TextBox tb)
            {
                e.Handled = true;
                tb.SelectAll();
            }
        }
    }
}

Solution

  • Ok finaly using a Behavior solved my issue in conjunction with using Events on the CustomControl. I still have no clues why it did not work using Events though...

    CustomControl XAML:

    <ResourceDictionary xmlns       = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                        xmlns:x     = "http://schemas.microsoft.com/winfx/2006/xaml"
                        xmlns:b     = "http://schemas.microsoft.com/xaml/behaviors"
    
                        xmlns:ccont = "clr-namespace:App.Controls"
                        xmlns:res   = "clr-namespace:App.Controls.Resources"
                        xmlns:valid = "clr-namespace:App.Controls.Validation"
                        xmlns:conv  = "clr-namespace:Common.MVVM.Converter;assembly=Common.MVVM"
                        xmlns:behav = "clr-namespace:Common.MVVM.Behavior;assembly=Common.MVVM">
    
        <!-- Default style for the Validation Buttons -->
        <Style TargetType="{x:Type ccont:vokDataGridEdit}">
    
            <Setter Property="SnapsToDevicePixels"  Value="true" />
    
            <Setter Property="Template">
                <Setter.Value>
    
                    <ControlTemplate TargetType="{x:Type ccont:vokDataGridEdit}">
    
                        <Grid>
    
                            <!-- !!! The Value edited !!! -->
                            <TextBox BorderThickness    = "0"
                                     x:Name             = "textBox">
    
                                <!-- Create a binding proxy to serve binding properties to data validation Binding Wrapper
                                     see: https://social.technet.microsoft.com/wiki/contents/articles/31422.wpf-passing-a-data-bound-value-to-a-validation-rule.aspx -->
                                <TextBox.Resources>
                                    <valid:BindingProxy x:Key="proxy" Context="{Binding HintsInternal, RelativeSource={RelativeSource AncestorType=ccont:vokDataGridEdit}}"/>
                                </TextBox.Resources>
    
                                <!-- Bind with data validation -->
                                <TextBox.Text>
                                    <Binding RelativeSource         = "{RelativeSource AncestorType=ccont:vokDataGridEdit}"
                                             Path                   = "Text"
                                             Mode                   = "TwoWay"
                                             UpdateSourceTrigger    = "PropertyChanged"
                                             ValidatesOnExceptions  = "True">
    
                                        <Binding.ValidationRules>
                                            <valid:vokDataGridEditValidation>
                                                <valid:vokDataGridEditValidation.Bindings>
                                                    <valid:vokDataGridEditValidationBindings InvalidEntries="{Binding Context, Source={StaticResource proxy}}" />
                                                </valid:vokDataGridEditValidation.Bindings>
                                            </valid:vokDataGridEditValidation>
                                        </Binding.ValidationRules>
    
                                    </Binding>
                                </TextBox.Text>
    
                                <!-- Select all text on focus -->
                                <b:Interaction.Behaviors>
                                    <behav:TextBoxSelectAllBehavior />
                                </b:Interaction.Behaviors>
    
                            </TextBox>
    
                        </Grid>
    
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    
    </ResourceDictionary>
    

    And CustomControl event declarations:

    /// <summary>
    /// DataGrid Edit control (see: https://www.c-sharpcorner.com/article/wpf-routed-events/ for RoutedEvents)
    /// </summary>
    public class vokDataGridEdit : Control
    {
        #region Initialization
    
        static vokDataGridEdit()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(vokDataGridEdit), new FrameworkPropertyMetadata(typeof(vokDataGridEdit)));
        }
    
        /// <summary>
        /// Set event handlers when applying the template
        /// </summary>
        public override void OnApplyTemplate()
        {   
            base.OnApplyTemplate();
    
            // OnLoaded set focus to the TextBox (lambda and own Event need not Garbage Collection) and ensure it has focus for new text input
            if (this.GetTemplateChild("textBox") is TextBox textBox)
            {
                this.Loaded             += (sender, e) => { textBox.Focus(); };
                this.PreviewTextInput   += (sender, e) => { textBox.Focus(); };
            }
        }
    
        #endregion
    
        // Skipping Dependency Properties
    }
    

    And Finaly the behavior based on an Answer by user Rekshino here (https://stackoverflow.com/a/60844159/12886393):

    /// <summary>
    /// Behavior for control to select all text in TextBox on GotFocus
    /// </summary>
    public class TextBoxSelectAllBehavior : Behavior<TextBox>
    {
        /// <summary>
        /// Flag marking if Selection is to be performed on MouseUp
        /// </summary>
        private bool _doSelectAll = false;
    
        /// <summary>
        /// Setup the behavior
        /// </summary>
        protected override void OnAttached()
        {
            base.OnAttached();
    
            this.AssociatedObject.GotFocus          += this.AssociatedObject_GotFocus;
            this.AssociatedObject.PreviewMouseUp    += this.AssociatedObject_MouseUp;
            this.AssociatedObject.PreviewMouseDown  += this.AssociatedObject_MouseDown;
        }
    
        /// <summary>
        /// Select all via dispatcher if action set for MouseUp
        /// </summary>
        private void AssociatedObject_MouseUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
        {
            if (this._doSelectAll)
            {
                this.AssociatedObject.Dispatcher.BeginInvoke((Action)(() => { this.AssociatedObject.SelectAll(); }));
            }
    
            this._doSelectAll = false;
        }
    
        /// <summary>
        /// Triggers SelectAll on mouse up if focus was not set
        /// </summary>
        private void AssociatedObject_MouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
        {
            this._doSelectAll = !this.AssociatedObject.IsFocused;
        }
    
        /// <summary>
        /// Selects all
        /// </summary>
        private void AssociatedObject_GotFocus(object sender, System.Windows.RoutedEventArgs e)
        {
            this.AssociatedObject.SelectAll();
        }
    
        /// <summary>
        /// Clean-up the behavior
        /// </summary>
        protected override void OnDetaching()
        {
            this.AssociatedObject.GotFocus          -= this.AssociatedObject_GotFocus;
            this.AssociatedObject.PreviewMouseUp    -= this.AssociatedObject_MouseUp;
            this.AssociatedObject.PreviewMouseDown  -= this.AssociatedObject_MouseDown;
    
            base.OnDetaching();
        }
    }