0

I have done the following in a new WPF project titled CustomControls.

Step 1: Created a Controls folder. Added a new file titled WatermarkTextBox.cs to it. Added this C# code in the file:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;

namespace Controls
{
    public class WatermarkTextBox : TextBox
    {
        public static readonly DependencyProperty WatermarkProperty = DependencyProperty.Register("Watermark", typeof(String), typeof(WatermarkTextBox), new PropertyMetadata(String.Empty));

        public String Watermark
        {
            get { return (String)GetValue(WatermarkProperty); }
            set { SetValue(WatermarkProperty, value); }
        }
    }
}

Step 2: Within the Controls folder, added a new file titled WatermarkTextBox.xaml. Added this XAML in the file:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:sys="clr-namespace:System;assembly=mscorlib"
                    xmlns:controls="clr-namespace:Controls">
    <Style TargetType="{x:Type controls:WatermarkTextBox}" BasedOn="{StaticResource {x:Type TextBox}}">
        <Style.Resources>
            <VisualBrush x:Key="WatermarkBrush" AlignmentX="Left" AlignmentY="Center" Stretch="None">
                <VisualBrush.Visual>
                    <Label Content="{Binding Watermark}" FontFamily="Segoe UI" FontSize="20" Foreground="LightGray" Padding="5" />
                </VisualBrush.Visual>
            </VisualBrush>
        </Style.Resources>
        <Style.Triggers>
            <Trigger Property="Text" Value="{x:Static sys:String.Empty}">
                <Setter Property="Background" Value="{StaticResource WatermarkBrush}" />
            </Trigger>
            <Trigger Property="Text" Value="{x:Null}">
                <Setter Property="Background" Value="{StaticResource WatermarkBrush}" />
            </Trigger>
            <Trigger Property="IsKeyboardFocused" Value="True">
                <Setter Property="Background" Value="White" />
            </Trigger>
        </Style.Triggers>
    </Style>
</ResourceDictionary>

Step 3: Create a Themes folder. Add a new file titled Generic.xaml to it. Add this XAML in the file:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ResourceDictionary.MergedDictionaries>
    <ResourceDictionary Source="/CustomControls;component/Controls/WatermarkTextBox.xaml" />
</ResourceDictionary.MergedDictionaries>

Step 4: In MainWindow.xaml: Added xmlns:controls="clr-namespace:Controls", and defined a new WatermarkTextBox:

<controls:WatermarkTextBox x:Name="Hostname" Height="40" FontFamily="Segoe UI" FontSize="20" VerticalContentAlignment="Center" Watermark="Hello, world."/>

I see the text box that this custom control is based upon, but I don't see the watermark which I've extended for it. Why is that, and how may I render my watermark? P.S. I should note that the watermark DOES show up if I change {Binding Watermark} in the XAML to a hard-coded string. Also, if I debug my code I see that WatermarkTextBox picks up the correct value...so why is the XAML not displaying it? How can someone even debug this stuff?

Alexandru
  • 12,264
  • 17
  • 113
  • 208

1 Answers1

0

The trick was to use the Windows application titled Blend for Visual Studio to edit the default template of the existing WatermarkTextBox and extend it to introduce a Grid which includes not only the content scroll view but also a new label to house the watermark. In all, I had to make this change to WatermarkTextBox.xaml:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                xmlns:system="clr-namespace:System;assembly=mscorlib"
                xmlns:controls="clr-namespace:Controls">
    <Style TargetType="{x:Type controls:WatermarkTextBox}" BasedOn="{StaticResource {x:Type TextBox}}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="controls:WatermarkTextBox">
                    <Border x:Name="border" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" SnapsToDevicePixels="True">
                        <Grid>
                            <ScrollViewer x:Name="PART_ContentHost" Focusable="False" HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Hidden"/>
                            <Label Cursor="IBeam" Visibility="Hidden" x:Name="WatermarkText" Content="{TemplateBinding Watermark}" FontFamily="Segoe UI" FontSize="20" Foreground="LightGray" Padding="5" />
                        </Grid>
                    </Border>
                    <ControlTemplate.Triggers>
                        <Trigger Property="IsEnabled" Value="False">
                            <Setter Property="Opacity" TargetName="border" Value="0.56"/>
                        </Trigger>
                        <Trigger Property="IsMouseOver" Value="True">
                            <Setter Property="BorderBrush" TargetName="border" Value="#FF7EB4EA"/>
                        </Trigger>
                        <Trigger Property="IsKeyboardFocused" Value="True">
                            <Setter Property="BorderBrush" TargetName="border" Value="#FF569DE5"/>
                        </Trigger>
                        <Trigger Property="Text" Value="{x:Static system:String.Empty}">
                            <Setter Property="Visibility" Value="Visible" TargetName="WatermarkText" />
                        </Trigger>
                        <Trigger Property="Text" Value="{x:Null}">
                            <Setter Property="Visibility" Value="Visible" TargetName="WatermarkText" />
                        </Trigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

Edit: If you're thinking about doing this in production, think again. I thought to myself, "Oh, it would be so nice to extend every control I need to provide functionality like this." Well unfortunately some control classes such as the PasswordBox are sealed, making this a moot point in those situations. In fact it would just be easier to programmatically show and hide a label in the same location as your TextBox, as most hacks or extensions do just that.

Alexandru
  • 12,264
  • 17
  • 113
  • 208
  • 1
    The main problem why your original implementation failed is that, binding done within resources tend to not work. Moving that to a template would work though, like what you did here. – Jai Jun 22 '16 at 02:40
  • Yeah, its unfortunate. Ultimately its pretty clear that WPF is a mess when it comes to extensibility, but at least the technology lives up to providing an extremely rich user experience with a simple but strictly followed programming model, and for that I do love it. – Alexandru Jun 22 '16 at 03:46