Nov 15, 2008

NumericUpDown Control in Silverlight Toolkit

Introduction

I wrote the NumericUpDown control image in Silverlight Toolkit that was released to CodePlex three weeks ago. There are NumericUpDown or similar controls on Win32, WinForm and other platforms, but it is new to WPF and Silverlight. We went through rounds of designs to make sure its interface and implementation are simple, extensible and reusable. Some of the design benefits may not be obvious, so it may be helpful to write a post about NumericUpDown and its related types in Microsoft.Windows.Controls.Input assembly.

Overview

The NumericUpDown control exposes following properties:

  • Minimum, Maximum and Value of double type: Minimum and Value have default value 0, Maximum defaults to 100. NumericUpDown ensures that Minimum <= Value <= Maximum, regardless of how those properties are set (via xaml, code, or typed in by user) or in what order.
  • DecimalPlaces of int type: it decides how many digits after decimal point are displayed. It must be a value between 0 and 15, and default to 0.
  • IsEditable of bool type: if true, user can type in a value directly into the text box of the NumericUpDown control; if false, user can still change the value by clicking the up or down repeat button, or selecting the text box and then hitting up or down arrow key, or via code.
  • Increment of double type: it decides by how much Value will change each time when user clicks the up/down button or hits up/down arrow key. It must be a positive double value.

NumericUpDown fires three events, usually in the following order:

  1. ParseError event of type EventHandler<UpDownParseErrorEventArgs>. The event handler has a parameter e of type UpDownParseErrorEventArgs: UpDownParseErrorEventArgs
    e.Text is the string user typed in, and e.Error is the exception thrown while trying to parse e.Text into a double value. This event is fired when user typed in an invalid string in the control's text box. The ParseError event gives client code a chance to do its own error handling. If the event handler handles the event , it should set e.Handled to true. If the event is not handled, NumericUpDown by default will discard user input and refresh the text box to display previous value. Either way, when ParseError event fires, the following two events usually will not be fired.
  2. ValueChanging event of type RoutedPropertyChangingEventHandler<double> type. The event handler takes a parameter e of type RoutedPropertyChangingEventArgs<double>:
    RoutedPropertyChangingEventArgs<T>
    ValueChanging event is fired when the control's Value property is about to change from e.OldValue to e.NewValue. This event gives client code a chance to modify or cancel the event. If the event handler modifies e.NewValue, the the following ValueChanged event will be fired with the modified e.NewValue. If e.IsCancelable is true, the event handler can cancel the value changing event all together by setting e.Cancel to true, then the NumericUpDown control's Value property will not change, and no ValueChanged event will be fired.
  3. ValueChanged event of type RoutedPropertyChangedEventHandler<double> type. The event handler takes a parameter e of type RoutedPropertyChangedEventArgs<double>:
    RoutedPropertyChangedEventArgs<T>
    ValueChanged event is fired when the previous ValueChanging event is not canceled and the control's Value property has changed from e.OldValue to e.NewValue. This event gives client code a chance to respond to the value change.

NumericUpDown has the following control contract:

NumericUpDown Control Contract

It expects two template parts:

  • A TextBox named "Text", which allows user to type in a value directly;
  • A Spinner named "Spinner", which allows user to pick up a value from a range instead of typing it in directly. In default template, it is a ButtonSpinner control with the up and down repeat buttons.

It also has a StyleTypeProperty named "SpinnerStyle" that allows NumericUpDown control visual to be customized without requiring re-templating.

Design & Implementation

Below is class diagram of NumericUpDown and its related types:

Class Diagram of NumericUpDown & Co.

  • The class hierarchy of UpDownBase <- UpDownBase<T> <- NumericUpDown is designed to:
    • reuse control template/visual among similar UpDown controls like DomainUpDown, DateTimeUpDown etc.
    • reuse common logic among UpDown controls
  • The non generic UpDownBase class implements simulated covariance among all UpDown controls.
  • The generic UpDownBase<T> implements:
    • the control contract: the Text and Spinner template parts, common and focused visual state groups, and the SpinnerStyle property.
    • the Value property.
    • the ParseError, ValueChanging and ValueChanged events.
    • the ApplyValue, ParseValue and FormatValue value input and output protocol.
  • The NumericUpDown class implements NumericUpDown specific semantics, like the Minimum and Maximum properties and the Minimum <= Value <= Maximum coercion logic; the DecimalPlaces property and display logic etc.
  • The Spinner class is an abstraction over the up/down buttons, to provide support for more flexible UI paradigms and controls.
  • The ValueChanging event is an attempt to provide WPF like preview events.

Scenarios

Basic Scenario

The most basic scenario of NumericUpDown control is to use it to allow user to type or pick a value from a range. For example:

    <StackPanel Orientation="Horizontal">
        <TextBlock Text="Age: "/>
        <input:NumericUpDown x:Name="age" Width="100"/>
    </StackPanel>
Customize Visual
NumericTextBox: NumericUpDown Without Spinner

It may be common to use NumericUpDown without the Spinner (the up and down buttons) as a special TextBox to input a number. The beauty of WPF/Silverlight control design is that control behavior and UI are separated. A NumericUpDown instance without the spinner is still a fully functional NumericUpDown:

  • All properties like Minimum, Maximum, Value, IsEditable etc are still there, can be set via xaml or code, and function as expected.
  • Coercion logic to enforce Minimum <= Value <= Maximum still functions as expected.
  • All events (ParseError, ValueChanging, ValueChanged) still fires as expected.
  • User can still use up/down arrow keys to increment/decrement Value.

There are two easy ways to implement this:

  • Change SpinnerStyle StyleTypedProperty, like the following xaml hides the spinner:
    <StackPanel Orientation="Horizontal">
        <StackPanel.Resources>
            <Style x:Key="HideSpinner" TargetType="input:Spinner">
                <Setter Property="Visibility" Value="Collapsed"/>
            </Style>
        </StackPanel.Resources>
        <TextBlock Text="Age: "/>
        <input:NumericUpDown x:Name="age" Width="100"
            SpinnerStyle="{StaticResource HideSpinner}"/>
    </StackPanel>
  • Re-template: as shown below, it is pretty easy to use Blend to remove the spinner from NumericUpDown's default template:

Remove Spinner via templating

Remove Spinner via templating

Customize Layout

NumericUpDown uses a ButtonSpinner, and ButtonSpinner has Content property that is marked with [ContentProperty("Content")] attribute. This is a very elaborate design to enable NumericUpDown's like those:

NumericUpDown Top/Down Layout NumericUpDown Left/Right Layout

The trick is to make the NumericUpDown's TextBox the ButtonSpinner's Content. This is actually rather easy to do with Blend:

  • First, edit the NumericUpDown's template, drag and drop the Text TextBox element to be inside the Spinner element, thus making it the Spinner's Content property. Below screenshot is for NudTopDown template:

Edit NumericUpDown template, move Text inside Spinner.

  • then edit the Spinner's template to fine turn the layout of the two RepeatButton's relative to the TextBox. Below screen shot is for the NudLeftRight template, where we need to make StackPanel's Orientation Horizontal, and make RepeatButton's VerticalAlignment Center and HorizontalAlignment Left or Right.

NumericUpDown Left/Right Layout

Customize Behavior

We can easily add snap behavior to NumericUpDown: make the Value always snap to the next integral multiple of Increment. This can be done easily by handling ValueChanging event. We can either provide an event handler for ValueChanging event, or override OnValueChanging method, to modify the new value to be the next integral multiple of Increment. The coding is trivial, so I don't provide the code here.

Conclusion

This post so far has only focused on NumericUpDown, but our design actually allows customization, extension and reuse of all the classes shown in above class diagrams, like Spinner, ButtonSpinner, UpDownBase<T> etc. We may provide DomainUpDown, DateUpDown etc classes in future releases of the Silverlight Toolkit, which may further demonstrate the power of the design for NumericUpDown.

As always, feedbacks (comments, suggestion, corrections etc) are welcome!