Search TextBox Control (adapted from another control) cannot fire textchanged, nor can I set or get text WPF

Here is my searchtextbox class, the actual textbox inside the XAML design is PART_TextBox, I have tried using PART_TextBox_TextChanged, TextBox_TextChanged, and OnTextBox_TextChanged, none work, and .text is empty always.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace SearchTextBox
{


[TemplatePart(Name = PartRootPanelName, Type = typeof(Panel))]
[TemplatePart(Name = PartTextBoxName, Type = typeof(TextBox))]
[TemplatePart(Name = PartSearchIconName, Type = typeof(Button))]
[TemplatePart(Name = PartCloseButtonName, Type = typeof(Button))]
public class SearchTextBox : Control
{

    private const string PartRootPanelName = "PART_RootPanel";
    private const string PartTextBoxName = "PART_TextBox";
    private const string PartSearchIconName = "PART_SearchIcon";
    private const string PartCloseButtonName = "PART_CloseButton";


    // Commands.

    public static readonly RoutedCommand ActivateSearchCommand;
    public static readonly RoutedCommand CancelSearchCommand;


    // Properties.

    public static readonly DependencyProperty HandleClickOutsidesProperty;
    public static readonly DependencyProperty UpdateDelayMillisProperty;
    public static readonly DependencyProperty HintTextProperty;
    public static readonly DependencyProperty DefaultFocusedElementProperty;
    public static readonly DependencyProperty TextBoxTextProperty;
    public static readonly DependencyProperty TextBoxTextChangedProperty;



    static SearchTextBox()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(SearchTextBox), new FrameworkPropertyMetadata(typeof(SearchTextBox)));

        ActivateSearchCommand = new RoutedCommand();
        CancelSearchCommand = new RoutedCommand();


        // // Using of CommandManager.
        // // https://www.codeproject.com/Articles/43295/ZoomBoxPanel-add-custom-commands-to-a-WPF-control
        CommandManager.RegisterClassCommandBinding(typeof(SearchTextBox),
            new CommandBinding(ActivateSearchCommand, ActivateSearchCommand_Invoke));

        CommandManager.RegisterClassCommandBinding(typeof(SearchTextBox),
            new CommandBinding(CancelSearchCommand, CancelSearchCommand_Invoke));


        // Register properties.

        HandleClickOutsidesProperty = DependencyProperty.Register(
            nameof(HandleClickOutsides), typeof(bool), typeof(SearchTextBox),
            new UIPropertyMetadata(true));
        // // Set default value.
        // // https://stackoverflow.com/questions/6729568/how-can-i-set-a-default-value-for-a-dependency-property-of-type-derived-from-dep

        UpdateDelayMillisProperty = DependencyProperty.Register(
            nameof(UpdateDelayMillis), typeof(int), typeof(SearchTextBox),
            new UIPropertyMetadata(1000));

        HintTextProperty = DependencyProperty.Register(
        nameof(HintText), typeof(string), typeof(SearchTextBox),
        new UIPropertyMetadata("Set HintText property"));

        DefaultFocusedElementProperty = DependencyProperty.Register(
            nameof(DefaultFocusedElement), typeof(UIElement), typeof(SearchTextBox));

        TextBoxTextProperty =
            DependencyProperty.Register(
                "Text",
                typeof(string),
                typeof(SearchTextBox));

       

    }

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

    public static void ActivateSearchCommand_Invoke(object sender, ExecutedRoutedEventArgs e)
    {
        if (sender is SearchTextBox self)
            self.ActivateSearch();
    }


    public static void CancelSearchCommand_Invoke(object sender, ExecutedRoutedEventArgs e)
    {
        if (sender is SearchTextBox self)
        {
            self.textBox.Text = "";
            self.CancelPreviousSearchFilterUpdateTask();
            self.UpdateFilterText();
            self.DeactivateSearch();
        }
    }


    private static UIElement GetFirstSelectedControl(Selector list)
        => list.SelectedItem == null ? null :
            list.ItemContainerGenerator.ContainerFromItem(list.SelectedItem) as UIElement;


    private static UIElement GetDefaultSelectedControl(Selector list)
        => list.ItemContainerGenerator.ContainerFromIndex(0) as UIElement;


    // Events.

    // // Using of events.
    // // https://stackoverflow.com/questions/13447940/how-to-create-user-define-new-event-for-user-control-in-wpf-one-small-example
    public event EventHandler SearchTextFocused;
    public event EventHandler SearchTextUnfocused;
    // // Parameter passing:
    // // https://stackoverflow.com/questions/4254636/how-to-create-a-custom-event-handling-class-like-eventargs
    public event EventHandler<string> SearchRequested;

    public event TextChangedEventHandler TextChanged;



    

    // Parts.

    private Panel rootPanel;
    private TextBox textBox;
    private Button searchIcon;
    private Label closeButton;


    // Handlers.

    // Field for click-outsides handling.
    private readonly MouseButtonEventHandler windowWideMouseButtonEventHandler;


    // Other fields.

    private CancellationTokenSource waitingSearchUpdateTaskCancellationTokenSource;


    // <init>

    public SearchTextBox()
    {
        // Click events in the window will be previewed by
        // function OnWindowWideMouseEvent (defined later)
        // when the handler is on. Now it's off.
        windowWideMouseButtonEventHandler =
            new MouseButtonEventHandler(OnWindowWideMouseEvent);

       
    }

   

    // Properties.
    

    public bool HandleClickOutsides
    {
        get => (bool)GetValue(HandleClickOutsidesProperty);
        set => SetValue(HandleClickOutsidesProperty, value);
    }

    public int UpdateDelayMillis
    {
        get => (int)GetValue(UpdateDelayMillisProperty);
        set => SetValue(UpdateDelayMillisProperty, value);
    }

    public string HintText
    {
        get => (string)GetValue(HintTextProperty);
        set => SetValue(HintTextProperty, value);
    }


    public UIElement DefaultFocusedElement
    {
        get => (UIElement)GetValue(DefaultFocusedElementProperty);
        set => SetValue(DefaultFocusedElementProperty, value);
    }


    //Event handler functions.


    // This would only be on whenever search box is focused.
    private void OnWindowWideMouseEvent(object sender, MouseButtonEventArgs e)
    {
        // By clicking outsides the search box deactivate the search box.
        if (!IsMouseOver) DeactivateSearch();
    }


    private void PART_TextBox_TextChanged(object sender, TextChangedEventArgs e)
    {
        TextChanged?.Invoke(this, e);
    }


    public void OnTextBox_GotFocus(object sender, RoutedEventArgs e)
    {
        SearchTextFocused?.Invoke(this, e);
        if (HandleClickOutsides)
            // Get window.
            // https://stackoverflow.com/questions/302839/wpf-user-control-parent
            Window.GetWindow(this).AddHandler(
                Window.PreviewMouseDownEvent, windowWideMouseButtonEventHandler);
    }


    public void OnTextBox_LostFocus(object sender, RoutedEventArgs e)
    {
        SearchTextUnfocused?.Invoke(this, e);
        if (HandleClickOutsides)
            Window.GetWindow(this).RemoveHandler(
                Window.PreviewMouseDownEvent, windowWideMouseButtonEventHandler);
    }


    private void OnTextBox_KeyUp(object sender, KeyEventArgs e)
    {
        if (e.Key == Key.Escape)
            CancelSearchCommand.Execute(null, this);
        else if (e.Key == Key.Enter)
        {
            CancelPreviousSearchFilterUpdateTask();
            UpdateFilterText();
        }
        else
        {
            CancelPreviousSearchFilterUpdateTask();
            // delay == 0: Update now;
            // delay < 0: Don't update except Enter or Esc;
            // delay > 0: Delay and update.
            var delay = UpdateDelayMillis;
            if (delay == 0) UpdateFilterText();
            else if (delay > 0)
            {
                // // Delayed task.
                // // https://stackoverflow.com/questions/15599884/how-to-put-delay-before-doing-an-operation-in-wpf
                waitingSearchUpdateTaskCancellationTokenSource = new CancellationTokenSource();
                Task.Delay(delay, waitingSearchUpdateTaskCancellationTokenSource.Token)
                   .ContinueWith(self => {
                       if (!self.IsCanceled) Dispatcher.Invoke(() => UpdateFilterText());
                   });
            }
        }
    }


    // Public interface.


    public void ActivateSearch()
    {
        textBox?.Focus();
    }

    public void DeactivateSearch()
    {
        // // Use keyboard focus instead.
        // // https://stackoverflow.com/questions/2914495/wpf-how-to-programmatically-remove-focus-from-a-textbox
        //Keyboard.ClearFocus();
        if (DefaultFocusedElement != null)
        {
            UIElement focusee = null;
            if (DefaultFocusedElement is Selector list)
            {
                focusee = GetFirstSelectedControl(list);
                if (focusee == null)
                    focusee = GetDefaultSelectedControl(list);
            }
            if (focusee == null) focusee = DefaultFocusedElement;
            Keyboard.Focus(focusee);
        }
        else
        {
            rootPanel.Focusable = true;
            Keyboard.Focus(rootPanel);
            rootPanel.Focusable = false;
        }
    }


    // Helper functions.


    private void CancelPreviousSearchFilterUpdateTask()
    {
        if (waitingSearchUpdateTaskCancellationTokenSource != null)
        {
            waitingSearchUpdateTaskCancellationTokenSource.Cancel();
            waitingSearchUpdateTaskCancellationTokenSource.Dispose();
            waitingSearchUpdateTaskCancellationTokenSource = null;
        }
    }


    private void UpdateFilterText() => SearchRequested?.Invoke(this, textBox.Text);


    // .

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

        // // Idea of detaching.
        // // https://www.jeff.wilcox.name/2010/04/template-part-tips/

        if (textBox != null)
        {
            textBox.GotKeyboardFocus -= OnTextBox_GotFocus;
            textBox.LostKeyboardFocus -= OnTextBox_LostFocus;
            textBox.KeyUp -= OnTextBox_KeyUp;
        }

        rootPanel = GetTemplateChild(PartRootPanelName) as Panel;
        textBox = GetTemplateChild(PartTextBoxName) as TextBox;
        searchIcon = GetTemplateChild(PartSearchIconName) as Button;
        closeButton = GetTemplateChild(PartCloseButtonName) as Label;

        if (textBox != null)
        {
            textBox.GotKeyboardFocus += OnTextBox_GotFocus;
            textBox.LostKeyboardFocus += OnTextBox_LostFocus;
            textBox.KeyUp += OnTextBox_KeyUp;
            }
        }
    }
}

My XAML:

<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:SearchTextBox" xmlns:componentModel="clr-namespace:System.ComponentModel;assembly=WindowsBase">
<Style TargetType="{x:Type local:SearchTextBox}">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type local:SearchTextBox}">
                <Border Background="{TemplateBinding Background}"
                        BorderBrush="{TemplateBinding BorderBrush}"
                        BorderThickness="{TemplateBinding BorderThickness}">

                    <Grid Name="PART_RootPanel">
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="*"/>
                            <ColumnDefinition Width="28"/>
                        </Grid.ColumnDefinitions>

                        <Grid Grid.Column="0">
                            <TextBox Name="PART_TextBox" Background="#FF333337" BorderThickness="0" VerticalContentAlignment="Center" Foreground="White" FontSize="14px" Text=""/>
                            <TextBlock IsHitTestVisible="False" Text="{TemplateBinding HintText}" VerticalAlignment="Center" HorizontalAlignment="Left" Margin="4,0,0,0" FontSize="14px" Foreground="#FF7C7777">
                                <TextBlock.Style>
                                    <Style  TargetType="{x:Type TextBlock}">
                                        <Setter Property="Visibility" Value="Collapsed"/>
                                        <Style.Triggers>
                                            <DataTrigger Binding="{Binding Text, ElementName=PART_TextBox}" Value="">
                                                <Setter Property="Visibility" Value="Visible"/>
                                            </DataTrigger>
                                        </Style.Triggers>
                                    </Style>
                                </TextBlock.Style>
                            </TextBlock>
                        </Grid>
                        <Button Width="28" Grid.Column="1" Name="PART_SearchIcon"  Content="🔍" Background="#FF252526"
                                Focusable="False" Command="{x:Static local:SearchTextBox.ActivateSearchCommand}">
                            <Button.Template>
                                <ControlTemplate TargetType="Button">
                                    <Grid Background="#FF333337">
                                        <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
                                    </Grid>
                                </ControlTemplate>
                            </Button.Template>
                            <Button.Style>
                                <Style TargetType="Button">
                                    <Setter Property="Visibility" Value="Collapsed"/>
                                    <Style.Triggers>
                                        <DataTrigger Binding="{Binding Text, ElementName=PART_TextBox}" Value="">
                                            <Setter Property="Visibility" Value="Visible"/>
                                        </DataTrigger>
                                    </Style.Triggers>
                                </Style>
                            </Button.Style>
                        </Button>
                        <Label Grid.Column="1" Width="28" HorizontalAlignment="Center" HorizontalContentAlignment="Center" Cursor="Hand" VerticalContentAlignment="Center" VerticalAlignment="Stretch" Margin="0" Padding="4" Foreground="White" FontWeight="Bold" Content="x" Name="PART_CloseButton" Focusable="False"
                                   Background="#FF333337">
                            <Label.InputBindings>
                                <MouseBinding Command="{x:Static local:SearchTextBox.CancelSearchCommand}" MouseAction="LeftClick" />
                            </Label.InputBindings>
                         
                            <Label.Style>
                                <Style TargetType="Label">
                                    <Setter Property="Visibility" Value="Visible"/>
                                    <Style.Triggers>
                                        <DataTrigger Binding="{Binding Text, ElementName=PART_TextBox}" Value="">
                                            <Setter Property="Visibility" Value="Collapsed"/>
                                        </DataTrigger>
                                        <Trigger Property="IsMouseOver" Value="True">
                                            <Setter Property="Background" Value="#000"/>
                                        </Trigger>
                                    </Style.Triggers>
                                </Style>
                            </Label.Style>
                        </Label>
                       
                    </Grid>    
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

The control builds fine, but I cannot get searchTextBox.Text, it returns null, and on TextChanged does not fire. Any ideas?

Answer

I would suggest that you do a bit more research and learning about the code you’re using before you copy and paste it. Try to understand what it’s doing and why before use it.

  1. You don’t reference your method PART_TextBox_TextChanged anywhere, it’s not used in OnApplyTemplate where I would expect it to be assigned as a handler. Without that, it will never be called. I would expect a textBox.TextChanged -= PART_TextBox_TextBox_TextChanged in the first if statement, and a textBox.TextChanged += PART_TextBox_TextBox_TextChanged in the second.
  2. You never set the value of your TextBoxText dependency property anywhere; neither in code nor with a Binding or TemplateBinding. Your Text property is referencing TextBoxText for its value, which would always be null if it’s never set. public static readonly DependencyProperty TextBoxTextProperty; (along with the code in static SearchTextBox()) declares that a dependency property exists, but the value of it is never set.
    In a custom control like this, you can bind the Text property of PART_TextBox to your TextBoxText property using a TemplateBinding like so:
    <TextBox Name="PART_TextBox" ... Text="{TemplateBinding TextBoxText}"/>

Leave a Reply

Your email address will not be published. Required fields are marked *