Numpad’s decimal-point correction for WPF

numpadJust a quick-and-dirty solution for solving the tedious problem of the numpad’s decimal-point insertion where the culture require something different than a dot “.”.

The problem.

Many of you, who aren’t using a dot as decimal separator, maybe noticed that the character issued by the numeric-pad is always a doe, regardless the OS settings. In Italy, for instance, we’re using the comma as decimal separator.
You know, if you type the wrong character, the number won’t be recognized as valid (sometimes even worse, because it’s mistaken as valid).
If you try to open Notepad or any raw-input application, you’ll notice that there’s no way to “hack” the Windows settings in order to correctly input the numeric-pad “point” as a comma. By the way, if you enter a number in Microsoft Excel or so, the character is actually a comma.
Looks like the translation is something managed by the application.

It’s not so simple, though.
Imagine to write your own application (WPF in my case), and have a series of textboxes. Whereas a textbox used for entering a number (e.g. most physical units) would be fine having a “translation” to a comma, when another textbox used for an IP-pattern, clearly should *NOT* be translated any time.
Looks like that some countries use a different punctuation for generic numbers and for currency: my “neighbor” friends of Switzerland do use the comma for any number but currency, where the dot is preferred.

The solution.

Here is a solution, but I believe is difficult to satisfy all the developers’ habits. I just opted for a simple attached-property, as “behavior” to any TextBoxBase object, which “intercepts” the Decimal key (the numpad’s DP) and replaces it with the proper one.

namespace DecimalPointCorrectorDemo
{
    public enum DecimalPointCorrectionMode
    {
        /// <summary>
        /// (Default) No correction is applied, and any style
        /// inherited setting may influence the correction behavior.
        /// </summary>
        Inherits,

        /// <summary>
        /// Enable the decimal-point correction for generic numbers.
        /// </summary>
        Number,

        /// <summary>
        /// Enable the decimal-point correction for currency numbers.
        /// </summary>
        Currency,

        /// <summary>
        /// Enable the decimal-point correction for percent-numbers.
        /// </summary>
        Percent,
    }


    /// <summary>
    /// General purpose container for <see cref="System.Windows.Controls.TextBox"/> helpers.
    /// </summary>
    public static class TextBoxHelper
    {

        #region DPA DecimalPointCorrection

        public static readonly DependencyProperty DecimalPointCorrectionProperty = DependencyProperty.RegisterAttached(
            "DecimalPointCorrection",
            typeof(DecimalPointCorrectionMode),
            typeof(TextBoxHelper),
            new UIPropertyMetadata(
                default(DecimalPointCorrectionMode),
                DecimalPointCorrectionChanged
                ));


        public static DecimalPointCorrectionMode GetDecimalPointCorrection(TextBoxBase obj)
        {
            return (DecimalPointCorrectionMode)obj.GetValue(DecimalPointCorrectionProperty);
        }


        public static void SetDecimalPointCorrection(TextBoxBase obj, DecimalPointCorrectionMode value)
        {
            obj.SetValue(DecimalPointCorrectionProperty, value);
        }

        #endregion


        private static void DecimalPointCorrectionChanged(
            object sender,
            DependencyPropertyChangedEventArgs args
            )
        {
            var tbox = (TextBoxBase)sender;

            //remove any existent event subscription
            switch ((DecimalPointCorrectionMode)args.OldValue)
            {
                case DecimalPointCorrectionMode.Number:
                case DecimalPointCorrectionMode.Currency:
                case DecimalPointCorrectionMode.Percent:
                    tbox.PreviewKeyDown -= tbox_PreviewKeyDown;
                    break;
            }

            //subscribe the event handler, whereas necessary
            switch ((DecimalPointCorrectionMode)args.NewValue)
            {
                case DecimalPointCorrectionMode.Number:
                case DecimalPointCorrectionMode.Currency:
                case DecimalPointCorrectionMode.Percent:
                    tbox.PreviewKeyDown += tbox_PreviewKeyDown;
                    break;
            }
        }


        /// <summary>
        /// 
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        static void tbox_PreviewKeyDown(object sender, KeyEventArgs e)
        {
            //filter the numpad's decimal-point key only
            if (e.Key == System.Windows.Input.Key.Decimal)
            {
                //mark the event as handled, so no further action will take place
                e.Handled = true;

                //grab the originating texbox control...
                var tbox = (TextBoxBase)sender;

                //the current correction mode...
                var mode = TextBoxHelper.GetDecimalPointCorrection(tbox);

                //and the culture of the thread involved (UI)
                var culture = Thread.CurrentThread.CurrentCulture;

                //surrogate the blocked key pressed
                SimulateDecimalPointKeyPress(
                    tbox, 
                    mode, 
                    culture
                    );
            }
        }


        /// <summary>
        /// Insertion of the proper decimal-point as part of the textbox content
        /// </summary>
        /// <param name="tbox"></param>
        /// <param name="mode"></param>
        /// <param name="culture"></param>
        /// <remarks>
        /// Typical "async-void" pattern as "fire-and-forget" behavior.
        /// </remarks>
        private static async void SimulateDecimalPointKeyPress(
            TextBoxBase tbox,
            DecimalPointCorrectionMode mode,
            CultureInfo culture
            )
        {
            //select the proper decimal-point string upon the context
            string replace;
            switch (mode)
            {
                case DecimalPointCorrectionMode.Number:
                    replace = culture.NumberFormat.NumberDecimalSeparator;
                    break;

                case DecimalPointCorrectionMode.Currency:
                    replace = culture.NumberFormat.CurrencyDecimalSeparator;
                    break;

                case DecimalPointCorrectionMode.Percent:
                    replace = culture.NumberFormat.PercentDecimalSeparator;
                    break;

                default:
                    replace = null;
                    break;
            }

            if (string.IsNullOrEmpty(replace) == false)
            {
                //insert the desired string
                var tc = new TextComposition(
                    InputManager.Current,
                    tbox,
                    replace
                    );

                TextCompositionManager.StartComposition(tc);
            }

            await Task.FromResult(false);
        }

    }
}

The code is rather simple, so I think would be useless chatting more.
The only worthwhile point is regarding the “async” pattern in the key-replace function. I just wanted to leave the originating event (PreviewKeyDown) a bit of time to finish before adding another (possible) event. Honestly, I don’t know whether that’s really necessary: the async-await pattern comes easy and reliable, so I prefer to keep the code safer. Feel free to improve the it.

correction-off

correction-on

The complete demo solution source code can be downloaded here.

This code has been tested widely enough, including on the Windows 8 on-screen touch-keyboard.
Enjoy!

Leave a comment