Introduction
This article is to show how control templates can be used to add an additional behaviour to textbox and convert the look and feel into a picker. We will define a new picker based style for a textbox control and apply it based on the picker type that we need. This helps us in extending our current design with ease and flexibility without making too much of change. Field control will appear as a textbox and will show the picker type based on focus or mouseover on textbox - this helps in maintaining the old layout for existing control and save real estate for new ones.
Background
In one of the product that I was working on, we had a defined formatting for the textbox but not a date picker feature for a given date field. So, this year, we had this requirement of showing calendar icon for the date fields in our WPF app. Fields are defined at runtime based on certain logic and we draw the needed controls for those fields dynamically. Lets say, for a text type field, we have a textbox painter that would create a textbox of defined dimension that we add to the view.
While working on the date-picker feature, we observed few things in our implementation:
-
Textboxes were used for taking date input, thus Textbox Painter created textboxes for the date fields when needed
-
Formatting for date field was applied via a formatter on the textbox at runtime as per business logic
-
There were number of screens (with a defined layout) that had date field
Now, based on above observation (and yes, it is a large codebase!), we discussed the possible options to have the new feature of datepicker. Since, the field was on multiple screen, it was clear that we need to make change at one place/time that would reflect it automatically at all the places. We could have created a new CalendarPainter kind of class to handle it, but we wanted to do minimum change possible. So, we decided to add a new behaviour to textbox to achieve it - it had quite a few advantages over other options:
-
Quality Analysis time would be limited to verification of new behaviour with a smoke test of existing formatting being maintained
-
No layout change would be needed for any screen and existing control will handle the new responsibility
-
No change at the core level (by creating calendarpainter) can be avoided giving confidence to everyone from developer to manager
-
XAML based template driven change with minimum code behind change will be easy, flexible and maintainable
So, we extended the existing textbox with an additional behaviour that was template driven. This article will share this extension implementation.
Using the code
Following are the various steps and directions that I followed and moved ahead in order to obtain the functionality I was aiming for.
Exploring the internal XAML of a standard textbox
My first hurdle cum learning was how a textbox can be extended. Based on little research and reading, I found how a standard textbox is presented in XAML internally. This gave a clue on how we can extend it. The details were not well documented but with bits and pieces shared on web and small quick experiments, made it clear on how to use the internal control template of a textbox.
A ScrollViewer
control named as PART_ContentHost
was a good discovery for me that constitutes a major part of TextBox. TextBox
uses it to allow the use of vertical and horizontal scroll bars incase of a multiple line textbox. I found that the naming convention was fixed and we have to follow the same - looks like the original internal template expects this name and we have to follow it to keep things working. Overall, the default presentation of TextBox
is shown below:
<Style x:Key="CustomTextBoxStyle" TargetType="{x:Type TextBox}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TextBox}">
<Border BorderThickness="1" BorderBrush="Black">
<ScrollViewer x:Name="PART_ContentHost" />
<!---->
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Defining a ToggleButton for opening-closing of the picker dropdown popup
In order to have a picker, we would need a ToggleButton
to switch on and off. Apart from OnClick
, this toggle button will be triggered by various events like OnFocus
, OnMouseLeave
, etc based on the need. Since, it's going to be a part of TextBox control template, we define it's content with an image that will be used as a visual presentation to user for toggle.
Here is the default presentation of a ToggleButton
that we will use in our custom textbox template:
<ControlTemplate x:Key="IconButton" TargetType="{x:Type ToggleButton}">
<Border>
<ContentPresenter />
</Border>
</ControlTemplate>
<ToggleButton Template="{StaticResource IconButton}"
MaxHeight="21"
Margin="-1,0,0,0"
Name="PopUpImageButton"
Focusable="False"
IsChecked="False">
<Image Source="Images\Expand_Collapse_Icon.png" Stretch="None" Visibility="Hidden" HorizontalAlignment="Right" Name="imgPicker" >
</Image>
</ToggleButton>
Defining triggers for toggle button
As I mentioned earlier, there can be various Triggers
for ToggleButton
. By default, for a picker behaviour, most of us would prefer to have MouseOver
and Focus
event defined as Triggers
.
In our example here, just above in ToggleButton
presenter, we have set the Visibility
of the image as Hidden by default. Now, via various Trigger
, we will make the ToggleButton
visible - this keeps the look and feel of the UI/view clean and shows the extra/additional behavior only when user triggers the defined events. This saves us the real estate issue, allowing to fit the control in the same space as a normal textbox would.
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="true">
<Setter Property="Visibility" TargetName="imgPicker" Value="Visible" />
</Trigger>
<Trigger Property="IsFocused" Value="true">
<Setter Property="Visibility" TargetName="imgPicker" Value="Visible" />
</Trigger>
</ControlTemplate.Triggers>
Popup of Calendar
Following is the picker popup sample design used for having date picker. Standard WPF Calendar control is used. SelectedDate
and DisplayDate
of Calendar control are tied to the main Textbox via CalendarConverter
(Sample is covered in detail in the later half of this article). Here, SelectedDatesChanged
event has been added as an additional Trigger
to on-off the ToggleButton
switch.
<Calendar Margin="0,-1,0,0" x:Name="CalDisplay"
SelectedDate="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=Text, Mode=TwoWay, Converter={StaticResource calendarConverter}}"
Focusable="False"
DisplayDate="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=Text, Mode=OneWay, Converter={StaticResource calendarConverter}}" >
<Control.Triggers>
<EventTrigger RoutedEvent="Calendar.SelectedDatesChanged">
<BeginStoryboard>
<Storyboard>
<BooleanAnimationUsingKeyFrames Storyboard.TargetName="PopUpImageButton" Storyboard.TargetProperty="IsChecked">
<DiscreteBooleanKeyFrame KeyTime="00:00:00" Value="False"></DiscreteBooleanKeyFrame>
</BooleanAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Control.Triggers>
</Calendar>
Popup of Calculator
Following is the picker popup sample design used for having calculator picker. WPF Toolkit based, Calculator control is used (Toolkit assemblies are attached in the sample download). DisplayText
of the calculator control is tied to the main Textbox via DoubleStringConverter
. Here, LostFocus
event has been added as an additional Trigger
to on-off the ToggleButton
switch.
<extToolkit:Calculator Margin="0,0,0,0" x:Name="CalDisplay"
DisplayText="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=Text, Mode=TwoWay, Converter={StaticResource doubleStringConverter}}"
Focusable="False" Precision="2">
<Control.Triggers>
<EventTrigger RoutedEvent="extToolkit:Calculator.LostFocus">
<BeginStoryboard>
<Storyboard>
<BooleanAnimationUsingKeyFrames Storyboard.TargetName="PopUpImageButton" Storyboard.TargetProperty="IsChecked">
<DiscreteBooleanKeyFrame KeyTime="00:00:00" Value="False"></DiscreteBooleanKeyFrame>
</BooleanAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
<EventTrigger RoutedEvent="extToolkit:Calculator.MouseLeave">
<BeginStoryboard>
<Storyboard>
<BooleanAnimationUsingKeyFrames Storyboard.TargetName="PopUpImageButton" Storyboard.TargetProperty="IsChecked">
<DiscreteBooleanKeyFrame KeyTime="00:00:00" Value="False"></DiscreteBooleanKeyFrame>
</BooleanAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Control.Triggers>
</extToolkit:Calculator>
Popup of ListView
Following is the picker popup sample design used for having a list picker. Standard WPF ListView control is used. SelectedItem of ListView control is tied to the main Textbox via ListboxConverter
. Here, an additional close button was added to the view. This Button Click
event has been added as an additional Trigger
to on-off the ToggleButton
switch.
<Grid>
<ListView Name="ListView1" HorizontalAlignment="Left" VerticalAlignment="Top" Width="170" Height="85"
SelectedItem="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=Text, Mode=TwoWay, Converter={StaticResource listboxConveter}}">
<ListViewItem Content="One"></ListViewItem>
<ListViewItem Content="Two"></ListViewItem>
<ListViewItem Content="Three"></ListViewItem>
<ListViewItem Content="Four"></ListViewItem>
<ListViewItem Content="Five"></ListViewItem>
</ListView>
<Button Width="20" Height="20" HorizontalAlignment="Right" VerticalAlignment="Bottom" Name="btn">X
<Control.Triggers>
<EventTrigger RoutedEvent="Button.Click">
<BeginStoryboard>
<Storyboard>
<BooleanAnimationUsingKeyFrames Storyboard.TargetName="PopUpImageButton" Storyboard.TargetProperty="IsChecked">
<DiscreteBooleanKeyFrame KeyTime="00:00:00" Value="False"></DiscreteBooleanKeyFrame>
</BooleanAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Control.Triggers>
</Button>
</Grid>
Defining Converters for Date, String, etc
Based on our need, either we can use exisiting standard WPF converters or have our own custom converters. Converters are classes that help in binding two properties that are of incompatible types - they convert value from source to target and back. These classes are called ValueConverter
that implements a simple interface IValueConverter
with the two methods object Convert(object value, Type targetType, object parameter, CultureInfo culture)
and object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
.
To use a converter in XAML, we add an instance of it to the resources and reference it by using a Key. Following is one of the sample converter code snippet: CalendarConverter
[ValueConversion(typeof(DateTime), typeof(string))]
public class CalendarConverter : IValueConverter
{
#region IValueConverter Members
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
DateTime date = (DateTime)value;
string param = parameter as string;
return string.IsNullOrEmpty(param) ? date.ToShortDateString() : date.ToString(param);
}
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
string dateString = (string)value;
DateTime resultDateTime;
if (DateTime.TryParse(dateString, out resultDateTime))
return resultDateTime;
return DateTime.Now;
}
#endregion
}
Apply new behaviour
Now, once we have the XAML style with all the bindings and triggers in place, all we need to do is, simply apply the new control style at runtime:
TextBox tb = new TextBox();
Style sCalendar = (Style)tb.TryFindResource("tbCalendarStyle");
if (sCalendar != null)
textBox1.Style = sCalendar;
Style sCalculator = (Style)tb.TryFindResource("tbCalculatorStyle");
if (sCalculator != null)
textBox2.Style = sCalculator;
Style sList = (Style)tb.TryFindResource("tbListStyle");
if (sList != null)
textBox3.Style = sList;
New behaviours as defined in the control template will be applied to the textboxes and one can use them as picker now.
Currently, formats (like 'mm/dd/yyyy', '$') specified next to labels of textbox are just for indicating the datatype expected in the textboxes. Currently, they are not enforced on textboxes if someone tries to enter anything else. This article is to show how to extend a textbox as a picker and is not showcasing a control that can be plugged in directly into a project.
Depending on the project structure and ease, we can also achieve the same functionality using a DataTemplate
and then a DataTemplateSelector
. Via them, one would not need to apply styles explicitly as done here.
Points of Interest
While working on this feature enhancement, I learnt about how internals of standard control works. It was not straight forward for me at first but getting the desired result at the end proved time well spent. Having this feature implemented in a product that will help number of customers is satisfying.
History
Version 2: 25-Nov-2012 (Minor Update) [Thanks Pete O'Hanlon]
Version 1: 01-Nov-2012 (Minor Update) [Thanks Paulo Zemek]