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?