Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
The following example shows how to display placeholder text in a TextBox when the TextBox
is empty. When the TextBox
has text, the placeholder text is hidden. Placeholder text help users understand what type of input the TextBox
expects.
In this article you learn how to:
- Create an attached property to provide the placeholder text.
- Create an adorner to display the placeholder text.
- Add the attached property to a TextBox control.
Create an attached property
With attached properties, you can append values to a control. You use this feature a lot in WPF, such as when you set Grid.Row
or Panel.ZIndex
properties on a control. For more information, see Attached Properties Overview. This example uses attached properties to add placeholder text to a TextBox.
Add a new class to your project named
TextBoxHelper
and open it.Add the following namespaces:
using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Documents; using System.Windows.Media;
Imports System.Linq Imports System.Security.Cryptography Imports System.Windows Imports System.Windows.Controls Imports System.Windows.Documents Imports System.Windows.Media
Create a new dependency property named
Placeholder
.This dependency property uses the property changed callback delegate.
public static string GetPlaceholder(DependencyObject obj) => (string)obj.GetValue(PlaceholderProperty); public static void SetPlaceholder(DependencyObject obj, string value) => obj.SetValue(PlaceholderProperty, value); public static readonly DependencyProperty PlaceholderProperty = DependencyProperty.RegisterAttached( "Placeholder", typeof(string), typeof(TextBoxHelper), new FrameworkPropertyMetadata( defaultValue: null, propertyChangedCallback: OnPlaceholderChanged) );
Public Shared Function GetPlaceholder(obj As DependencyObject) As String Return obj.GetValue(PlaceholderProperty) End Function Public Shared Sub SetPlaceholder(obj As DependencyObject, value As String) obj.SetValue(PlaceholderProperty, value) End Sub Public Shared ReadOnly PlaceholderProperty As DependencyProperty = DependencyProperty.RegisterAttached( "Placeholder", GetType(String), GetType(TextBoxHelper), New FrameworkPropertyMetadata( defaultValue:=Nothing, propertyChangedCallback:=AddressOf OnPlaceholderChanged) )
Create the
OnPlaceholderChanged
method to integrate the attached property with aTextBox
.private static void OnPlaceholderChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is TextBox textBoxControl) { if (!textBoxControl.IsLoaded) { // Ensure that the events are not added multiple times textBoxControl.Loaded -= TextBoxControl_Loaded; textBoxControl.Loaded += TextBoxControl_Loaded; } textBoxControl.TextChanged -= TextBoxControl_TextChanged; textBoxControl.TextChanged += TextBoxControl_TextChanged; // If the adorner exists, invalidate it to draw the current text if (GetOrCreateAdorner(textBoxControl, out PlaceholderAdorner adorner)) adorner.InvalidateVisual(); } }
Private Shared Sub OnPlaceholderChanged(d As DependencyObject, e As DependencyPropertyChangedEventArgs) Dim textBoxControl = TryCast(d, TextBox) If textBoxControl IsNot Nothing Then If Not textBoxControl.IsLoaded Then 'Ensure that the events are not added multiple times RemoveHandler textBoxControl.Loaded, AddressOf TextBoxControl_Loaded AddHandler textBoxControl.Loaded, AddressOf TextBoxControl_Loaded End If RemoveHandler textBoxControl.TextChanged, AddressOf TextBoxControl_TextChanged AddHandler textBoxControl.TextChanged, AddressOf TextBoxControl_TextChanged 'If the adorner exists, invalidate it to draw the current text Dim adorner As PlaceholderAdorner = Nothing If GetOrCreateAdorner(textBoxControl, adorner) Then adorner.InvalidateVisual() End If End If End Sub
There are two ways this method is called when the attached property value changes:
- When the attached property is first added to a
TextBox
, this method is called. That action provides an opportunity for the attached property to integrate with the control's events. - Whenever this property is changed, the adorner can be invalidated to refresh the visual placeholder text.
The
GetOrCreateAdorner
method is created in the next section.- When the attached property is first added to a
Create the event handlers for the
TextBox
.private static void TextBoxControl_Loaded(object sender, RoutedEventArgs e) { if (sender is TextBox textBoxControl) { textBoxControl.Loaded -= TextBoxControl_Loaded; GetOrCreateAdorner(textBoxControl, out _); } } private static void TextBoxControl_TextChanged(object sender, TextChangedEventArgs e) { if (sender is TextBox textBoxControl && GetOrCreateAdorner(textBoxControl, out PlaceholderAdorner adorner)) { // Control has text. Hide the adorner. if (textBoxControl.Text.Length > 0) adorner.Visibility = Visibility.Hidden; // Control has no text. Show the adorner. else adorner.Visibility = Visibility.Visible; } }
Private Shared Sub TextBoxControl_Loaded(sender As Object, e As RoutedEventArgs) Dim textBoxControl As TextBox = TryCast(sender, TextBox) If textBoxControl IsNot Nothing Then RemoveHandler textBoxControl.Loaded, AddressOf TextBoxControl_Loaded GetOrCreateAdorner(textBoxControl, Nothing) End If End Sub Private Shared Sub TextBoxControl_TextChanged(sender As Object, e As TextChangedEventArgs) Dim textBoxControl As TextBox = TryCast(sender, TextBox) Dim adorner As PlaceholderAdorner = Nothing If textBoxControl IsNot Nothing AndAlso GetOrCreateAdorner(textBoxControl, adorner) Then If textBoxControl.Text.Length > 0 Then 'Control has text. Hide the adorner. adorner.Visibility = Visibility.Hidden Else 'Control has no text. Show the adorner. adorner.Visibility = Visibility.Visible End If End If End Sub
The Loaded event is handled so that the adorner can be created after the control's template is applied. The handler removes itself after the event is raised and the adorner is created.
The TextChanged event is handled to ensure that the adorner is hidden or displayed depending if the Text is set to a value.
Create an adorner
The Adorner is a visual that's bound to a control and rendered in an AdornerLayer. For more information, see Adorners Overview.
Open the
TextBoxHelper
class.Add the following code to create the
GetOrCreateAdorner
method.private static bool GetOrCreateAdorner(TextBox textBoxControl, out PlaceholderAdorner adorner) { // Get the adorner layer AdornerLayer layer = AdornerLayer.GetAdornerLayer(textBoxControl); // If null, it doesn't exist or the control's template isn't loaded if (layer == null) { adorner = null; return false; } // Layer exists, try to find the adorner adorner = layer.GetAdorners(textBoxControl)?.OfType<PlaceholderAdorner>().FirstOrDefault(); // Adorner never added to control, so add it if (adorner == null) { adorner = new PlaceholderAdorner(textBoxControl); layer.Add(adorner); } return true; }
Private Shared Function GetOrCreateAdorner(textBoxControl As TextBox, ByRef adorner As PlaceholderAdorner) As Boolean 'Get the adorner layer Dim layer As AdornerLayer = AdornerLayer.GetAdornerLayer(textBoxControl) 'If nothing, it doesn't exist or the control's template isn't loaded If layer Is Nothing Then adorner = Nothing Return False End If 'Layer exists, try to find the adorner adorner = layer.GetAdorners(textBoxControl)?.OfType(Of PlaceholderAdorner)().FirstOrDefault() 'Adorner never added to control, so add it If adorner Is Nothing Then adorner = New PlaceholderAdorner(textBoxControl) layer.Add(adorner) End If Return True End Function
This method provides a safe way to either add or retrieve the Adorner. Adorners require extra safety because they're added to the control's AdornerLayer, which might not exist. When a XAML attached property is applied to a control, the control's template hasn't yet been applied to create the visual tree, so the adorner layer doesn't exist. The adorner layer must be retrieved after the control is loaded. The adorner layer might also be missing if a template that omits the adorner layer is applied to the control.
Add a child class named
PlaceholderAdorner
to theTextBoxHelper
class.public class PlaceholderAdorner : Adorner { public PlaceholderAdorner(TextBox textBox) : base(textBox) { } protected override void OnRender(DrawingContext drawingContext) { TextBox textBoxControl = (TextBox)AdornedElement; string placeholderValue = TextBoxHelper.GetPlaceholder(textBoxControl); if (string.IsNullOrEmpty(placeholderValue)) return; // Create the formatted text object FormattedText text = new FormattedText( placeholderValue, System.Globalization.CultureInfo.CurrentCulture, textBoxControl.FlowDirection, new Typeface(textBoxControl.FontFamily, textBoxControl.FontStyle, textBoxControl.FontWeight, textBoxControl.FontStretch), textBoxControl.FontSize, SystemColors.InactiveCaptionBrush, VisualTreeHelper.GetDpi(textBoxControl).PixelsPerDip); text.MaxTextWidth = System.Math.Max(textBoxControl.ActualWidth - textBoxControl.Padding.Left - textBoxControl.Padding.Right, 10); text.MaxTextHeight = System.Math.Max(textBoxControl.ActualHeight, 10); // Render based on padding of the control, to try and match where the textbox places text Point renderingOffset = new Point(textBoxControl.Padding.Left, textBoxControl.Padding.Top); // Template contains the content part; adjust sizes to try and align the text if (textBoxControl.Template.FindName("PART_ContentHost", textBoxControl) is FrameworkElement part) { Point partPosition = part.TransformToAncestor(textBoxControl).Transform(new Point(0, 0)); renderingOffset.X += partPosition.X; renderingOffset.Y += partPosition.Y; text.MaxTextWidth = System.Math.Max(part.ActualWidth - renderingOffset.X, 10); text.MaxTextHeight = System.Math.Max(part.ActualHeight, 10); } // Draw the text drawingContext.DrawText(text, renderingOffset); } }
Public Class PlaceholderAdorner Inherits Adorner Public Sub New(adornedElement As UIElement) MyBase.New(adornedElement) End Sub Protected Overrides Sub OnRender(drawingContext As DrawingContext) Dim textBoxControl As TextBox = DirectCast(AdornedElement, TextBox) Dim placeholderValue As String = TextBoxHelper.GetPlaceholder(textBoxControl) If String.IsNullOrEmpty(placeholderValue) Then Return End If 'Create the formatted text object Dim text As New FormattedText( placeholderValue, System.Globalization.CultureInfo.CurrentCulture, textBoxControl.FlowDirection, New Typeface(textBoxControl.FontFamily, textBoxControl.FontStyle, textBoxControl.FontWeight, textBoxControl.FontStretch), textBoxControl.FontSize, SystemColors.InactiveCaptionBrush, VisualTreeHelper.GetDpi(textBoxControl).PixelsPerDip) text.MaxTextWidth = Math.Max(textBoxControl.ActualWidth - textBoxControl.Padding.Left - textBoxControl.Padding.Right, 10) text.MaxTextHeight = Math.Max(textBoxControl.ActualHeight, 10) 'Render based on padding of the control, to try and match where the textbox places text Dim renderingOffset As New Point(textBoxControl.Padding.Left, textBoxControl.Padding.Top) 'Template contains the content part; adjust sizes to try and align the text Dim part As FrameworkElement = TryCast(textBoxControl.Template.FindName("PART_ContentHost", textBoxControl), FrameworkElement) If part IsNot Nothing Then Dim partPosition As Point = part.TransformToAncestor(textBoxControl).Transform(New Point(0, 0)) renderingOffset.X += partPosition.X renderingOffset.Y += partPosition.Y text.MaxTextWidth = Math.Max(part.ActualWidth - renderingOffset.X, 10) text.MaxTextHeight = Math.Max(part.ActualHeight, 10) End If ' Draw the text drawingContext.DrawText(text, renderingOffset) End Sub End Class
An adorner inherits from the Adorner class. This particular adorner overrides the OnRender(DrawingContext) method to draw the placeholder text. Let's breakdown the code:
- First, check that the placeholder text exists by calling
TextBoxHelper.GetPlaceholder(textBoxControl)
. - Create a FormattedText object. This object contains all of the information about what text is drawn on the visual.
- Both the FormattedText.MaxTextWidth and FormattedText.MaxTextHeight properties are set to the region of the control. They're also set a minimum value of 10 to make sure the
FormattedText
object is valid. - The
renderingOffset
stores the position of the drawn text. - Use the
PART_ContentHost
If the control's template declares it. This part represents where the text is drawn on the control's template. If that part is found, modify therenderingOffset
to account for its position. - Draw the text by calling DrawText(FormattedText, Point) and passing the
FormattedText
object and the position of the text.
- First, check that the placeholder text exists by calling
Apply the attached property
Once the attached property is defined, its namespace needs to be imported into the XAML, and then referenced on a TextBox control. The following code maps the .NET namespace DotnetDocsSample
to the XML namespace l
.
<Window x:Class="DotnetDocsSample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:l="clr-namespace:DotnetDocsSample"
Title="Recipe Tracker" Width="400" SizeToContent="Height">
<StackPanel Margin="10">
<TextBlock FontSize="20" TextWrapping="Wrap">Welcome to Recipe Tracker! To get started, create a new account.</TextBlock>
<Label Padding="0,5">Name</Label>
<TextBox l:TextBoxHelper.Placeholder="Ex. Jeffry Goh" />
<Label Padding="0,5">Email</Label>
<TextBox l:TextBoxHelper.Placeholder="[email protected]" />
<Label Padding="0,5">Password</Label>
<PasswordBox />
<Button HorizontalAlignment="Right" Width="100" Margin="0,10,0,5">Submit</Button>
</StackPanel>
</Window>
The attached property is added to a TextBox
using the syntax xmlNamespace:Class.Property
.
Complete example
The following code is the complete TextBoxHelper
class.
public static class TextBoxHelper
{
public static string GetPlaceholder(DependencyObject obj) =>
(string)obj.GetValue(PlaceholderProperty);
public static void SetPlaceholder(DependencyObject obj, string value) =>
obj.SetValue(PlaceholderProperty, value);
public static readonly DependencyProperty PlaceholderProperty =
DependencyProperty.RegisterAttached(
"Placeholder",
typeof(string),
typeof(TextBoxHelper),
new FrameworkPropertyMetadata(
defaultValue: null,
propertyChangedCallback: OnPlaceholderChanged)
);
private static void OnPlaceholderChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is TextBox textBoxControl)
{
if (!textBoxControl.IsLoaded)
{
// Ensure that the events are not added multiple times
textBoxControl.Loaded -= TextBoxControl_Loaded;
textBoxControl.Loaded += TextBoxControl_Loaded;
}
textBoxControl.TextChanged -= TextBoxControl_TextChanged;
textBoxControl.TextChanged += TextBoxControl_TextChanged;
// If the adorner exists, invalidate it to draw the current text
if (GetOrCreateAdorner(textBoxControl, out PlaceholderAdorner adorner))
adorner.InvalidateVisual();
}
}
private static void TextBoxControl_Loaded(object sender, RoutedEventArgs e)
{
if (sender is TextBox textBoxControl)
{
textBoxControl.Loaded -= TextBoxControl_Loaded;
GetOrCreateAdorner(textBoxControl, out _);
}
}
private static void TextBoxControl_TextChanged(object sender, TextChangedEventArgs e)
{
if (sender is TextBox textBoxControl
&& GetOrCreateAdorner(textBoxControl, out PlaceholderAdorner adorner))
{
// Control has text. Hide the adorner.
if (textBoxControl.Text.Length > 0)
adorner.Visibility = Visibility.Hidden;
// Control has no text. Show the adorner.
else
adorner.Visibility = Visibility.Visible;
}
}
private static bool GetOrCreateAdorner(TextBox textBoxControl, out PlaceholderAdorner adorner)
{
// Get the adorner layer
AdornerLayer layer = AdornerLayer.GetAdornerLayer(textBoxControl);
// If null, it doesn't exist or the control's template isn't loaded
if (layer == null)
{
adorner = null;
return false;
}
// Layer exists, try to find the adorner
adorner = layer.GetAdorners(textBoxControl)?.OfType<PlaceholderAdorner>().FirstOrDefault();
// Adorner never added to control, so add it
if (adorner == null)
{
adorner = new PlaceholderAdorner(textBoxControl);
layer.Add(adorner);
}
return true;
}
public class PlaceholderAdorner : Adorner
{
public PlaceholderAdorner(TextBox textBox) : base(textBox) { }
protected override void OnRender(DrawingContext drawingContext)
{
TextBox textBoxControl = (TextBox)AdornedElement;
string placeholderValue = TextBoxHelper.GetPlaceholder(textBoxControl);
if (string.IsNullOrEmpty(placeholderValue))
return;
// Create the formatted text object
FormattedText text = new FormattedText(
placeholderValue,
System.Globalization.CultureInfo.CurrentCulture,
textBoxControl.FlowDirection,
new Typeface(textBoxControl.FontFamily,
textBoxControl.FontStyle,
textBoxControl.FontWeight,
textBoxControl.FontStretch),
textBoxControl.FontSize,
SystemColors.InactiveCaptionBrush,
VisualTreeHelper.GetDpi(textBoxControl).PixelsPerDip);
text.MaxTextWidth = System.Math.Max(textBoxControl.ActualWidth - textBoxControl.Padding.Left - textBoxControl.Padding.Right, 10);
text.MaxTextHeight = System.Math.Max(textBoxControl.ActualHeight, 10);
// Render based on padding of the control, to try and match where the textbox places text
Point renderingOffset = new Point(textBoxControl.Padding.Left, textBoxControl.Padding.Top);
// Template contains the content part; adjust sizes to try and align the text
if (textBoxControl.Template.FindName("PART_ContentHost", textBoxControl) is FrameworkElement part)
{
Point partPosition = part.TransformToAncestor(textBoxControl).Transform(new Point(0, 0));
renderingOffset.X += partPosition.X;
renderingOffset.Y += partPosition.Y;
text.MaxTextWidth = System.Math.Max(part.ActualWidth - renderingOffset.X, 10);
text.MaxTextHeight = System.Math.Max(part.ActualHeight, 10);
}
// Draw the text
drawingContext.DrawText(text, renderingOffset);
}
}
}
Public Class TextBoxHelper
Public Shared Function GetPlaceholder(obj As DependencyObject) As String
Return obj.GetValue(PlaceholderProperty)
End Function
Public Shared Sub SetPlaceholder(obj As DependencyObject, value As String)
obj.SetValue(PlaceholderProperty, value)
End Sub
Public Shared ReadOnly PlaceholderProperty As DependencyProperty =
DependencyProperty.RegisterAttached(
"Placeholder",
GetType(String),
GetType(TextBoxHelper),
New FrameworkPropertyMetadata(
defaultValue:=Nothing,
propertyChangedCallback:=AddressOf OnPlaceholderChanged)
)
Private Shared Sub OnPlaceholderChanged(d As DependencyObject, e As DependencyPropertyChangedEventArgs)
Dim textBoxControl = TryCast(d, TextBox)
If textBoxControl IsNot Nothing Then
If Not textBoxControl.IsLoaded Then
'Ensure that the events are not added multiple times
RemoveHandler textBoxControl.Loaded, AddressOf TextBoxControl_Loaded
AddHandler textBoxControl.Loaded, AddressOf TextBoxControl_Loaded
End If
RemoveHandler textBoxControl.TextChanged, AddressOf TextBoxControl_TextChanged
AddHandler textBoxControl.TextChanged, AddressOf TextBoxControl_TextChanged
'If the adorner exists, invalidate it to draw the current text
Dim adorner As PlaceholderAdorner = Nothing
If GetOrCreateAdorner(textBoxControl, adorner) Then
adorner.InvalidateVisual()
End If
End If
End Sub
Private Shared Sub TextBoxControl_Loaded(sender As Object, e As RoutedEventArgs)
Dim textBoxControl As TextBox = TryCast(sender, TextBox)
If textBoxControl IsNot Nothing Then
RemoveHandler textBoxControl.Loaded, AddressOf TextBoxControl_Loaded
GetOrCreateAdorner(textBoxControl, Nothing)
End If
End Sub
Private Shared Sub TextBoxControl_TextChanged(sender As Object, e As TextChangedEventArgs)
Dim textBoxControl As TextBox = TryCast(sender, TextBox)
Dim adorner As PlaceholderAdorner = Nothing
If textBoxControl IsNot Nothing AndAlso GetOrCreateAdorner(textBoxControl, adorner) Then
If textBoxControl.Text.Length > 0 Then
'Control has text. Hide the adorner.
adorner.Visibility = Visibility.Hidden
Else
'Control has no text. Show the adorner.
adorner.Visibility = Visibility.Visible
End If
End If
End Sub
Private Shared Function GetOrCreateAdorner(textBoxControl As TextBox, ByRef adorner As PlaceholderAdorner) As Boolean
'Get the adorner layer
Dim layer As AdornerLayer = AdornerLayer.GetAdornerLayer(textBoxControl)
'If nothing, it doesn't exist or the control's template isn't loaded
If layer Is Nothing Then
adorner = Nothing
Return False
End If
'Layer exists, try to find the adorner
adorner = layer.GetAdorners(textBoxControl)?.OfType(Of PlaceholderAdorner)().FirstOrDefault()
'Adorner never added to control, so add it
If adorner Is Nothing Then
adorner = New PlaceholderAdorner(textBoxControl)
layer.Add(adorner)
End If
Return True
End Function
Public Class PlaceholderAdorner
Inherits Adorner
Public Sub New(adornedElement As UIElement)
MyBase.New(adornedElement)
End Sub
Protected Overrides Sub OnRender(drawingContext As DrawingContext)
Dim textBoxControl As TextBox = DirectCast(AdornedElement, TextBox)
Dim placeholderValue As String = TextBoxHelper.GetPlaceholder(textBoxControl)
If String.IsNullOrEmpty(placeholderValue) Then
Return
End If
'Create the formatted text object
Dim text As New FormattedText(
placeholderValue,
System.Globalization.CultureInfo.CurrentCulture,
textBoxControl.FlowDirection,
New Typeface(textBoxControl.FontFamily,
textBoxControl.FontStyle,
textBoxControl.FontWeight,
textBoxControl.FontStretch),
textBoxControl.FontSize,
SystemColors.InactiveCaptionBrush,
VisualTreeHelper.GetDpi(textBoxControl).PixelsPerDip)
text.MaxTextWidth = Math.Max(textBoxControl.ActualWidth - textBoxControl.Padding.Left - textBoxControl.Padding.Right, 10)
text.MaxTextHeight = Math.Max(textBoxControl.ActualHeight, 10)
'Render based on padding of the control, to try and match where the textbox places text
Dim renderingOffset As New Point(textBoxControl.Padding.Left, textBoxControl.Padding.Top)
'Template contains the content part; adjust sizes to try and align the text
Dim part As FrameworkElement = TryCast(textBoxControl.Template.FindName("PART_ContentHost", textBoxControl), FrameworkElement)
If part IsNot Nothing Then
Dim partPosition As Point = part.TransformToAncestor(textBoxControl).Transform(New Point(0, 0))
renderingOffset.X += partPosition.X
renderingOffset.Y += partPosition.Y
text.MaxTextWidth = Math.Max(part.ActualWidth - renderingOffset.X, 10)
text.MaxTextHeight = Math.Max(part.ActualHeight, 10)
End If
' Draw the text
drawingContext.DrawText(text, renderingOffset)
End Sub
End Class
End Class
See also
.NET Desktop feedback