Click here to Skip to main content
14,768,455 members
Articles » Desktop Development » Edit Controls » General
Article
Posted 21 Feb 2021

Stats

1.9K views
57 downloads
6 bookmarked

WPF IntegerUpDown Control - Adapt and Overcome

Rate me:
Please Sign up or sign in to vote.
5.00/5 (1 vote)
21 Feb 2021CPOL
Not about just writing code, but adapting someone else's code to your own needs

Introduction

2021.02.25 - Make sure you read the update section(s) for breaking changes regarding this code.

In a perfect world, when you download the source code to an article, you do so with the expectation that for the most part, it will work as advertised, and that you'll be able to make use of the code with little/no changes. In a perfect world. Well, Sparky, the world isn't perfect, and chances are pretty good that you'll have to tweak something to make it usable in your own application. I'm not talking about minor formatting changes - I'm talking about outright modification of the code's base/primary function. This is the primary reason this article was written, to give you a taste of the process. The target audience is people that are fairly new to programming that come to sites like CodeProject, and expect things to be handed to them on a silver platter. The reality is that THERE IS NO SILVER PLATTER. At least, most of the time there isn't.'

AutomaticListView example apps
 

The reason I downloaded the article cited below was that I was using the XCeed WPF Toolkit so I could use the IntegerUpDown control contained therein. That toolkit is very nice and has a lot of nice controls in it, but considering I only used one small control from it, I didn't feel it was worth the 1.3mb disk footprint of the compiled assembly. So I went out in search of a numeric up/down control. While NuGet had some controls available, they either had dependencies I didn't want to deal with, or they didn't work, so I came to CodeProject to see what was available, and found the NumberBox control.

Establishing a Baseline

Having downloaded the code from this article (posted in 2011) - WPF User Control - NumericBox[^] - I immediately copied the NumberBox code into a "NumberBox" folder in my application, and copied the MainWindow XAML code to my MainWindow.XAML file.

<Window x:Class="WpfApp1_2020.MainWindow"
		...
		clr-namespace:NumericBox"
        Title="MainWindow" Height="250" Width="400" >

<nb:NumericBox x:Name="numberBox" Background="Orange"
               Value="10.50000" ValueFormat="0.000" 
               Increment="0.5" Minimum="-100" Maximum="100"						   
               ValueChanged="NumberBox_ValueChanged" Width="75"/ >

When I compiled and ran the code, it seemed to work as advertised (baseline established!), but changes were required to make it fit my specific needs. I copied the NumberBox folder to a new folder named IntegerUpDown, and started working on it.

Making It Mine

The very first thing I did was change the layout and appearance of the user control to have the buttons better adapt their height to the accompanying TextBox. Notably, I fixed the textbox being overlaid by the buttons in the designer, changed the border of the TextBox to black, and put the buttons into a grid with RowDefinitions that caused the buttons to take up 50% of the available height. I also changed the arrow polygons Fill property to be bound to the button's foreground color, and the buttons themselves to be RepeatButtons.

<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*"/>
        <ColumnDefinition Width="Auto"/>
    </Grid.ColumnDefinitions>

    <Grid Grid.Column="0">
    <TextBox x:Name="PART_NumericTextBox" Grid.Column="0" BorderBrush="Black" Margin="0,0,0.2,0" 
                PreviewTextInput="numericBox_PreviewTextInput" 
                MouseWheel="numericBox_MouseWheel" />
    </Grid>
    <Grid Grid.Column="1">
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <RepeatButton x:Name="PART_IncreaseButton" Grid.Row="0" Margin="0,0,0,0.1" 
                        BorderBrush="Black" BorderThickness="0.75" Width="13"
                        Foreground="Black" Background="#cecece"
                        Style="{DynamicResource UpDownButtonStyle}"
                        Click="increaseBtn_Click" >
            <RepeatButton.Content>
                <Polygon StrokeThickness="0.5" Stroke="Transparent" 
                            Points="0,0 -2,5 2,5" Stretch="Fill"
                            Fill="{Binding RelativeSource={RelativeSource Mode=FindAncestor, 
                                AncestorType=RepeatButton}, Path=Foreground}" />
            </RepeatButton.Content>
        </RepeatButton>
        <RepeatButton x:Name="PART_DecreaseButton" Grid.Row="1" Margin="0,0.1,0,0" Width="13"
                        BorderBrush="Black" BorderThickness="0.75"
                        Foreground="Black" Background="#cecece"
                        Style="{DynamicResource UpDownButtonStyle}" 
                        Click="decreaseBtn_Click" >
            <RepeatButton.Content>
                <Polygon StrokeThickness="0.5" Stroke="Transparent" 
                            Points="-2,0 2,0 0,5 " Stretch="Fill" 
                            Fill="{Binding RelativeSource={RelativeSource Mode=FindAncestor, 
                            AncestorType=RepeatButton}, Path=Foreground}" />
            </RepeatButton.Content>
        </RepeatButton>
    </Grid>
</Grid>

To support the newly defined RepeatButtons in the XAML, the next thing I did was remove the timer and button left mouse button down preview events from the code, simply because repeat buttons already handle this stuff. I also removed all of the supporting Popup and Menu code, because I didn't particularly need that stuff and it really serves no useful purpose for my situation. (Because the code was removed, there's nothing to show you here. Because it was removed.)

My next task was to convert it from double to int. This involved the changing the Minimum, Maximum, Increment, Value, and ValueChangedEvent properties.

//===========================================================
public static readonly DependencyProperty MinimumProperty =
    DependencyProperty.Register("Minimum", typeof(int), typeof(IntegerUpDown), new PropertyMetadata(int.MinValue, OnMinimumChanged));
private static void OnMinimumChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
{
    IntegerUpDown numericBoxControl = new IntegerUpDown();
    numericBoxControl.minimum = (int)args.NewValue;
}
public int Minimum
{
    get { return (int)this.GetValue(MinimumProperty); }
    set { this.SetValue(MinimumProperty, value); }
}

//===========================================================
public static readonly DependencyProperty MaximumProperty =
    DependencyProperty.Register("Maximum", typeof(int), typeof(IntegerUpDown), new PropertyMetadata(int.MaxValue, OnMaximumChanged));
private static void OnMaximumChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
{
    IntegerUpDown numericBoxControl = new IntegerUpDown();
    numericBoxControl.maximum = (int)args.NewValue;
}
public int Maximum
{
    get { return (int)this.GetValue(MaximumProperty); }
    set { this.SetValue(MaximumProperty, value); }
}

//===========================================================
public static readonly DependencyProperty IncrementProperty =
    DependencyProperty.Register("Increment", typeof(int), typeof(IntegerUpDown), new PropertyMetadata(1, OnIncrementChanged));
private static void OnIncrementChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
{
    IntegerUpDown numericBoxControl = new IntegerUpDown();
    numericBoxControl.increment = (int)args.NewValue;
}
public int Increment
{
    get { return (int)this.GetValue(IncrementProperty); }
    set { this.SetValue(IncrementProperty, value); }
}

//===========================================================
public static readonly DependencyProperty ValueProperty =
    DependencyProperty.Register("Value", typeof(int), typeof(IntegerUpDown), new PropertyMetadata(new Int32(), OnValueChanged));
private static void OnValueChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
{
    IntegerUpDown numericBoxControl = (IntegerUpDown)sender;
    numericBoxControl.value = (int)args.NewValue;
    numericBoxControl.PART_NumericTextBox.Text = numericBoxControl.value.ToString(numericBoxControl.ValueFormat);
    numericBoxControl.OnValueChanged((int)args.OldValue, (int)args.NewValue);
}
public int Value
{
    get { return (int)this.GetValue(ValueProperty); }
    set { this.SetValue(ValueProperty, value); }
}

//===========================================================
public static readonly DependencyProperty ValueFormatProperty =
    DependencyProperty.Register("ValueFormat", typeof(string), typeof(IntegerUpDown), new PropertyMetadata("0", OnValueFormatChanged));
private static void OnValueFormatChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
{
    IntegerUpDown numericBoxControl = new IntegerUpDown();
    numericBoxControl.valueFormat = (string)args.NewValue;
}
public string ValueFormat
{
    get { return (string)this.GetValue(ValueFormatProperty); }
    set { this.SetValue(ValueFormatProperty, value); }
}

//===========================================================
public static readonly RoutedEvent ValueChangedEvent =
    EventManager.RegisterRoutedEvent("ValueChanged", RoutingStrategy.Direct, typeof(RoutedPropertyChangedEventHandler<int>), typeof(IntegerUpDown));
public event RoutedPropertyChangedEventHandler<int> ValueChanged
{
    add    { this.AddHandler   (ValueChangedEvent, value); }
    remove { this.RemoveHandler(ValueChangedEvent, value); }
}
private void OnValueChanged(int oldValue, int newValue)
{
    RoutedPropertyChangedEventArgs<int> args = new RoutedPropertyChangedEventArgs<int>(oldValue, newValue);
    args.RoutedEvent = IntegerUpDown.ValueChangedEvent;
    this.RaiseEvent(args);
}


</int></int></int></int>

While playing around with the control "as delivered", I noticed that the manual text entry didn't really work very well. If you place the cursor at the end of the text, and just starting typing a number character, eventually the newly typed characters start to be added at the BEGINNING of the value. This called for a fix to the existing numericBox_TextInput event handler. I also renamed the event handler to numericBox_PreviewTextInput, because that's the event it was actually handling.

Old code:

private void numericBox_TextInput(object sender, TextCompositionEventArgs e)
{
    try
    {
        double tempValue = Double.Parse(PART_NumericTextBox.Text);
        if (!(tempValue < Minimum || tempValue > Maximum)) Value = tempValue;
    }
    catch (FormatException)
    {
    }
}

New code:

As you can see, adequate care is taken to account for caret position, while still validating the value to ensure that it's not oustide the specified min/max range.

private void numericBox_PreviewTextInput(object sender, TextCompositionEventArgs e)
{
	TextBox textbox = sender as TextBox;
	int caretIndex = textbox.CaretIndex;
	try
	{
		int newvalue;
        // see if the text will parse to an integer
		bool error = !int.TryParse(e.Text, out newvalue);
		string text = textbox.Text;
		if (!error)
		{
            // we have a valid integer, so insert the new text at the 
            //caret's position 
			text = text.Insert(textbox.CaretIndex, e.Text);
            // check the string again to make sure it still parses
			error = !int.TryParse(text, out newvalue);
			if (!error)
			{
                // we're good, so make sure the value is in the 
                // specified min/max range
				error = (newvalue < this.Minimum || newvalue > this.Maximum);
			}
		}
		if (error)
		{
            // play the error sound
			SystemSounds.Hand.Play();
            // reset the caret index to where it was when we entered 
            // this method
			textbox.CaretIndex = caretIndex;
		}
		else
		{
            // set the textbox text (this will set the caret index to 0)
			this.PART_NumericTextBox.Text = text;
            // put the caret at the END of the inserted text
			textbox.CaretIndex = caretIndex+e.Text.Length;
            // set the Value to the new value 
			this.Value = newvalue;
		}
	}
	catch (FormatException)
	{
	}
	e.Handled = true;
}

Next, I wanted to improve the increase/decrease methods that were called by the button click events.

Old code:

//=============================================================
private void IncreaseValue()
{
    Value += Increment;
    if (Value < Minimum || Value > Maximum) Value -= Increment;
}
//=============================================================
private void DecreaseValue()
{
    Value -= Increment;
    if (Value < Minimum || Value > Maximum) Value += Increment;
}

New code:

//=============================================================
private void IncreaseValue()
{
    Value = Math.Min(this.Maximum, this.Value + this.Increment);
}
//=============================================================
private void DecreaseValue()
{
    Value = Math.Max(this.Minimum, this.Value - this.Increment);
}

Finally, the ApplyTemplate method need some work to align with all the previous changes.

Old code:

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

    Button btn = GetTemplateChild("PART_IncreaseButton") as Button;
    if (btn != null)
    {
        btn.Click += increaseBtn_Click;
        btn.PreviewMouseLeftButtonDown += increaseBtn_PreviewMouseLeftButtonDown;
        btn.PreviewMouseLeftButtonUp += increaseBtn_PreviewMouseLeftButtonUp;
    }

    btn = GetTemplateChild("PART_DecreaseButton") as Button;
    if (btn != null)
    {
        btn.Click += decreaseBtn_Click;
        btn.PreviewMouseLeftButtonDown += decreaseBtn_PreviewMouseLeftButtonDown;
        btn.PreviewMouseLeftButtonUp += decreaseBtn_PreviewMouseLeftButtonUp;
    }

    TextBox tb = GetTemplateChild("PART_NumericTextBox") as TextBox;
    if (tb != null)
    {
        PART_NumericTextBox = tb;
        PART_NumericTextBox.Text = Value.ToString(ValueFormat);
        PART_NumericTextBox.PreviewTextInput += numericBox_TextInput;
        PART_NumericTextBox.MouseWheel += numericBox_MouseWheel;
    }

    System.Windows.Controls.Primitives.Popup popup = GetTemplateChild("PART_Popup") as System.Windows.Controls.Primitives.Popup;
    if (popup != null)
    {
        PART_Popup = popup;
        PART_Popup.MouseLeftButtonDown += optionsPopup_MouseLeftButtonDown;
    }

    tb = GetTemplateChild("PART_IncrementTextBox") as TextBox;
    if (tb != null)
    {
        PART_IncrementTextBox = tb;
        PART_IncrementTextBox.KeyDown += incrementTB_KeyDown;
    }

    MenuItem mi = GetTemplateChild("PART_MenuItem") as MenuItem;
    if (mi != null)
    {
        PART_MenuItem = mi;
        PART_MenuItem.Click += MenuItem_Click;
    }
    btn = null;
    mi = null;
    tb = null;
    popup = null;
}

New code:

You can see that removing the menu and popup components reduced the code quite a bit (and note that I had to change Button to RepeatButton).
public override void OnApplyTemplate()
{
    base.OnApplyTemplate();

    RepeatButton btn = GetTemplateChild("PART_IncreaseButton") as RepeatButton;
    if (btn != null)
    {
        btn.Click += increaseBtn_Click;
    }

    btn = GetTemplateChild("PART_DecreaseButton") as RepeatButton;
    if (btn != null)
    {
        btn.Click += decreaseBtn_Click;
    }

    TextBox tb = GetTemplateChild("PART_NumericTextBox") as TextBox;
    if (tb != null)
    {
        PART_NumericTextBox = tb;
        PART_NumericTextBox.Text = Value.ToString(ValueFormat);
        PART_NumericTextBox.PreviewTextInput += numericBox_PreviewTextInput;
        PART_NumericTextBox.MouseWheel += numericBox_MouseWheel;
    }

    btn = null;
    tb = null;
}

The Sample Application

For the purposes of comparison, I put both the original control AND my version of the control into the main window of the application.

<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" 
               VerticalAlignment="Center" Grid.Row="1">
	<TextBlock Text="Original version:  " VerticalAlignment="Center" />

	<nb:NumericBox x:Name="numberBox" Background="Orange"
					Value="10.50000" ValueFormat="0.000" 
                    Increment="0.5" Minimum="-100" Maximum="100"						   
					ValueChanged="NumberBox_ValueChanged" Width="75"/>
	<Border Background="Black" BorderBrush="Red" BorderThickness="1" 
               Width="90" Margin="15,0,0,0" Padding="5,0" >
		<TextBlock Text="{Binding Path=NudValue,Mode=OneWay}" 
                      VerticalAlignment="Center" Foreground="Yellow" />
	</Border>
</StackPanel>

<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" 
               VerticalAlignment="Center" Grid.Row="2" Margin="0,20,0,0">
	<TextBlock Text="Modified version:  " VerticalAlignment="Center" />
	<nud2:IntegerUpDown x:Name="numberBox2" Increment="10" Minimum="1" 
                           Maximum="100000" 
						   ValueChanged="NumberBox2_ValueChanged" Width="75"/>
	<Border Background="Black" BorderBrush="Red" BorderThickness="1" 
               Width="90" Margin="15,0,0,0" Padding="5,0" >
		<TextBlock Text="{Binding Path=NudValue2,Mode=OneWay}" 
                      VerticalAlignment="Center" Foreground="Yellow" />
	</Border>
</StackPanel>

Update - 2021.02.25

Several problems were reported with manual text entry (and other small issues), so I completely dismantled that part of the code and re-constructed it. The main problem was that the preview input event was playing tug-of-war with the value-changed event. This cause a complete re-factoring of the InputPreview event handler, and the addition of the TextChanged event handler. I also uncoupled the ValueChanged event and the textbox.

Finally, in the process of making the changes cited above, I was compelled to add some properties to the control:

  • AllowManualEdit - A flag indicating whether the user should be able to manually edit the value. Recommendation - set the Increment to "1" if you set this property to false. Default value is true.
     
  • SilentError - A flag indicating whether typing errors cause an error sound to be played. Default value is false.
     

Behavior Modification

When manually entering text into the textbox, there are some behaviors you should be aware of:

  • Generally, typing any character that is not a number or a hyphen is an error.
     
  • If you type a hyphen that is not the first character in the textbox, it is an error.
     
  • If the edit action that you perform results in text box being completely empty, the Value you specified in the XAML will be presented in the textbox
     
  • If the text box only contains a hyphen, the Value will be invisibly set to the initial Value specified in the XAML, but will not be display in the text box until the textbox looses focus.
     
  • If you type a hyphen anywhere in the textbox that is not the first character position, it is an error.
     

Honestly? Handling manual text entry is a pain in the ass (in ANY context), and fraught with danger for the developer. I have to say that I don't know if I killed all of the bugs related to manual text entry, and until the code has been tested by several other people, you should expect to encounter minor issues. If you do encounter any issues, don't just report it in the comments below, take a stab at trying to fix it (you're a programmer, right?), and THEN report what was wrong, and what you did to fix it. I will take steps to evaluate the problem and the fix, and will update the article and the ZIP download file.

NOTE: Problems with manual text entry were the driving force behind the addition of the new AllowManualEdit property.

Changes to the code are fully commented in the IntegerUpDown.cs file.

Style Changes

In the original article, I noted that the styles used in the control created a dependency on PresentationFramework.Aero. Well, that annoyed me to the point that I took steps to eliminate the dependency - I abhor pointless dependencies. I also "fixed" the styles for the buttons so that the background and foreground colors change on mouse hover. If you don't care for the colors I've chosen, it's easy to change them to suit your own requirements by changing the color definitions in the control's Resources section in the XAML file.

Points of Interest

It should be noted that binding to your target property needs to be done differently. The required approach didn't bother me too god-awful much, so I didn't address it. When you put the control on your form, you need to handle the ValueChanged event, and use that even handler to set your target property (as opposed to binding directly to it). I'm sure there's a simple way to fix this, but I was running out of time and had to implement the control the way it was. You can check the sample app to see what I mean.

The new code requires that you add a reference to PresentationFramework.Aero, but that can be eliminated by reworking the Resources section in control.

I didn't put the control into a DLL because I figured you probably have your own WPF-related DLL with other stuff you normally use a lot. I did, however, give it its own namespace and put it in a folder to make it easier to copy into your preferred assembly.

Closing Statement

For all of the potential article authors out there, my advice is to write your code to be as ready as possible to be used in enterprise-level projects. People (like me) that are looking for a quick/easy solution to a problem almost never have time to dick around with your code, especially if they have to fix bugs that YOU could/should have found and mitigated. I understand that bugs are a necessary evil, but the manual text entry problem I encountered in the original code honestly should never have happened. Most people that download your code, will simply delete it if there's a problem, or worse, they'll hound you for fixes, or with questions, and will NEVER consider fixing the problem themselves, much less notify you when they do encounter/fix an issue. I'm not saying everybody will act this way, but a disturbing majority do, and it just makes for a bad experience for everybody.

In short, write better code, and in your article clearly state the usage criteria, and what the intent of the code is. At least that way, your readers will know exactly what to expect from the code. Nobody likes surprises.

For the article consumers that may be watching, more often than not, you gotta muscle through the code and fix/change it on your own. If you run into problems, your BEST strategy is to post a question in Questions and Answers, or in the appropriate forum. The reason is that CodeProject does not currently notify article authors when someone posts a new question in the article's comments section. Depending on the age of the article (the one cited in this article is 10 years old), the author may not be proactively monitoring the comment section, and may never even see your question.

History

  • 2021.02.25 - Took a genuine stab at fixing manuaal text entry issues, removed the dependency for PresentationFramework.Aero, and change the button styles to highlight on mouse hover. Also added two new properties (see Update section for details).
     
  • 2021.02.22 - Initial publication.
     

License

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

Share

About the Author

#realJSOP
Software Developer (Senior) Paddedwall Software
United States United States
I've been paid as a programmer since 1982 with experience in Pascal, and C++ (both self-taught), and began writing Windows programs in 1991 using Visual C++ and MFC. In the 2nd half of 2007, I started writing C# Windows Forms and ASP.Net applications, and have since done WPF, Silverlight, WCF, web services, and Windows services.

My weakest point is that my moments of clarity are too brief to hold a meaningful conversation that requires more than 30 seconds to complete. Thankfully, grunts of agreement are all that is required to conduct most discussions without committing to any particular belief system.

Comments and Discussions

 
BugUser input of "-" not possible Pin
harbor23-Feb-21 0:20
Memberharbor23-Feb-21 0:20 
GeneralRe: User input of "-" not possible Pin
#realJSOP23-Feb-21 6:22
mva#realJSOP23-Feb-21 6:22 
GeneralRe: User input of "-" not possible Pin
harbor23-Feb-21 23:15
Memberharbor23-Feb-21 23:15 
GeneralRe: User input of "-" not possible Pin
#realJSOP24-Feb-21 2:15
mva#realJSOP24-Feb-21 2:15 
GeneralRe: User input of "-" not possible Pin
#realJSOP25-Feb-21 6:45
mva#realJSOP25-Feb-21 6:45 
GeneralRe: User input of "-" not possible Pin
harbor26-Feb-21 5:05
Memberharbor26-Feb-21 5:05 
QuestionOriginal Article Cited in This Article Pin
#realJSOP22-Feb-21 4:56
mva#realJSOP22-Feb-21 4:56 

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.