Click here to Skip to main content
15,175,960 members
Articles / Desktop Programming / WPF
Article
Posted 2 Nov 2010

Tagged as

Stats

101.9K views
2.8K downloads
26 bookmarked

Numeric Up/Down Textbox that Inherits from TextBox

Rate me:
Please Sign up or sign in to vote.
4.82/5 (27 votes)
17 May 2011CPOL11 min read
Describes how to create a control that inherits from a base control

Introduction

One of the significant items missing from the collection of Microsoft WPF controls is the numeric up/down control. I am sure that almost all other WPF developers were equally frustrated with Microsoft when they did not include this as an important control in the new controls in the 2010 release of Visual Studio (only four new controls in Visual Studio 2010). I have seen a number of versions of the numeric up/down (spinner) control on the Internet, but most of them have to be customized if the look and feel is to be different from the implementation.

Background

To provide the type of flexibility I would like to see in such a control, it could not be created as a UserControl, or a basic ControlTemplate. In any case, a significant amount of code would be required to implement the functionality of the buttons and the arrow keys. I have seen some implementations of TextBox controls that inherited from Control, but that meant that all the normal properties of the TextBox would not be available. It turns out that inheriting from a TextBox actually works very well. The XAML required to inherit from the TextBox is as follows:

XML
<TextBox x:Class="CustomControls.NumericUpDownTextBox"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
        xmlns:local="clr-namespace:CustomControls"/>

When overriding the TextBox, in addition to the TextBox control, there are two Button controls (or controls that emulate the Button functionality) required to give the user the ability to increment or decrement the value in the TextBox with the mouse button. When inheriting from a TextBox, the TextBox capability is provided for free. All that is required is to create two Button controls in code. This is done in the constructor. In addition to creating the controls, we also need to attach event handlers to the base TextBox that are responsible to handling the up and down key presses, and ensure that input is limited to numbers (and negative sign if first character, and negative values are allowed):

C#
/// <summary>
/// Constructor: initializes the TextBox, creates the buttons,
/// and attaches event handlers for the buttons and TextBox
/// </summary>
public NumericUpDownTextBox()
{
    InitializeComponent();
    var buttons = new ButtonsProperties(this);
    ButtonsViewModel = buttons;

    // Create buttons
    upButton = new Button()
    {
        Cursor = Cursors.Arrow,
        DataContext = buttons,
        Tag = true
    };
    upButton.Click += upButton_Click;

    downButton = new Button()
    {
        Cursor = Cursors.Arrow,
        DataContext = buttons,
        Tag = false
    };
    downButton.Click += downButton_Click;

    // Create control collections
    controls = new VisualCollection(this);
    controls.Add(upButton);
    controls.Add(downButton);

    // Hook up text event handlers
    this.PreviewTextInput += control_PreviewTextInput;
    this.PreviewKeyDown += control_PreviewKeyDown;
    this.LostFocus += control_LostFocus;
}

In the above code, it can be seen that I use the binding for properties for the button properties. I attempted to set properties directly, but this had to be reapplied each time the button was displayed because the Button would lose the properties. Using binding means that the Buttons will only get the properties when they are needed. For some reason, the Cursor is able to maintain the property, as does the Click event. Another event I had trouble with was when I attempted to use a Border as a button.

The most critical part of the code is the override of the ArrangeOverride method. The method is responsible for positioning the controls on the allocated space. In this method, I do not paint the Buttons if the Width is below a certain ratio with the Height (I chose 1.5). The base ArrangeOverride is called with the rectangle allocated for the base TextBox and then uses the Arrange method for the Buttons, providing the method the rectangle for each button.

C#
/// <summary>
/// Called to arrange and size the content of a 
///         System.Windows.Controls.Control object.
/// </summary>
/// <param name="arrangeSize">The computed size that is used to 
///                 arrange the content</param>
/// <returns>The size of the control</returns>
protected override Size ArrangeOverride(Size arrangeSize)
{
    double height = arrangeSize.Height;
    double width = arrangeSize.Width;
    showButtons = width > 1.5 * height;

    if (showButtons)
    {
        double buttonWidth = 3 * height / 4;
        Size buttonSize = new Size(buttonWidth, height / 2);
        Size textBoxSize = new Size(width - buttonWidth, height);
        double buttonsLeft = width - buttonWidth;
        Rect upButtonRect = new Rect(new 
            Point(buttonsLeft, 0), buttonSize);
        Rect downButtonRect = new Rect(new 
            Point(buttonsLeft, height / 2), buttonSize);
        base.ArrangeOverride(textBoxSize);

        upButton.Arrange(upButtonRect);
        downButton.Arrange(downButtonRect);
        return arrangeSize;
    }
    else
    {
        return base.ArrangeOverride(arrangeSize);
    }
}

GetVisualChild just needs to pass the base GetVisualChild if the index argument is less than the base GetVisualChild, and pass the button otherwise.

C#
protected override Visual GetVisualChild(int index)
{
    if (index < base.VisualChildrenCount)
        return base.GetVisualChild(index);
    return controls[index - base.VisualChildrenCount];
}

VisualChildrenCount just needs to determine if the buttons are displayed or not, and either pass the base value, or the base value plus two.

C#
protected override int VisualChildrenCount
{
    get
    {
        if (showButtons)
            return controls.Count + base.VisualChildrenCount;
        else
            return base.VisualChildrenCount;
    }
}

Using the Code

Using the control is just like using any other custom control: a reference to the namespace has to be included as an attribute of the root element of the XAML, and the name assigned to the reference is then used to define the control in an element in the XAML. Within this element, all the properties of a TextBox can be assigned using attributes or elements defined within this element. There are also a number of custom properties that can be set. The ones that control the appearance of the buttons all start with "Button.". If these specialized properties are not used, then either the values used by the TextBox or the defaults are used. There are also several other properties to set the minimum (MinValue) and maximum (MaxValue) values, and a Value property which interfaces with the TextBox value as an integer:

XML
<Window x:Class="WPFControlTest.MainWindow"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:local="clr-namespace:CustomControls"
      Title="MainWindow" Height="192" Width="281">
    <Grid Background="SteelBlue">
        <local:NumericUpDownTextBox x:Name="textBox2" Height="25" Margin="20" 
              FontStyle="Italic" FontSize="10" BorderBrush="Green" 
              BorderThickness="2" Background="LightGray" Foreground="DarkBlue"
              MinValue="100" MaxValue="1001" 
              HorizontalAlignment="Stretch" VerticalAlignment="Center"
              ButtonBackground="BlueViolet" ButtonBorderBrush="LightGreen" 
              ButtonForeground="Azure" ButtonBorderThickness="1,3,1,3" 
              ButtonMouseOverBackground="Aquamarine" 
              ButtonPressedBackground="Red"/>
    </Grid>
</Window>

Points of Interest

I went through a lot of different implementations to attempt to get the buttons to inherit the TextBox properties. Basically, I wanted the Background, Foreground, and Border to be inherited from TextBox. In addition, I wanted the content of the Buttons to be a polygon to represent the up or down arrow. My first iteration just use used two basic buttons, and this was good enough to get the functionality I wanted for the TextBox. Unfortunately, a Button's border is not changed when the border properties are changed. That means I had to come up with something else.

My initial idea was to use a border created in code instead, and then attach to the Mouse events to simulate the MouseOver and MouseDown events of the button. Unfortunately, this did not work because the events were not fired.

I had wanted to minimize the amount of XAML needed for this control. Because the other options I had looked at did not provide the look and feel I wanted, that meant that I had to create a ControlTemplate for the Buttons as a Resource for the TextBox as part of the XAML.

Well, this did work, but not as I thought it would. I initially attempted to create the Border and Polygon (for the arrows) in code and assign it to the content of the Button. For some reason, I could not programmatically set the content of the TextBox (maybe I am missing something), so I backtracked. I initially put the Border inside the Template, and then the content inside that, but that still gave me problems setting the content from code, so I ended up defining the Polygon for the arrows inside a Border, which was inside the ControlTemplate. The Border then could inherit the properties for border Thickness and Background from the Button. This worked, except I needed to customize the Polygon for the inside size of the Border.

So to get the Arrow Polygon, I needed to get at the properties of the Border to determine the location of the Points for the Polygon. I initially attempted to create a Binding using a class derived from the IValueConverter interface and passing the Border as the Converter parameter. This did not work because the PropertyChanged event is only triggered when the property is changed, and for the Border, that only occurs during the loading of the control, and at that point, the size of the control is still zero. That left the only option being to using the IMultiValueConverter, and including the Border's Height, Width, and BorderThickness properties in the parameters. There was only one other piece of information I needed to create the arrow for each button: the direction of the arrow (up or down). Therefore, one more property was required, a Boolean. I decided to use the Button's DataContext to pass this information to the Polygon. I also considered using the Tag property, but figured that the DataContext was a slightly easier to understand method. The ControlTemplate ended up looking as follows:

XML
<ControlTemplate TargetType="{x:Type Button}">
    <Border Name="buttonBorder"
	        BorderBrush="{Binding BorderBrush}"
	        BorderThickness="{Binding BorderThickness}"
	        Background="{Binding Background}"
	        CornerRadius="3">
		<Polygon Fill="{Binding Foreground}" >
            <Polygon.Points>
              <MultiBinding Converter="{StaticResource ArrowCreater}" >
              </MultiBinding>
            </Polygon.Points>
        </Polygon>
    </Border>
    <ControlTemplate.Triggers>
      <!--<Trigger Property="IsFocused" Value="True">
      </Trigger>
      <Trigger Property="IsDefaulted" Value="True">
      </Trigger>-->
      <Trigger Property="IsMouseOver" Value="True">
         <Setter TargetName="buttonBorder" Property="Background"
                Value="{Binding IsMouseOverBackground}"/>
      </Trigger>
      <Trigger Property="IsPressed" Value="True">
        <Setter TargetName="buttonBorder" Property="Background"
          </Trigger>
          <!--<Trigger Property="IsEnabled" Value="False">
          </Trigger>-->
    </ControlTemplate.Triggers>
</ControlTemplate>

One important note on the control template, which is wrapped in a Style with a TargetType of Button, is that TargetType has to be reapplied to the ControlTemplate, or the compiler will complain about the IsPressed.

The IMultiValueConverter class had only one little detail that I dealt with, and that was to check if the Border's Height or Width was 0, which would be the case until the Border was actually being laid out. In these cases, I just returned without processing the code required to create the arrows.

C#
internal class ArrowCreater : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType,
           object parameter, System.Globalization.CultureInfo culture)
    {
        double width = (double)values[0];
        double height = (double)values[1];
        if ((height == 0.0) || (width == 0.0)) return null;
        Thickness borderThickness = (Thickness)values[2];
        bool up = (bool)values[3];
        double arrowHeight = height - borderThickness.Top -
            borderThickness.Bottom;
        double arrowWidth = width - borderThickness.Left -
            borderThickness.Right;
        return CreateArrow(arrowWidth, arrowHeight, up);
    }

    public object[] ConvertBack(object value, Type[] targetTypes,
        object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }

    private PointCollection CreateArrow(double width,
    {
        double margin = height * .2;
        double pointY;
        double baseY;

        if (isUp)
        {
            pointY = margin;
            baseY = height - margin;
        }
        else
        {
            baseY = margin;
            pointY = height - margin;
        }
        var pts = new PointCollection();
        pts.Add(new Point(margin, baseY));
        pts.Add(new Point(width / 2, pointY));
        pts.Add(new Point(width - margin, baseY));
        return pts;
    }
}

I also included optional properties for buttons. If these properties are not set, the properties for the TextBox are used when available (such as Background, Foreground, BorderBrush, BorderThickness), or defaults are used (CornerRadius, Background when button is pressed or on mouse over). I attempted to have a separate class to contain the button properties so that they could be set with the term "Buttons" followed by a period and the property name, but WPF apparently does not support having a property that contains properties, so I ended up just prefixing the property name with "Button.".

The buttons have their own DataContext, and I used a special class for this DataContext, which inherits the InotifyPropertyChanged interface. This has a pointer to the base NumericUpDownTextBox class, and the properties in this class only have a property Get. Each Get will either return the Button specific property, or the property for the TextBox, or the default if the Button specific property has not been set. The Button specific properties for NumericUpDownTextBox handle the change event, and call the Button's DataContext class' public NotifyPropertyChanged method to trigger the PropertyChangedEventHandler for the Button's DataContext. This works very cleanly.

User Input

The only other significant detail was code to handle the user input. There are three ways for the user to change the value: the keyboard's number keys (and negative sign), the keyboard's up and down arrow keys, and the two Button controls.

In this code, the user is prevented from using the keyboard to enter any text in the TextBox that is not numeric, except the negative sign ('-'), and the negative sign can only be entered if the minimum allowed value is less than zero, the input caret is at the beginning of the TextBox text, and a negative sign does not already exist. Also, if the user has entered keystrokes that will obviously create a value that will exceed the Maximum (if the Maximum is greater than zero) or the Minimum (if the Minimum is less than zero), then the value will be fixed. Part of the checking for validity of the TextBox text has to account for the caret position and the selection length. For this, the StringBuilder control makes it very easy to determine the value of the TextBox text after user input. The PreviewTextInput event is used to control the user's changes in the TextBox:

C#
private void control_PreviewTextInput(object sender, 
                     TextCompositionEventArgs e)
{
   // Catch any non-numeric keys
   if ("0123456789".IndexOf(e.Text) < 0)
   {
      // else check for negative sign
      if (e.Text == "-" && MinValue < 0)
      {
         if (this.Text.Length == 0 || (this.CaretIndex == 0 && 
             this.Text[0] != '-'))
         {
              e.Handled = false;
              return;
          }
       }
       e.Handled = true;
    }
    else // A digit has been pressed
    {
        // We now know that have good value: check for attempting 
            // to put number before '-'
        if (this.Text.Length > 0 && this.CaretIndex == 0 && 
            this.Text[0] == '-' && this.SelectionLength == 0)
        {
            // Can't put number before '-'
            e.Handled = true;
        }
        else
        {
            // check for what new value will be:
            StringBuilder sb = new StringBuilder(this.Text);
            sb.Remove(this.CaretIndex, this.SelectionLength);
            sb.Insert(this.CaretIndex, e.Text);
            int newValue = int.Parse(sb.ToString());
            // check if beyond allowed values
            if (FixValueKeyPress(newValue))
            {
                e.Handled = false;
            }
            else
            {
                e.Handled = true;
            }
        }
    }
}

The method FixValueKeyPress checks the resulting value of the user input, and will force correction, leaving the caret at the end of the text. If there are no issues with the user input, the TextBox will be left unchanged.

The pressing of the keyboard up and down arrow keys are captured with the PreviewKeyDown event:

C#
/// <summary>
/// Checks if the keypress is the up or down key, and then
/// handles keyboard input
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void control_PreviewKeyDown(object sender, KeyEventArgs e)
{
    if (e.Key == Key.Down)
    {
        HandleModifiers(-1);
        e.Handled = true;
    }
    else if (e.Key == Key.Up)
    {
        HandleModifiers(1);
        e.Handled = true;
    	// Space key is not caught by PreviewTextInput
    else if (e.Key == Key.Space)
        e.Handled = true;
    }
    else
        e.Handled = false;
}

/// <summary>
/// Checks if any of the Keyboard modifier keys are pressed that might
/// affect the change in the value of the TextBox.
/// In this case only the shift key affects the value
/// </summary>
/// <param name="value">Integer value to modify</param>
private void HandleModifiers(int value)
{
	if (Keyboard.Modifiers == ModifierKeys.Shift) value *= 10;
	Add(value);
}

The event handler checks if the Key in the event arguments is an up or down key, and then uses the HandleModifiers method (this may be used with the up/down button also). This method is provided an integer value which will indicate if the up or down arrow was pressed. This value (absolute value of 1) is then multiplied by the value of a constant if modifier keys are pressed, and then adds the value passed to the content of the TextBox. Note to also look for the space key in the preview since PreviewTextInput does not catch the space key. We should check to see if any text is selected when the space key is pressed, and remove selected text, but it did not seem worth the complexity.

History

  • 11/22/2010: Fixed update issues with MaxValue, MinValue, and Value. MaxValue and MinValue are now applied dynamically. Value now is correct after making a change in the TextBox and tabbing to the next control. Also added TextBoxes to the test form for Value, MinValue, and MaxValue.
  • 12/7/2010: Source code updated
    The following changes were made to the code:
    1. Changed the type for the "Value" DependencyProperty to int? instead of int, and set its initial value to null. This was required to be able to initialize the value of the control to "0". Initially the default value was 0, which meant that there was no change in the value of "Value" so the Text would not be updated.
    2. When the control loses focus, it is now initializes to 0, or the MinValue or MaxValue if 0 is not between these values.
    3. Added repeat button functionality working after several attempts. Discovered that can capture the preview events, but not the other RoutedEvents. The System.Windows.Timer is used to generate the delay and interval, and the timer is initialized and disposed of on the PreviewMouseUp and PreviewMouseDown events. For MouseDown and MouseUp, it does not matter since the Buttons are the consumers anyway, but could not capture the MouseEnter and MouseLeave events, and there is no preview for these events. Therefore, I could not use an event to determine if the mouse was over the button, but had to check the mouse position each timer event for the repeat functionality. If you check the scroll bars, you will see that the scroll bars only scroll after the MouseDown event on the ScrollBar when the mouse is over ScrollBar, and stop when it is not.
    4. Also made a few other changes in organization. Amazingly managed to keep the lines of code to about what they were before.
  • 5/17/2011: Source code updated
    1. This update adds support for the mouse wheel and is thanks to AndreyA

License

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

Share

About the Author

Clifford Nelson
Software Developer (Senior) Clifford Nelson Consulting
United States United States
Has been working as a C# developer on contract for the last several years, including 3 years at Microsoft. Previously worked with Visual Basic and Microsoft Access VBA, and have developed code for Word, Excel and Outlook. Started working with WPF in 2007 when part of the Microsoft WPF team. For the last eight years has been working primarily as a senior WPF/C# and Silverlight/C# developer. Currently working as WPF developer with BioNano Genomics in San Diego, CA redesigning their UI for their camera system. he can be reached at qck1@hotmail.com.

Comments and Discussions

 
QuestionThanks but something is forgetten Pin
muharip2-Nov-15 8:49
Membermuharip2-Nov-15 8:49 
AnswerRe: Thanks but something is forgetten Pin
Clifford Nelson5-Dec-15 19:08
MemberClifford Nelson5-Dec-15 19:08 
GeneralMy vote of 5 Pin
Christian Pirschalawa20-Jun-14 5:43
MemberChristian Pirschalawa20-Jun-14 5:43 
AnswerRe: My vote of 5 Pin
Clifford Nelson5-Dec-15 19:09
MemberClifford Nelson5-Dec-15 19:09 
BugEntering numbers larger than max int Pin
Christian Pirschalawa20-Jun-14 5:24
MemberChristian Pirschalawa20-Jun-14 5:24 
Questionuse it Pin
RafikProject31-Jan-14 14:06
MemberRafikProject31-Jan-14 14:06 
AnswerRe: use it Pin
Clifford Nelson31-Jan-14 14:16
MemberClifford Nelson31-Jan-14 14:16 
Questionawesomeness Pin
Member 104429243-Dec-13 14:30
MemberMember 104429243-Dec-13 14:30 
AnswerRe: awesomeness Pin
Clifford Nelson3-Dec-13 23:26
MemberClifford Nelson3-Dec-13 23:26 
GeneralMy vote of 3 Pin
Benny S. Tordrup6-Mar-13 0:03
MemberBenny S. Tordrup6-Mar-13 0:03 
AnswerRe: My vote of 3 Pin
Clifford Nelson6-Mar-13 8:24
MemberClifford Nelson6-Mar-13 8:24 
Questionhow to add this control to a existing project? Pin
Roberto Pulvirenti25-May-12 1:06
MemberRoberto Pulvirenti25-May-12 1:06 
AnswerRe: how to add this control to a existing project? Pin
Clifford Nelson25-May-12 9:19
MemberClifford Nelson25-May-12 9:19 
GeneralRe: how to add this control to a existing project? Pin
Roberto Pulvirenti10-Jun-12 6:48
MemberRoberto Pulvirenti10-Jun-12 6:48 
GeneralMy vote of 5 Pin
LLINF29-Mar-12 14:11
MemberLLINF29-Mar-12 14:11 
AnswerRe: My vote of 5 Pin
Clifford Nelson12-Apr-12 15:53
MemberClifford Nelson12-Apr-12 15:53 
GeneralMy vote of 5 Pin
ProEnggSoft3-Mar-12 17:58
MemberProEnggSoft3-Mar-12 17:58 
AnswerRe: My vote of 5 Pin
Clifford Nelson12-Apr-12 15:54
MemberClifford Nelson12-Apr-12 15:54 
GeneralHuge help Pin
Wobblysquee18-Sep-11 9:12
MemberWobblysquee18-Sep-11 9:12 
AnswerRe: Huge help Pin
Clifford Nelson12-Apr-12 15:55
MemberClifford Nelson12-Apr-12 15:55 
GeneralGreat Article Pin
rctaubert18-May-11 10:10
Memberrctaubert18-May-11 10:10 
GeneralMy vote of 5 Pin
Filip D'haene17-May-11 4:45
MemberFilip D'haene17-May-11 4:45 
GeneralRe: My vote of 5 Pin
Clifford Nelson17-May-11 10:20
MemberClifford Nelson17-May-11 10:20 
GeneralLacking mouse wheel support Pin
AndreyA16-May-11 0:53
MemberAndreyA16-May-11 0:53 
GeneralRe: Lacking mouse wheel support Pin
Clifford Nelson16-May-11 22:38
MemberClifford Nelson16-May-11 22:38 

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

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