Basics of extending a WPF control

Occasionally we need to extend an existing WPF control. We might apply a different style to the control, or applying a new control template both using XAML. Or maybe this isn’t sufficient and we need to add functionality to the control itself using code.

In this post we’ll not be touching the style, but we will be looking at extending an existing control’s functionality and changing the control template to use the new functionality. This result is not mean’t to be a production ready control (although hopefully it will be) but is more aimed at the steps required to create our new control etc.

Through this post we’ll create a simple watermark text box. In other word a text box which displays text, then when the control gets focus the text will disappear and when the control loses focus and only if no text has been typed in, the watermark will reappear.

The steps….

  1. Create a new control and derive from the WPF TextBox, create an initial style for the control and expose this to allows us to reference and use the control elsewhere.
  2. Add our new properties, such as a string property for the Watermark text and a flag to state whether text exists in the TextBox
  3. Create the new control template to work with the new control properies

Step 1

We’ve already decided that we want to simply added some functionality to an existing TextBox, so first we create a new class (in the file named WatermarkTextBox,cs) and derive it from TextBox.

public class WatermarkTextBox : TextBox
{
   static WatermarkTextBox()
   {
      DefaultStyleKeyProperty.OverrideMetadata(typeof(WatermarkTextBox), 
             new FrameworkPropertyMetadata(typeof(WatermarkTextBox)));
   }
}

The important addition here is the static constructor and the DefaultStyleKeyProperty.OverrideMetadata. Without this the WatermarkTextBox will get the default theme for the TextBox, but we know we’re going to need to change this, so this line sets the default style to one with a target type of WatermarkTextBox.

If you ran code with this class as it is, you’d seen no output as we’ve not defined the default style yet. If you comment out the line in DefaultStyleKeyProperty.OverrideMetadata you’ll obviously see the default style for a TextBox.

So to complete step 1. We need to create a .xaml file (named WatermarkTextBox.xaml), so in VS2012 add a new Resource Dictionary and then add the following code to it

<Style TargetType="Controls:WatermarkTextBox" BasedOn="{StaticResource {x:Type TextBox}}">
</Style>

You’ll obviously need to add the namespace, which I’ve named as Controls.

I’ve in essence defined a style for this control which obviously adds nothing and thus looks like a TextBox’s default style. But we’ll flesh this out later. For now this will display nothing unless we create a Generic.xaml file.

Themes\Generic.xaml

By default WPF expects any generic styles etc. to be stored in a folder named Themes off of the project. Here we create another Resource Dictionary file name Generic.xaml. To this we add the following code

<ResourceDictionary.MergedDictionaries>
   <ResourceDictionary Source="/SimpleControl;component/WatermarkTextBox.xaml" />
</ResourceDictionary.MergedDictionaries>

We’re telling WPF to merge the WatermarkTextBox.xaml file into the Themes\Generic.xaml Resource Dictionary.

Step 2

Step 1 was basically about getting the plumbing in place to allow us to work with our new control, so let’s now added some new functionality to the WatermarkTextBox. This step will write all the code in the .cs file so go to that file and type dpp and tab (twice) within the class to use the DependencyProperty code snippet that ships with VS2012. Select 0 — Dependency Property — default value.

Give the property the name Watermark. This will be the text displayed as the watermark. VS should fill in the name and create the snippet. We need to change the text new FrameworkPropertyMetadata((bool)false) to new FrameworkPropertyMetadata(String.Empty) so that this property is a string and by default displays an empty string, i.e. nothing. Also we need to change the from bool to string elsewhere.

So the code should look like this (comments and regions removed)

public static readonly DependencyProperty WatermarkProperty =
         DependencyProperty.Register("Watermark", typeof(string), 
                      typeof(WatermarkTextBox),
		      new FrameworkPropertyMetadata(String.Empty));

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

Now we want the watermark to disappear when the control gains focus which is easy enough, but we also want it so that when the control loses focus the watermark is redisplayed but only if no text exists. So we need a property to tell us whether text exists. It’s not something that can be altered outside of the class so under the Dependency Property we just added (and within the class) type dp and tab twice selecting the Read-Only Dependency Property — default value option. To added a read only dependency property.

Give the name RemoveWatermark and let the snippet fill in the rest. This code snippet added a SetRemoveWatermark method, we don’t need this as the value is going to be determined by whether there’s text in the TextBox and therefore cannot be set directly. The code added should therefore look like this (comments and regions removed).

private static readonly DependencyPropertyKey RemoveWatermarkPropertyKey = 
            DependencyProperty.RegisterReadOnly("RemoveWatermark", typeof(bool), 
                typeof(WatermarkTextBox),
                new FrameworkPropertyMetadata((bool)false));

public static readonly DependencyProperty RemoveWatermarkProperty =                 
            RemoveWatermarkPropertyKey.DependencyProperty;

public bool RemoveWatermark
{
   get { return (bool)GetValue(RemoveWatermarkProperty); }
}

Finally for the code we need to put the code in place to update RemoveWatermark. So we’ve already mentioned that this code will depend on there being text in the TextBox. So naturally we’ll need to override the TextPropertyChanged event. To do this we need to add the following code to the static constructor

TextProperty.OverrideMetadata(typeof(WatermarkTextBox),
	new FrameworkPropertyMetadata(new PropertyChangedCallback(TextPropertyChanged)));

This tells TextBox TextProperty to call our TextPropertyChanged event for WatermarkTextBox types. So now let’s add the TextPropertyChanged code

static void TextPropertyChanged(DependencyObject sender, 
                  DependencyPropertyChangedEventArgs args)
{
   WatermarkTextBox watermarkTextBox = (WatermarkTextBox)sender;

   bool textExists = watermarkTextBox.Text.Length > 0;
   if (textExists != watermarkTextBox.RemoveWatermark)
   {
      watermarkTextBox.SetValue(RemoveWatermarkPropertyKey, textExists);
   }
}

This is simple enough. The sender should always be of type WatermarkTextBox so we simply cast it. We then check whether the Text.Length is greater than zero to see whether text exists. If the RemoveWatermark property differs from the new value we set the value on the RemoveWatermarkPropertyKey.

If we run a test app with the WatermarkTextBox it will still look like a TextBox but now has extra properties so if you’re working through this example go to your test app and add a Watermark string to your WatermarkTextBox ready for the next step. So it looks something like this

<Controls:WatermarkTextBox Watermark="Search" />

Step 3

We now need to fill in the control’s style. Open the WatermarkTextBox.xaml file and insert the following code into the Style created previously

<Setter Property="Template">
   <Setter.Value>
      <ControlTemplate TargetType="Controls:WatermarkTextBox">
                    
         <Border BorderThickness="{Binding Path=BorderThickness, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay}" 
                 BorderBrush="{Binding Path=BorderBrush, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay}">
            <Grid>
               <ScrollViewer x:Name="PART_ContentHost" Margin="3"/>
                  <TextBlock x:Name="watermarkText" 
                        Text="{TemplateBinding Watermark}" 
                        FontStyle="Italic" 
                        VerticalAlignment="Center"
			Margin="5,0,0,0" 
                        FontWeight="Bold" 
                        Foreground="Gray"/>
             </Grid>
          </Border>
                    
         <ControlTemplate.Triggers>
            <MultiTrigger>
               <MultiTrigger.Conditions>
                  <Condition Property="IsFocused" Value="True"/>
               </MultiTrigger.Conditions>
               <Setter Property="Visibility" Value="Collapsed" TargetName="watermarkText" />
            </MultiTrigger>

            <MultiTrigger>
               <MultiTrigger.Conditions>
                  <Condition Property="RemoveWatermark" Value="True"/>
                  <Condition Property="IsFocused" Value="False"/>
               </MultiTrigger.Conditions>
               <Setter Property="Visibility" Value="Collapsed" TargetName="watermarkText" />
            </MultiTrigger>
         </ControlTemplate.Triggers>
      </ControlTemplate>
   </Setter.Value>
</Setter>

There’s a lot to take in there, but basically we’re setting the Template for the WatermarkTextBox. The first part is the ControlTemplate whereby we replace the TextBox’s default look with out own, adding a TextBlock which will display our Watermark text.

Note: The Textbox’s actually Template is far larger than the one shown above but basically we’re only really interested in the PART_ContentHost for this sample. But feel free to use Blend or whatever you prefer to edit the whole template if you wish.

As originally decided, we need this text to disappear when the control gets focus and reappear if the control loses focus AND there’s no text in the text box. So we create the two triggers.

The fist checks the IsFocused property and if it’s true collapses the water mark text. The second trigger checks whether we need to remove the watermark AND the IsFocused is False. If it is then the water mark is collapsed.

And that’s it, we’ve created a simple control, added properties, created the default style for the control and made it available to other code (outside it’s assembly).