Click here to Skip to main content
Click here to Skip to main content

Tagged as

How to extend a WPF Textbox to Custom Picker

, 24 Nov 2012
Rate this:
Please Sign up or sign in to vote.
This article is to show how to extend a standard WPF Textbox to behave as a Picker via Template Style.


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

/// <summary>
/// Value converter to convert a datetime object to the specified string format. 
/// If the format is not specified, it will be converted to short date string &quot;12/31/2011&quot;
/// </summary>
[ValueConversion(typeof(DateTime), typeof(string))]
public class CalendarConverter : IValueConverter
{
	#region IValueConverter Members

	/// <summary>
	/// Implement the ConvertBack method of IValueConverter. Converts DateTime object to specified format
	/// </summary>
	/// <param name="value">The DateTime value we&#39;re converting
	/// <param name="targetType">Not used
	/// <param name="parameter">String format to convert to (optional)
	/// <param name="culture">Not used
	/// <returns>Collapsed if value is true, else Visible</returns>
	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);
	}

	/// <summary>
	/// Implement the Convert method of IValueConverter. Converts a string representation of a date to DateTime
	/// </summary>
	/// <param name="value">The visibility value to convert to a boolean.
	/// <param name="targetType">Not used
	/// <param name="parameter">Not used
	/// <param name="culture">Not used
	/// <returns>false if Visible, else true</returns>
	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();

// Calendar DatePicker
Style sCalendar = (Style)tb.TryFindResource("tbCalendarStyle");
if (sCalendar != null)
   textBox1.Style = sCalendar;

// Calculator NumberPicker
Style sCalculator = (Style)tb.TryFindResource("tbCalculatorStyle");
if (sCalculator != null)
   textBox2.Style = sCalculator;

// List StringPicker
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]

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Sandeep Mewara
Software Developer (Senior)
India India
Current location: Bangalore, India.
Follow on   Twitter

Comments and Discussions

 
GeneralMy vote of 5 PinprofessionalMohammed Hameed4-Jun-13 20:54 
GeneralMy vote of 5 PinmemberSalCon25-Feb-13 3:14 
QuestionAdd binding to ListView PinmemberAndreas Hellström2-Jan-13 11:32 
GeneralMessage Removed PinadminMatthew Dennis29-Nov-12 5:18 
GeneralRe: My vote of 3 PinmvpSandeep Mewara29-Nov-12 5:24 
GeneralRe: My vote of 3 PinmemberSoMad1-Dec-12 11:27 
GeneralMy vote of 5 PinmemberChandra Mohan26-Nov-12 6:03 
Nicely written. Thanks for the article Sandeep.
GeneralRe: My vote of 5 PinmvpSandeep Mewara26-Nov-12 15:14 
QuestionCan this be done for web application as well? PinmemberVani Kulkarni25-Nov-12 22:43 
AnswerRe: Can this be done for web application as well? PinmvpSandeep Mewara26-Nov-12 15:12 
GeneralMy vote of 5 PinmemberSavalia Manoj M18-Nov-12 16:46 
GeneralRe: My vote of 5 PinmvpSandeep Mewara20-Nov-12 0:45 
QuestionInteresting, but... PinprotectorPete O'Hanlon6-Nov-12 5:01 
AnswerRe: Interesting, but... PinmvpSandeep Mewara6-Nov-12 7:16 
GeneralMy vote of 5 PinmentorWayne Gaylard5-Nov-12 18:49 
GeneralRe: My vote of 5 PinmvpSandeep Mewara5-Nov-12 19:13 
GeneralMy vote of 5 PinmvpOriginalGriff3-Nov-12 5:27 
GeneralRe: My vote of 5 PinmvpSandeep Mewara3-Nov-12 6:00 
GeneralWell done PinmvpEspen Harlinn3-Nov-12 1:45 
GeneralRe: Well done PinmvpSandeep Mewara3-Nov-12 1:50 
GeneralMy vote of 3 PinmvpPaulo Zemek2-Nov-12 5:40 
GeneralRe: My vote of 3 PinmvpSandeep Mewara2-Nov-12 5:50 
GeneralRe: My vote of 3 PinmvpPaulo Zemek2-Nov-12 6:02 
GeneralRe: My vote of 3 PinmvpSandeep Mewara2-Nov-12 6:05 
GeneralRe: My vote of 3 PinmvpSandeep Mewara2-Nov-12 6:14 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web02 | 2.8.140821.2 | Last Updated 24 Nov 2012
Article Copyright 2012 by Sandeep Mewara
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid