Introduction
This article describes how to extend the WPF TextBox control to function like the Microsoft Blend TextBox that increments and decrements the value by using the mouse. As a bonus feature, a very cool data binding tooltip is included.
Background
As part of a Blend presentation, I included a WPF Take Home Pay Calculator program. (The demo only calculates North Carolina state tax for 2007, and is not a full blown program.) As a demo feature, I wanted TextBoxes that the user could change the value by using their mouse. You can download that presentation and code at my blog here: Get The Code From: Grain Of Sand.
Obviously, developers wouldn't want every TextBox on their form to have this feature! However, you may have an application that this fits. This is more of a you can do this article.
Also included in the .xaml is a resource that renders the tooltip and data binds back to the properties of the control that spawned the tooltip.
I must give credit to Tom Mathews for his demo code on tooltip data binding. You can view his code here: Binding a ToolTip in XAML.
I spent a good deal of time trying different things and almost gave up before I found Tom's post. Thank you Tom!
Desired Feature of TextBox: To Function Like the Microsoft Blend TextBox
- Increment or decrement the numeric value of a
TextBox
by allowing the user to click on the TextBox
and drag up, down, right, or left and have the value change.
- If the user enters a non-numeric value, just return the
TextBox
to its previous value after the user leaves the TextBox
.
- Extra credit: Display a tooltip to the user explaining how the feature works and how much the value will change based on the direction they drag their mouse.
Extending the TextBox
Like a good number of developers, I have authored all my own ASP.NET controls and WinForm controls. I have not extended any WPF controls so far, so this is my first. If I missed something or didn't do it the WPF way, please post a comment.
Properties
I added two new properties: XIncrementValue
and YIncrementValue
to the extended TextBox.
XIncrementValue
is how much the value will be incremented or decremented when the user drags the mouse right or left.
YIncrementValue
is how much the value will be incremented or decremented when the user drags the mouse up or down.
Public Property XIncrementValue() As Integer
Get
Return _intXIncrementValue
End Get
Set(ByVal Value As Integer)
If Value < 0 Then
Throw New ArgumentOutOfRangeException("XIncrementValue", _
"Must be equal to or greater than 0.")
End If
_intXIncrementValue = Value
End Set
End Property
Public Property YIncrementValue() As Integer
Get
Return _intYIncrementValue
End Get
Set(ByVal Value As Integer)
If Value < 0 Then
Throw New ArgumentOutOfRangeException("YIncrementValue", _
"Must be equal to or greater than 0.")
End If
_intYIncrementValue = Value
End Set
End Property
Trapping the Mouse Down to Begin the Increment - Decrement
I have declared a module level variable _objMouseIncrementor
of type MouseIncrementor
. When the user first clicks the TextBox
, a new MouseIncrementor
object is fired up and stored.
Notice that we are storing the location of the mouse and setting the initial state of the MouseIncrementor
to MouseIncrementor.MouseDirections.None
. This is important because until the user starts dragging right, left, up, or down, the TextBox
control does not know how to change the value of the TextBox
.
The MouseIncrementor
class is a simple container for the data we need to know in order to make the calculations and also to know if the control is in Mouse Increment mode. It keeps track of the mouse movement direction and the Point
of the mouse.
Private _objMouseIncrementor As MouseIncrementor
Protected Overrides Sub OnMouseDown(ByVal e As _
System.Windows.Input.MouseButtonEventArgs)
MyBase.OnMouseDown(e)
_objMouseIncrementor = New MouseIncrementor(e.GetPosition(Me), _
MouseIncrementor.MouseDirections.None)
End Sub
A simple class for storing the state of the mouse increment, decrement operation.
Class MouseIncrementor
Private _enumMouseDirection As MouseDirections = MouseDirections.None
Private _objPoint As Point
Enum MouseDirections
LeftRight
None
UpDown
End Enum
Public Property MouseDirection() As MouseDirections
Get
Return _enumMouseDirection
End Get
Set(ByVal Value As MouseDirections)
_enumMouseDirection = Value
End Set
End Property
Public Property Point() As Point
Get
Return _objPoint
End Get
Set(ByVal Value As Point)
_objPoint = Value
End Set
End Property
Public Sub New(ByVal objPoint As Point, _
ByVal enumMouseDirection As MouseDirections)
_objPoint = objPoint
_enumMouseDirection = enumMouseDirection
End Sub
End Class
Trapping the MouseMove, Incrementing, Decrementing the Value
Sanity checks:
- Verify that we are in an incrementing or decrementing operation. We know this by checking if the
_objMouseIncrementor
object has been instantiated or not.
- Check if the developer set the
XIncrementValue
and YIncrementValue
property values at design or run-time.
- Check to make sure the value in the
TextBox
will parse to a number.
We need to determine if this is the first time the OnMouseMove
sub has run since the _objMouseIncrementor
object was instantiated. We know this by checking the _objMouseIncrementor.MouseDirection
property. If its currently set to None
, then by checking the delta of the X
and Y
movements and testing for the higher value, we can set the _objMouseIncrementor.MouseDirection
to either LeftRight
or UpDown
so that the next time OnMouseMove
is called, we know how to calculate the new TextBox
value.
Now that we know which direction the mouse is moving and the delta from the last position, we can quickly calculate the new value of the TextBox
.
The last order of business is to actually set the TextBox
value and to record the current position of the mouse.
Protected Overrides Sub OnMouseMove(ByVal e As System.Windows.Input.MouseEventArgs)
MyBase.OnMouseMove(e)
If _objMouseIncrementor Is Nothing Then
Exit Sub
End If
If _intXIncrementValue = 0 AndAlso _intYIncrementValue = 0 Then
Exit Sub
End If
Dim dblValue As Double
If Double.TryParse(Me.Text, dblValue) = False Then
_objMouseIncrementor = Nothing
Exit Sub
End If
Dim intDeltaX As Double = _objMouseIncrementor.Point.X - e.GetPosition(Me).X
Dim intDeltaY As Double = _objMouseIncrementor.Point.Y - e.GetPosition(Me).Y
If _objMouseIncrementor.MouseDirection = _
MouseIncrementor.MouseDirections.None Then
If Math.Abs(intDeltaX) > Math.Abs(intDeltaY) Then
_objMouseIncrementor.MouseDirection = _
MouseIncrementor.MouseDirections.LeftRight
Else
_objMouseIncrementor.MouseDirection = _
MouseIncrementor.MouseDirections.UpDown
End If
End If
If _objMouseIncrementor.MouseDirection = _
MouseIncrementor.MouseDirections.LeftRight Then
If intDeltaX > 0 Then
dblValue -= _intXIncrementValue
ElseIf intDeltaX < 0 Then
dblValue += _intXIncrementValue
End If
Else
If intDeltaY > 0 Then
dblValue += _intYIncrementValue
Else
dblValue -= _intYIncrementValue
End If
End If
Me.Text = dblValue.ToString
_objMouseIncrementor.Point = e.GetPosition(Me)
End Sub
When the User Releases the Mouse Button
When the OnMouseUp
event code runs, it sets _objMouseIncrementor = Nothing
. This takes the TextBox
control out of the mouse increment mode.
Protected Overrides Sub OnMouseUp(ByVal e As System.Windows.Input.MouseButtonEventArgs)
MyBase.OnMouseUp(e)
_objMouseIncrementor = Nothing
_strOriginalTextValue = Nothing
End Sub
Allowing the User to Select the Text in the TextBox with the Mouse
With all this mouse movement and mouse down trapping going on, we can't forget to allow the user to double click the TextBox
to select the text. Easily accomplish this by the following event handler:
Protected Overrides Sub OnMouseDoubleClick(ByVal e As _
System.Windows.Input.MouseButtonEventArgs)
MyBase.OnMouseDoubleClick(e)
_objMouseIncrementor = Nothing
Me.SelectAll()
End Sub
Handling Text Entry in Our Numeric TextBox
Please keep in mind, there are better ways to restrict the allowed characters in a TextBox
than the code below. We are trying to emulate the Microsoft Blend TextBox, so let us just do it like Blend. Below is the simple code that accomplishes this:
The only comment for this code is the _strOriginalTextValue
variable that gets set in the OnGotFocus
event. This value allows us to restore the TextBox
value if the user enters a non-numeric value.
Protected Overrides Sub OnLostFocus(ByVal e As System.Windows.RoutedEventArgs)
MyBase.OnLostFocus(e)
Dim dblValue As Double
If Double.TryParse(MyBase.Text, dblValue) = True Then
MyBase.Text = dblValue.ToString
Else
If String.IsNullOrEmpty(_strOriginalTextValue) Then
_strOriginalTextValue = "0"
End If
MyBase.Text = _strOriginalTextValue
End If
_strOriginalTextValue = Nothing
_objMouseIncrementor = Nothing
End Sub
The ToolTip!
Most of the code below is standard XAML markup.
However, the cool part of this tooltip is the data binding to properties from the control this tooltip belongs to.
Since a ToolTip
is really a new Window
, you can't data bind by trying to set its source to an element on the Window
like we normally would by setting the ElementName
property. Instead, you must set a DataContext
for the ToolTip
. It took me forever to find out how to do this. I finally found this in Tom Mathews' article that I mentioned at the start of this article.
The magic is all in the statement: DataContext="{Binding Path=PlacementTarget, RelativeSource={RelativeSource Self}}"
. This establishes the DataContext
of the ToolTip
as the control. Now we have full access to all of the control's properties. This code is slightly different from Tom's, but is also performing a different function.
Since we now have a DataContext
, just data bind to properties on the control that opened this ToolTip
. Example: Text="{Binding Path=XIncrementValue}"
Below the two bold values, 100 and 50 are the YIncrementValue
and XIncrementValue
properties of the TextBox
.

<Window.Resources>
<ToolTip x:Key="toolTip4TextBoxWithMouseIncrementing"
DataContext="{Binding Path=PlacementTarget,
RelativeSource={RelativeSource Self}}">
<StackPanel HorizontalAlignment="Left" VerticalAlignment="Top"
Width="409" Height="Auto" Background="#FFDEE5F0">
<StackPanel.Resources>
<LinearGradientBrush x:Key="CoolToolTipGradientBrush"
EndPoint="1,0.5" StartPoint="0,0.5">
<GradientStop Color="#FF0024A5" Offset="0"/>
<GradientStop Color="#FF148CAB" Offset="1"/>
<GradientStop Color="#FF14A7AE" Offset="0.242"/>
<GradientStop Color="#FF1676AA" Offset="0.542"/>
<GradientStop Color="#FE09E4CE" Offset="0.836"/>
</LinearGradientBrush>
</StackPanel.Resources>
<TextBlock FontWeight="Bold" Foreground="White"
Text="Using The Incrementing TextBox"
Background="{DynamicResource CoolToolTipGradientBrush}"
Margin="0,0,0,10" Padding="0,3,0,3" TextAlignment="Center"/>
<TextBlock TextWrapping="WrapWithOverflow" Margin="5,0,5,0">
You can increment or decrement the value of this
textbox by simply clicking the mouse, hold and drag
up or down, right or left. Release when happy with the value.</TextBlock>
<Line Stroke="#FF0069D7" StrokeThickness="2" Margin="0,5,0,5"
HorizontalAlignment="Stretch" X2="409"/>
<StackPanel Orientation="Horizontal" Margin="0,0,0,0">
<TextBlock Margin="5,0,5,0">When moving the mouse up(+)
or down(-) the value will change by :</TextBlock>
<TextBlock FontWeight="Bold" Text="{Binding Path=YIncrementValue}"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,5,0,0">
<TextBlock Margin="5,0,5,0">When moving the mouse right(+)
or left(-) the value will change by :</TextBlock>
<TextBlock FontWeight="Bold" Text="{Binding Path=XIncrementValue}"/>
</StackPanel>
<TextBlock Text="WPF is so cool!"
HorizontalAlignment="Stretch"
Background="{DynamicResource CoolToolTipGradientBrush}"
Foreground="#FFFFFFFF" TextAlignment="Center"
Margin="0,5,0,0" Padding="0,3,0,3"/>
</StackPanel>
</ToolTip>
</Window.Resources>
Closing
Hope this article can help someone learn a little more about WPF.
History
- 24 June 2007: Initial release.