I am creating a Custom Control to create a particular TextBox style for my application:

Here is the resource dictionary which I have created to get the expected layout:
<Style TargetType="{x:Type ctrl:CustomTextBox}">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="{DynamicResource TextBox_Border_Empty}" />
<Setter Property="Foreground" Value="{DynamicResource TextBox_Label_Empty}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ctrl:CustomTextBox}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="1"
Focusable="False"
CornerRadius="4"
Cursor="IBeam"
Margin="16,8"
MinHeight="{DynamicResource TextBox_Height}"
Padding="20,10,3,10"
x:Name="bdrBorder">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Background="{TemplateBinding Background}"
Grid.Column="0"
HorizontalAlignment="Stretch"
Orientation="Vertical"
VerticalAlignment="Center">
<Label Background="{TemplateBinding Background}"
Content="{TemplateBinding Label}"
Focusable="False"
FontFamily="{DynamicResource Inter600}"
FontSize="12"
Foreground="{TemplateBinding Foreground}"
IsHitTestVisible="False"
Margin="3,0,0,0"
Padding="0,3"
Visibility="{TemplateBinding LabelVisibility}"
x:Name="lblHeading"/>
<TextBox Background="Transparent"
BorderBrush="{TemplateBinding Background}"
BorderThickness="0"
Cursor="IBeam"
Focusable="True"
FontFamily="{DynamicResource Inter400}"
FontSize="16"
Foreground="{DynamicResource Theme_Foreground}"
Margin="0"
MaxLength="{TemplateBinding MaxLength}"
Text="{TemplateBinding Text}"
TextWrapping="Wrap"
x:Name="txtContent" />
</StackPanel>
<ContentControl Focusable="False"
Grid.Column="1"
HorizontalAlignment="Center"
Margin="5,0"
Style="{DynamicResource ErrorIcon_Canvas}"
ToolTip="{TemplateBinding ValidationMessage}"
VerticalAlignment="Center"
x:Name="cvsError" />
<Button Command="{TemplateBinding ButtonCommand}"
Content="{TemplateBinding ButtonLabel}"
Cursor="Hand"
Grid.Column="2"
IsEnabled="True"
Margin="5,0"
Padding="10,7"
Style="{DynamicResource TextBoxWithBorder_Button}"
VerticalAlignment="Center"
Visibility="{TemplateBinding ButtonVisibility}"
x:Name="btnAction"/>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="HasBorder" Value="False">
<Setter TargetName="bdrBorder" Property="BorderBrush" Value="Transparent" />
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="HasBorder" Value="True"/>
<Condition Property="IsValid" Value="False" />
</MultiTrigger.Conditions>
<Setter TargetName="bdrBorder" Property="BorderBrush" Value="{DynamicResource Theme_ErrorBrush}" />
</MultiTrigger>
<Trigger Property="IsValid" Value="False">
<Setter TargetName="cvsError" Property="Visibility" Value="Visible" />
<Setter TargetName="lblHeading" Property="Foreground" Value="{DynamicResource Theme_ErrorBrush}" />
</Trigger>
<Trigger Property="IsValid" Value="True">
<Setter TargetName="cvsError" Property="Visibility" Value="Collapsed" />
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="ShowText" Value="False" />
<Condition Property="IsMouseOver" Value="True" />
</MultiTrigger.Conditions>
<Setter TargetName="lblHeading" Property="FontSize" Value="12" />
<Setter TargetName="txtContent" Property="Visibility" Value="Visible" />
</MultiTrigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="ShowText" Value="False" />
<Condition Property="IsMouseOver" Value="False" />
</MultiTrigger.Conditions>
<Setter TargetName="lblHeading" Property="FontSize" Value="14" />
<Setter TargetName="txtContent" Property="Visibility" Value="Collapsed" />
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
Here is the code for the CustomTextBox (c#):
public class CustomTextBox : ContentControl
{
#region - Border -
public static readonly DependencyProperty HasBorderProperty =
DependencyProperty.Register(nameof(HasBorder), typeof(bool), typeof(CustomTextBox),
new PropertyMetadata(true, OnHasBorderChanged));
public bool HasBorder
{
get => (bool)GetValue(HasBorderProperty);
set => SetValue(HasBorderProperty, value);
}
private static void OnHasBorderChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is CustomTextBox ctrl)
ctrl.HasBorder = (bool)e.NewValue;
}
#endregion - Border -
#region - Button -
public static readonly DependencyProperty ButtonCommandProperty =
DependencyProperty.Register(nameof(ButtonCommand), typeof(ICommand), typeof(CustomTextBox),
new PropertyMetadata(null!, OnButtonCommandChanged));
public ICommand ButtonCommand
{
get => (ICommand)GetValue(ButtonCommandProperty);
set => SetValue(ButtonCommandProperty, value);
}
private static void OnButtonCommandChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is CustomTextBox ctrl)
ctrl.ButtonCommand = (ICommand)e.NewValue;
}
public static readonly DependencyProperty ButtonLabelProperty =
DependencyProperty.Register(nameof(ButtonLabel), typeof(string), typeof(CustomTextBox),
new PropertyMetadata(string.Empty, OnButtonLabelChanged));
public string ButtonLabel
{
get => (string)GetValue(ButtonLabelProperty);
set => SetValue(ButtonLabelProperty, value);
}
private static void OnButtonLabelChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is CustomTextBox ctrl)
{
ctrl.ButtonLabel = (string)e.NewValue;
ctrl.SetButtonVisibility();
}
}
public static readonly DependencyProperty ButtonVisibilityProperty =
DependencyProperty.Register(nameof(ButtonVisibility), typeof(Visibility), typeof(CustomTextBox),
new PropertyMetadata(Visibility.Collapsed, OnButtonVisibilityChanged));
public Visibility ButtonVisibility
{
get => (Visibility)GetValue(ButtonVisibilityProperty);
private set => SetValue(ButtonVisibilityProperty, value);
}
private static void OnButtonVisibilityChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is CustomTextBox ctrl)
ctrl.ButtonVisibility = (Visibility)e.NewValue;
}
private void SetButtonVisibility() => ButtonVisibility = string.IsNullOrWhiteSpace(ButtonLabel) ? Visibility.Collapsed : Visibility.Visible;
#endregion - Button -
#region - Label -
public static readonly DependencyProperty LabelProperty =
DependencyProperty.Register(nameof(Label), typeof(string), typeof(CustomTextBox),
new PropertyMetadata(string.Empty, OnLabelChanged));
public string Label
{
get => (string)GetValue(LabelProperty);
set => SetValue(LabelProperty, value);
}
private static void OnLabelChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is CustomTextBox ctrl)
{
ctrl.Label = (string)e.NewValue;
ctrl.SetLabelVisibility();
}
}
public static readonly DependencyProperty LabelVisibilityProperty =
DependencyProperty.Register(nameof(LabelVisibility), typeof(Visibility), typeof(CustomTextBox),
new PropertyMetadata(Visibility.Collapsed, OnLabelVisibilityChanged));
public Visibility LabelVisibility
{
get => (Visibility)GetValue(LabelVisibilityProperty);
private set => SetValue(LabelVisibilityProperty, value);
}
private static void OnLabelVisibilityChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is CustomTextBox ctrl)
ctrl.LabelVisibility = (Visibility)e.NewValue;
}
private void SetLabelVisibility() => LabelVisibility = string.IsNullOrWhiteSpace(Label) ? Visibility.Collapsed : Visibility.Visible;
#endregion
#region - Text -
public static readonly DependencyProperty MaxLengthProperty =
DependencyProperty.Register(nameof(MaxLength), typeof(int), typeof(CustomTextBox),
new PropertyMetadata(int.MaxValue, OnMaxLengthChanged));
public int MaxLength
{
get => (int)GetValue(MaxLengthProperty);
set => SetValue(MaxLengthProperty, value);
}
private static void OnMaxLengthChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is CustomTextBox ctrl)
ctrl.MaxLength = (int)e.NewValue;
}
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register(nameof(Text), typeof(string), typeof(CustomTextBox),
new PropertyMetadata(string.Empty, OnTextChanged));
public string Text
{
get => (string)GetValue(TextProperty);
set => SetValue(TextProperty, value);
}
private static void OnTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is CustomTextBox ctrl)
{
ctrl.Text = (string)e.NewValue;
ctrl.SetShowText();
}
}
public static readonly DependencyProperty ShowTextProperty =
DependencyProperty.Register(nameof(ShowText), typeof(bool), typeof(CustomTextBox),
new PropertyMetadata(false, OnShowTextChanged));
public bool ShowText
{
get => (bool)(GetValue(ShowTextProperty));
private set => SetValue(ShowTextProperty, value);
}
private static void OnShowTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is CustomTextBox ctrl)
ctrl.ShowText = (bool)e.NewValue;
}
private void SetShowText() => ShowText = !string.IsNullOrWhiteSpace(Text);
#endregion
#region - Validation -
public static readonly DependencyProperty IsValidProperty =
DependencyProperty.Register(nameof(IsValid), typeof(bool), typeof(CustomTextBox),
new PropertyMetadata(true, OnIsValidChanged));
public bool IsValid
{
get => (bool)GetValue(IsValidProperty);
set => SetValue(IsValidProperty, value);
}
private static void OnIsValidChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is CustomTextBox ctrl)
ctrl.IsValid = (bool)e.NewValue;
}
public static readonly DependencyProperty ValidationMessageProperty =
DependencyProperty.Register(nameof(ValidationMessage), typeof(string), typeof(CustomTextBox),
new PropertyMetadata(string.Empty, OnValidationMessageChanged));
public string ValidationMessage
{
get => (string)(GetValue(ValidationMessageProperty));
set => SetValue(ValidationMessageProperty, value);
}
private static void OnValidationMessageChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is CustomTextBox ctrl)
{
ctrl.ValidationMessage = (string)e.NewValue;
ctrl.SetIsValid();
}
}
private void SetIsValid() => IsValid = string.IsNullOrWhiteSpace(ValidationMessage);
#endregion - Validation -
}
I have a couple of issues with this code and I am looking for help resolving:
When I mouse over the TextBox (txtContent) I do not see the IBeam, instead the mouse is set to the default Windows Arrow.
My CustomTextBox I have had to make it implement a ContentControl rather than a TextBox, this is because I need to fire an event when Text is changed, but using a TextBox it seems that event will not fire, if anyone can give me advice on that I would be very happy too. The reason for this is when the TextBox.Text is empty, I want to hide the Texbox element (unless the control has focus or the mouse is over)
When I click anywhere within the Border (bdrBorder) I want to set focus to the TextBox (txtContent) but the only way I can set focus is to click on the actual TextBox, is there a way to achieve this?
I have tried a number of different approaches, but am unable to achieve these things
TextBoxthe cursor must switch to theIBeam(this is handled by theTextBoxinternally). This said, I was not able to reproduce this issue. Cursor toggles correctly.However, instead I observed that you are only able to input text as long as the mouse is over the control (and therefore the control is expanded). As soon as the mouse drifts away while typing, the control collapsed to hide the
TextBoxand keyboard focus is also lost.To fix it I suggest to improve the
MultiTriggerthat toggles theVisibilityof theTextBoxby adding the conditionIsKeyboardFocusWithin == false:TextBoxis the better way to go in my opinion. It eliminates all the delegation between theCustomTextBoxand its internalTextBox. You find the defaultTextBoxStyle here at Microsoft Docs: TextBox ControlTemplate Example. You can add your controls to it. Then in the static constructor of yourCustomTextBoxyou can override the metadata for theTextBox.TextProperty. Here you can register aTextdependency property changed callback. And you can configure theTextPropertyto bind using theUpdateTrigger.PropertyChangedtrigger by default (the native default isUpdateTrigger.LostFocus). Now, the registered callback is called every time theTextproperty changes:TextBoxsolves the issue. However, for your current solution you can override thePreviewLeftMouseButtonUpevent to move the focus explicitly:Some remarks: Your
ButtonCommandshould be a RoutedCommand.All classes that extend
Controlsupport to display validation errors by default.