Unless you have been living under a rock the past few days you already know about the recent release of Windows 8 Consumer Preview and Visual Studio 11 Beta.  Since I know you already downloaded and installed all the shiny new toys, let’s take a quick look at how to get started writing a custom control for the new metro style applications.  There is no better control to start with than the ever popular WatermarkTextBox control.

Setting up the Solution

First start off by creating a new metro style application in Visual Studio 11 beta.  A blank application will work just fine for our purposes.

image

When your solution loads right click the project and select" “Add New Item”.  When the dialog appears choose the “Template Control” item template.  Of course give it a name of WatermarkTextBox.

image

You will notice that the Template control is just like a Silverlight or WPF custom control.  It comes with a class file and a corresponding style in the Generic.xaml file.

image

Writing the Control

Our WatermarkTextBox control is going to derive from the TextBox class that is already provided by Microsoft.

public sealed class WatermarkTextBox : TextBox
{
    public WatermarkTextBox()
    {
        this.DefaultStyleKey = typeof(WatermarkTextBox);
    }
}

Having said that, we don’t want to reinvent the wheel when it comes to styling the TextBox we are deriving from.  So let’s save some time by dropping a TextBox onto the BlankPage.xaml page.  Now right click the TextBox on the design surface and select “Edit Template –> Edit a Copy”.  This will create the default TextBlock style that we can use in our WatermarkTextBox ControlTemplate.

image

Now open up the Generic.xaml file and replace this:

<Style TargetType="local:WatermarkTextBox">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="local:WatermarkTextBox">
                <Border
                    Background="{TemplateBinding Background}"
                    BorderBrush="{TemplateBinding BorderBrush}"
                    BorderThickness="{TemplateBinding BorderThickness}">
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

With this:

<Style TargetType="local:WatermarkTextBox">
    <Setter Property="MinWidth" Value="{StaticResource TextBoxMinWidth}"/>
    <Setter Property="MinHeight" Value="{StaticResource TextBoxMinHeight}"/>
    <Setter Property="Foreground" Value="{StaticResource TextBoxTextBrush}"/>
    <Setter Property="Background" Value="{StaticResource TextBoxFillBrush}"/>
    <Setter Property="BorderBrush" Value="{StaticResource TextBoxBorderBrush}"/>
    <Setter Property="BorderThickness" Value="{StaticResource InputControlBorderThickness}"/>
    <Setter Property="FontFamily" Value="{StaticResource ContentFontFamily}"/>
    <Setter Property="FontSize" Value="{StaticResource ContentFontSize}"/>
    <Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Hidden"/>
    <Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Hidden"/>
    <Setter Property="ScrollViewer.ZoomMode" Value="Disabled"/>
    <Setter Property="Padding" Value="{StaticResource TextBoxPaddingThickness}"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="local:WatermarkTextBox">
                <Grid>
                    <Grid.Resources>
                        <Style x:Name="DeleteButtonStyle" TargetType="Button">
                            <Setter Property="Template">
                                <Setter.Value>
                                    <ControlTemplate TargetType="Button">
                                        <Grid>
                                            <VisualStateManager.VisualStateGroups>
                                                <VisualStateGroup x:Name="CommonStates">
                                                    <VisualState x:Name="Normal"/>
                                                    <VisualState x:Name="PointerOver">
                                                        <Storyboard>
                                                            <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Background" Storyboard.TargetName="BackgroundElement">
                                                                <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource TextBoxButtonHoverFillBrush}"/>
                                                            </ObjectAnimationUsingKeyFrames>
                                                            <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="BorderBrush" Storyboard.TargetName="BorderElement">
                                                                <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource TextBoxButtonHoverBorderBrush}"/>
                                                            </ObjectAnimationUsingKeyFrames>
                                                            <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Foreground" Storyboard.TargetName="GlyphElement">
                                                                <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource TextBoxButtonHoverGlyphBrush}"/>
                                                            </ObjectAnimationUsingKeyFrames>
                                                        </Storyboard>
                                                    </VisualState>
                                                    <VisualState x:Name="Pressed">
                                                        <Storyboard>
                                                            <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Background" Storyboard.TargetName="BackgroundElement">
                                                                <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource TextBoxButtonPressedFillBrush}"/>
                                                            </ObjectAnimationUsingKeyFrames>
                                                            <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="BorderBrush" Storyboard.TargetName="BorderElement">
                                                                <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource TextBoxButtonPressedBorderBrush}"/>
                                                            </ObjectAnimationUsingKeyFrames>
                                                            <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Foreground" Storyboard.TargetName="GlyphElement">
                                                                <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource TextBoxButtonPressedGlyphBrush}"/>
                                                            </ObjectAnimationUsingKeyFrames>
                                                        </Storyboard>
                                                    </VisualState>
                                                    <VisualState x:Name="Disabled">
                                                        <Storyboard>
                                                            <DoubleAnimation Duration="0" To="0" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="BackgroundElement"/>
                                                            <DoubleAnimation Duration="0" To="0" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="BorderElement"/>
                                                        </Storyboard>
                                                    </VisualState>
                                                </VisualStateGroup>
                                            </VisualStateManager.VisualStateGroups>
                                            <Border x:Name="BorderElement" BorderBrush="{StaticResource TextBoxButtonBorderBrush}" BorderThickness="{TemplateBinding BorderThickness}"/>
                                            <Border x:Name="BackgroundElement" Background="{StaticResource TextBoxButtonFillBrush}" Margin="{TemplateBinding BorderThickness}">
                                                <TextBlock x:Name="GlyphElement" Foreground="{StaticResource TextBoxButtonGlyphBrush}" FontFamily="Segoe UI Symbol" HorizontalAlignment="Center" Text="X" VerticalAlignment="Center"/>
                                            </Border>
                                        </Grid>
                                    </ControlTemplate>
                                </Setter.Value>
                            </Setter>
                        </Style>
                    </Grid.Resources>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="*"/>
                        <ColumnDefinition Width="Auto"/>
                    </Grid.ColumnDefinitions>
                    <VisualStateManager.VisualStateGroups>
                        <VisualStateGroup x:Name="CommonStates">
                            <VisualState x:Name="Disabled">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Background" Storyboard.TargetName="BackgroundElement">
                                        <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource TextBoxDisabledFillBrush}"/>
                                    </ObjectAnimationUsingKeyFrames>
                                    <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="BorderBrush" Storyboard.TargetName="BorderElement">
                                        <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource TextBoxDisabledBorderBrush}"/>
                                    </ObjectAnimationUsingKeyFrames>
                                    <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Foreground" Storyboard.TargetName="ContentElement">
                                        <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource TextBoxDisabledTextBrush}"/>
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </VisualState>
                            <VisualState x:Name="Normal">
                                <Storyboard>
                                    <DoubleAnimation Duration="0" To="{StaticResource TextBoxRestFillOpacity}" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="BackgroundElement"/>
                                    <DoubleAnimation Duration="0" To="{StaticResource TextBoxRestBorderOpacity}" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="BorderElement"/>
                                </Storyboard>
                            </VisualState>
                            <VisualState x:Name="PointerOver">
                                <Storyboard>
                                    <DoubleAnimation Duration="0" To="{StaticResource TextBoxHoverFillOpacity}" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="BackgroundElement"/>
                                    <DoubleAnimation Duration="0" To="{StaticResource TextBoxHoverBorderOpacity}" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="BorderElement"/>
                                </Storyboard>
                            </VisualState>
                            <VisualState x:Name="Focused"/>
                        </VisualStateGroup>
                        <VisualStateGroup x:Name="ButtonStates">
                            <VisualState x:Name="ButtonVisible">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="DeleteButton">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Visible</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </VisualState>
                            <VisualState x:Name="ButtonCollapsed"/>
                        </VisualStateGroup>
                    </VisualStateManager.VisualStateGroups>
                    <Border x:Name="BackgroundElement" Background="{TemplateBinding Background}" Grid.ColumnSpan="2" Margin="{TemplateBinding BorderThickness}"/>
                    <Border x:Name="BorderElement" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Grid.ColumnSpan="2"/>
                    <ScrollViewer x:Name="ContentElement" HorizontalScrollMode="{TemplateBinding ScrollViewer.HorizontalScrollMode}" HorizontalScrollBarVisibility="{TemplateBinding ScrollViewer.HorizontalScrollBarVisibility}" IsTabStop="False" Margin="{TemplateBinding BorderThickness}" Padding="{TemplateBinding Padding}" VerticalScrollBarVisibility="{TemplateBinding ScrollViewer.VerticalScrollBarVisibility}" VerticalScrollMode="{TemplateBinding ScrollViewer.VerticalScrollMode}" ZoomMode="{TemplateBinding ScrollViewer.ZoomMode}"/>
                    <Button x:Name="DeleteButton" BorderThickness="{TemplateBinding BorderThickness}" Grid.Column="1" FontSize="{TemplateBinding FontSize}" IsTabStop="False" Style="{StaticResource DeleteButtonStyle}" Visibility="Collapsed" VerticalAlignment="Stretch"/>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

Let’s test what we have so far.  Add an instance of our WatermarkTextBox control to the BlankPage.xaml.

<Page
    x:Class="WatermarkTextBoxControl.BlankPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:WatermarkTextBoxControl"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <StackPanel Background="{StaticResource ApplicationPageBackgroundBrush}">
        <local:WatermarkTextBox />
    </StackPanel>
</Page>

image

Looks great.  Just like the default Windows TextBox.  Now we need to start adding our Watermark elements to it.  Obviously we need a property for our Watermark.  Your first thought maybe to define it as a type of string, but I want to support any element, not just text, so I will define it as a type of Object.  This will allow me to nest controls inside of the Watermark property instead of being restricted to a simple string.

public static DependencyProperty WatermarkProperty = DependencyProperty.Register("Watermark", typeof(object), typeof(WatermarkTextBox), new PropertyMetadata(null));
public object Watermark
{
    get { return (object)GetValue(WatermarkProperty); }
    set { SetValue(WatermarkProperty, value); }
}

I also want to define a DataTemplate that will be used to define the ContentTemplate for the Watermark.

public static DependencyProperty WatermarkTemplateProperty = DependencyProperty.Register("WatermarkTemplate", typeof(DataTemplate), typeof(WatermarkTextBox), new PropertyMetadata(null));
public DataTemplate WatermarkTemplate
{
    get { return (DataTemplate)GetValue(WatermarkTemplateProperty); }
    set { SetValue(WatermarkTemplateProperty, value); }
}

Now we need to modify our control template to support our new Watermark property. We will do this by simply adding a ContentPresenter to our Template.

<Border x:Name="BackgroundElement" Background="{TemplateBinding Background}" Grid.ColumnSpan="2" Margin="{TemplateBinding BorderThickness}"/>
<Border x:Name="BorderElement" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Grid.ColumnSpan="2"/>
<ScrollViewer x:Name="ContentElement" HorizontalScrollMode="{TemplateBinding ScrollViewer.HorizontalScrollMode}" HorizontalScrollBarVisibility="{TemplateBinding ScrollViewer.HorizontalScrollBarVisibility}" IsTabStop="False" Margin="{TemplateBinding BorderThickness}" Padding="{TemplateBinding Padding}" VerticalScrollBarVisibility="{TemplateBinding ScrollViewer.VerticalScrollBarVisibility}" VerticalScrollMode="{TemplateBinding ScrollViewer.VerticalScrollMode}" ZoomMode="{TemplateBinding ScrollViewer.ZoomMode}"/>
<ContentPresenter x:Name="PART_Watermark"
                  Content="{TemplateBinding Watermark}"
                                                           ContentTemplate="{TemplateBinding WatermarkTemplate}"
                  IsHitTestVisible="False"
                  Margin="{TemplateBinding Padding}"
                  Visibility="Collapsed"/>
<Button x:Name="DeleteButton" BorderThickness="{TemplateBinding BorderThickness}" Grid.Column="1" FontSize="{TemplateBinding FontSize}" IsTabStop="False" Style="{StaticResource DeleteButtonStyle}" Visibility="Collapsed" VerticalAlignment="Stretch"/>

Oh, and don’t forget the ContentTemplate that will define the default look for our watermark.  This is placed at the top of the Generic.xaml.

<DataTemplate x:Key="DefaultWatermarkTemplate">
    <ContentControl Content="{Binding}" Foreground="Gray" IsTabStop="False" />
</DataTemplate>

And we need to set the default value in a style setter.

<Setter Property="WatermarkTemplate" Value="{StaticResource DefaultWatermarkTemplate}" />

As you can see, I placed the ComtentPresenter after the ContentElement and before the DeleteButton elements.  You may have also noticed that I set the visibility to Collapsed.  This is because by default I want the Watermark hidden.  So that means we need to add some code to show the watermark when the text box has no focus and it has no text.  First we need to add some visual states to the VisualStateManager.

<VisualStateGroup x:Name="WatermarkStates">
    <VisualState x:Name="WatermarkVisible">
        <Storyboard>
            <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="PART_Watermark">
                <DiscreteObjectKeyFrame KeyTime="0">
                    <DiscreteObjectKeyFrame.Value>
                        <Visibility>Visible</Visibility>
                    </DiscreteObjectKeyFrame.Value>
                </DiscreteObjectKeyFrame>
            </ObjectAnimationUsingKeyFrames>
        </Storyboard>
    </VisualState>
    <VisualState x:Name="WatermarkCollapsed" />
</VisualStateGroup>

Next we need to add some code to handle the GotFocus and LostFocus events of the TextBox.  Start by adding event handlers for the events in the constructor.

public WatermarkTextBox()
{
    this.DefaultStyleKey = typeof(WatermarkTextBox);
    this.GotFocus += WatermarkTextBox_GotFocus;
    this.LostFocus += WatermarkTextBox_LostFocus;
}

Now add the code that does the heavy lifting.

void WatermarkTextBox_GotFocus(object sender, RoutedEventArgs e)
{
    GoToWatermarkVisualState();
}

void WatermarkTextBox_LostFocus(object sender, RoutedEventArgs e)
{
    GoToWatermarkVisualState(false);
}

private void GoToWatermarkVisualState(bool hasFocus = true)
{
    //if our text is empty and our control doesn't have focus then show the watermark
    //otherwise the control eirther has text or has focus which in either case we need to hide the watermark
    if (String.IsNullOrEmpty(Text) && !hasFocus)
        GoToVisualState("WatermarkVisible"); //TODO: create constants for our magic strings
    else
        GoToVisualState("WatermarkCollapsed");
}

private void GoToVisualState(string stateName, bool useTransitions = true)
{
    VisualStateManager.GoToState(this, stateName, useTransitions);
}

One last thing is to make sure we set the visual state when the template is applied.  So override the OnApplyTemplate method as follows.

protected override void OnApplyTemplate()
{
    base.OnApplyTemplate();

    //we need to set the initial state of the watermark
    GoToWatermarkVisualState(false);
}

That should do it.  Now let check it out in action. Be sure to provide a watermark for our control.

<StackPanel Background="{StaticResource ApplicationPageBackgroundBrush}">
    <local:WatermarkTextBox Watermark="Edit Text" />
</StackPanel>

image

image

Yes, we found a bug

Everything seems to be working perfectly.  Until you actually try to use a watermark that isn’t text.  Try using this as your watermark.

<local:WatermarkTextBox>
    <local:WatermarkTextBox.Watermark>
        <StackPanel Orientation="Horizontal">
            <Image Source="Images/PencilTool16.png" Stretch="None" />
            <TextBlock Text="Edit Text" Margin="4,0,0,0" />
        </StackPanel>
    </local:WatermarkTextBox.Watermark>
</local:WatermarkTextBox>

Now let’s see our results.

image

Hey!  Where is my watermark?  Well there appears to be a bug with a ContentPresenter/ContentControl that will not display content when the ContentTemplate has been set and the Content is anything else except a string.  This just so happens to be the exact same bug that exists in Silverlight.  Interesting I know.  So how do we fix it?  We have to delete the usage of our Watermarktemplate until this gets fixed.

Change:

<ContentPresenter x:Name="PART_Watermark"
                  Content="{TemplateBinding Watermark}"
                                                           ContentTemplate="{TemplateBinding WatermarkTemplate}"
                  IsHitTestVisible="False"
                  Margin="{TemplateBinding Padding}"
                  Visibility="Collapsed"/>

To:

<ContentPresenter x:Name="PART_Watermark"
                                                        Content="{TemplateBinding Watermark}"
                  IsHitTestVisible="False"
                  Margin="{TemplateBinding Padding}"
                  Visibility="Collapsed"/>

Now let’s see what happens.

image

Now that’s much better.  Let’s hope this is just a beta bug.

Download the source

Brian Lagunas

View all posts

Follow Me

Follow me on Twitter, subscribe to my YouTube channel, and watch me stream live on Twitch.