[API Proposal]: Placeholder for TextBox etc.
- Dominant language
- C#
- Stars
- 7.7k
- Forks
- 1.3k
- Avg merge
- 1d 11h
- Merged PRs (30d)
- 61
Description
Based on the discussion in https://github.com/dotnet/wpf/issues/8645 I am suggesting an implementation of placeholders and looking for feedback. This involves 3 new properties:
* object **Placeholder**
* Visibility **PlaceholderVisiblity**
* Style **PlaceholderContainerStyle** (depending on feature requirements)
They increasingly allow different scenarios, see below.
## Code changes
##### Public API surface
```C#
namespace System.Windows.Controls.Primitives;
public abstract class TextBoxBase : Control
{
+ public static readonly DependencyProperty PlaceholderProperty = DependencyProperty.Register(... default: null);
+ public static readonly DependencyProperty PlaceholderVisibilityProperty = DependencyProperty.Register(... default: Collapsed);
+ public static readonly DependencyProperty PlaceholderContainerStyleProperty = DependencyProperty.Register(... default: null);
+ public object Placeholder { ... }
+ public Visibility PlaceholderVisibility { ... }
+ public Style Placeholder PlaceholderContainerStyle { ... }
}
```
The concept of placeholder is shared amongst all `TextBox`-based controls. Whether, how and when a placeholder is displayed depends on each of them. Other controls, such as `PasswordBox` and `ComboBox` would add ownership to the properties above.
Precedence for visibility property: `DataGrid.GridLinesVisibility` / `HeadersVisibility` / `PlaceholderVisibility`, `DataGridRow.DetailsVisibility`, `DataGridRow/ColumnHeader.SeparatorVisibility` etc.
Precedence for style property: `GridView.ColumnHeaderContainerStyle`, `Calendar.CalendarItemStyle` / `CalendarDayButtonStyle` / `CalendarButtonStyle`, `DataGrid.DragIndicatorStyle` / `ColumnHeaderStyle` / `CellStyle` etc.
3rd party controls deriving from `TextBoxBase` will not get affected but could take advantage of the placeholder functionality. If they are based on the `TextBoxBase` template, they just need to set the `PlaceholderVisibility` when appropriate, it will not appear unexpectedly (it is collapsed by default).
##### Theme template
Aero2 example:
```xaml
+
+ <Setter Property="IsHitTestVisible" Value="False" />
+ <Setter Property="TextBlock.Foreground" Value="Gray" />
+ <Setter Property="VerticalAlignment" Value="{Binding VerticalContentAlignment, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay}" />
+ <Setter Property="Margin" Value="2,0" />
+
+ <Setter Property="PlaceholderContainerStyle" Value="{StaticResource DefaultPlaceholderContainerStyle}" />
...
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TextBoxBase}">
<Border x:Name="border"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
SnapsToDevicePixels="True">
+ <Grid>
+ <ContentPresenter
+ ContentSource="Placeholder"
+ Visibility="{TemplateBinding PlaceholderVisibility}"
+ Style="{TemplateBinding PlaceholderContainerStyle}"
+ />
<ScrollViewer x:Name="PART_ContentHost"
Focusable="false"
HorizontalScrollBarVisibility="Hidden"
VerticalScrollBarVisibility="Hidden" />
+ </Grid>
</Border>
...
</ControlTemplate>
</Setter.Value>
</Setter>
...
+ <Style.Triggers>
+ <Trigger Property="Text" Value="">
+ <Setter Property="PlaceholderVisibility" Value="Visible" />
+ </Trigger>
+ </Style.Triggers>
```
* The placeholder's visibility is driven by the `PlaceholderVisibility` property and should not be easily changeable. Overriding placeholder style should not affect this behavior, hence it is set as a local property on the `ContentPresenter`.
* Things we envision developer might want to customize in advanced scenarios are set in the `DefaultPlaceholderContainerStyle`.
* Most importantly this is `IsHitTestVisible`. In most cases this should be set to `false`, so that the placeholder does not affect the `TextBox` user experience (e.g. mouse cursor, accessibility tools, etc.). However if the developer wants the user to interact with the placeholder, the property must be settable to true. Local values have precedence and this property propagates down the tree, so it cannot be set on the `ContentPresenter` directly.
* `VerticalAlignment` should by default match the text alignment inside `TextBox`, but some placeholders might prefer to be at the top or bottom (buttons) or stretched (images) regardless of the text.
* `Margin="2,0"` comes from `TextBoxView.Margin` default metadata. This makes any placeholder text pixel-aligned with the same text content.
This design has a slight disadvantage that when a developer wants to override the style, they would need to provide all the properties that are currently in the `DefaultPlaceholderContainerStyle`. For example, if you just want to set `IsHitTestVisible` to `true`, you also need the style to have the `TextBlock.Foreground` and `VerticalAlignment` setters to provide the default behavior. Given the scenarios where you need to override the styles, you probably don't care about preserving these values anyway.
The `PlaceholderStyleProperty` is only needed if we want to support changing the values set in this style, otherwise it can be just an internal static reference to the theme style. Arguably, being able to control the `IsHitTestVisible` is important for some scenarios.
From UI perspective, this makes the placeholder affect the `TextBox` desired size, but the placeholder does not scroll with the content (normally placeholder is not visible when there is a content).
##### Codebehind
No changes in `TextBox.cs` needed. Other controls might need extra properties, see below.
## Scenarios and examples
1. Typical plain text placeholder
```xaml
```
2. Different text formatting (color, fonts, etc...)
```xaml
```
3. Focus with accelerator:
```xaml
```
4. Make placeholder disappear when control has focus:
```xaml
<Style.Triggers>
<Trigger Property="IsKeyboardFocusWithin" Value="True">
<Setter Property="PlaceholderVisibility" Value="Collapsed" />
</Trigger>
</Style.Triggers>
```
5. Have buttons generate content as a placeholder:
```xaml
<Setter Property="IsHitTestVisible" Value="True" />
<Setter Property="VerticalAlignment" Value="Bottom" />
<Setter Property="Margin" Value="5" />
```
Note that `Style` can replace the control template of the `ContentPresenter` (which can break the `PlaceholderVisibility` bind).
## Other controls
Other controls can use their notion of "no content" to set the placeholder visibility in their templates. That might or might not be trivial, for example `PasswordBox` does not have any DP to that effect, but it has a clear notion (i.e. zero-length password). `RichTextBox` is slightly more complicated, as it can have no paragraphs or an empty one, with or without formatting applied.
Controls can include support for placeholder incrementally.
**PasswordBox** does not derive from `TextBoxBase` at all. It will need to add its own set of `Placeholder*` properties (adding ownership to the proposed ones). Password boxes do have placeholders, for example the one on Windows login screen.
**RichTextBox** derives from `TextBoxBase` and would inherit the proposed properties. In order to utilize the placeholder, both `PasswordBox` and `RichTextBox` (or even `TextBoxBase`) could introduce a helper property such as `IsEmpty`. This would be useful for other purposes as well, such as validation.
**ComboBox** has a `TextBox` if `IsEditable` is set to true. It is notable that the style of the `TextBox` cannot be customized (without changing the template). This suggests that only `Placeholder` property could be added to `ComboBox`.
**DatePickerTextBox** is effectively a `TextBox` with a placeholder (using `internal object Watermark` property, it also selects all on focus). The comments mention it is not public because there is a good chance "`WatermarkTextBox`" control gets implemented. Its existence seems unnecessary once the placeholder feature is implemented. It derives from `TextBox` so it gets all the properties and will need to be migrated to them.
## Alternative designs
##### PlaceholderContainerStyle
I am open to suggestions on how to avoid the `PlaceholderContainerStyle` property. We can say an interactive placeholder is a non-goal (if not forever, at least for the initial release), it is not that complicated to override a `TextBox` template. One way to avoid it would be to have an actual class like `PlaceholderContainer` or `PlaceholderPresenter` that would be styled implicitly and whose style could be overridden by developer by providing their own implicit style. As a bonus, those styles could be `BasedOn` the existing one, so the unchanged values wouldn't need to be repeated. Note that that's how the `ToolTip` property and class basically works.
##### PlaceholderTemplate, PlaceholderTemplateSelector, PlaceholderStringFormat
On the other hand, the `ContentPresenter` allows us to follow full model of similar properties such as `Header` or `Content`: in addition to the container style, which might not be necessary in such case, we could add `PlaceholderTemplate`, `PlaceholderTemplateSelector` and `PlaceholderStringFormat` properties. This seems a bit of an overkill, placeholder is closer to a tooltip than being a fundamental content of a control. The user should still be able to perform their tasks if a placeholder is not shown.
##### Attached properties
The `Placeholder` and its related properties could be attached properties that could be used by any type of element that has a concept of placeholder. This is also similar to how `ToolTip` works. While elements like `TextBox` could expose the attached property directly, it could be applied to other controls independently:
```xaml
```
##### Visual states
Note that the `DatePickerTextBox` that currently implements a placeholder uses visual states:
https://github.com/dotnet/wpf/blob/0fee673e416438fa496be15f9fed2707d9bade23/src/Microsoft.DotNet.Wpf/src/Themes/XAML/DatePicker.xaml#L246-L257
This would allow animated transitions between placeholder shown and hidden states, such as fading in and out the placeholder text.
/cc @dipeshmsft @batzen @singhashish-wpf @lindexi
Contributor guide
Assessment
This issue has not been assessed yet.