question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

NumberBox with DecimalPlaces>0 configured does not allow keyboard input

See original GitHub issue

Describe the bug

I placed a NumberBox on my page as so:

<ui:NumberBox Min="0.0" Margin="0,0,0,5" PlaceholderEnabled="False" SpinButtonsEnabled="False" InputScope="CurrencyAmount" Step="0.01" Value="0" />

However there does not seem to be anyway to type number into this field, I can only make adjustments to it using the Up/Down arrowKeys (or if enable “SpinButtonsEnabled” the buttons adjust it by my Step Amount). Im not sure if there is something I am missing, however i would expect this to work similarly to how a normal Numberbox with DecimalPlaced=“0” works, but with add functionality to also accept a single decimal in the input.

*Note this may be a duplicate of #269 however that discussion seems to be more geared towards the Chinese Character issue

To Reproduce

Add a ui:NumberBox field into your page, and set the Decimal Places to 2 (default) Add another ui:NumberBox field into your page and set the Decimal Places to 0

The first ui:NumberBox will not allow you to type into it, while the second ui:NumberBox will allow number inputs

Expected behavior

ui:NumberBox with 2 Decimal Places should allow number input similar to how the input works with DecimalPlaces value set to 0, however a single decimal and the number of DecimalPlaces it is configured for should also be able to be typed in.

Screenshots

No response

OS version

Windows 11

.NET version

.NET 6.0

WPF-UI NuGet version

2.0.3

Additional context

This also seems to be true when running the Demo. If you go into the Input section, then scroll to the NumberBox section, and look at the decimal example, you cannot type anything into that field, you may only use arrowKeys or the SpinButtons

Issue Analytics

  • State:closed
  • Created 9 months ago
  • Reactions:1
  • Comments:5

github_iconTop GitHub Comments

1reaction
Dealmancommented, May 25, 2023

I haven’t tried this extensively, but seems to get the job done for the most part if you need to use decimals. It’s a bit lengthy. Make sure you set IsReadOnly and IsReadOnlyCaretVisible to True. This is necessary as it works as a workaround to disable the original behaviour.

Probably some weird edge-cases still in there, but you should be able to add numbers via either digits or numpad as well as remove with backspace, delete and by selecting text.

private void NumberBox_PreviewKeyUp(object sender, KeyEventArgs e)
{
	// Important; IsReadOnly & IsReadOnlyCaretVisible set to True!

	var numBox = ((NumberBox)sender);
	if (numBox is null || !numBox.IsLoaded)
		return;

	bool insertToggled = (e.KeyboardDevice.GetKeyStates(Key.Insert) == KeyStates.Toggled);

	// Handle Delete
	if (e.Key == Key.Delete)
	{
		int caretPos = numBox.CaretIndex;
		int delimiterPos = Array.IndexOf(numBox.Text.ToCharArray(), '.');
		int selectionLength = numBox.SelectionLength; // This is reset after manipulating the text it seems like, so we save it

		if (caretPos == numBox.Text.Length)
			return;

		if (caretPos == delimiterPos && selectionLength == 0)
		{
			numBox.CaretIndex++;
			caretPos = numBox.CaretIndex;
		}
		else if (numBox.SelectedText.Contains("."))
		{
			string[] leftAndRight = numBox.SelectedText.Split('.');
			int leftCount = leftAndRight[0].Length;
			int rightCount = leftAndRight[1].Length;
			numBox.Text = numBox.Text.Remove(caretPos, leftCount);
			int newDelimiterPos = Array.IndexOf(numBox.Text.ToCharArray(), '.');
			numBox.Text = numBox.Text.Remove(newDelimiterPos + 1, rightCount);
			e.Handled = true;
			return;
		}

		if (selectionLength > 0)
		{
			numBox.Text = numBox.Text.Remove(caretPos, selectionLength);
			numBox.CaretIndex = caretPos;
		}
		else
		{
			numBox.Text = numBox.Text.Remove(caretPos, 1);
			numBox.CaretIndex = caretPos;
		}
	}

	// Handle Backspace(Remove)
	if (e.Key == Key.Back)
	{
		int caretPos = numBox.CaretIndex;
		int delimiterPos = Array.IndexOf(numBox.Text.ToCharArray(), '.');
		int selectionLength = numBox.SelectionLength;

		if (caretPos == 0 && numBox.SelectionLength == 0)
			return;

		// Caret to the right of the delimiter, move it left one step
		if (caretPos - 1 == delimiterPos && selectionLength == 0)
		{
			numBox.CaretIndex--;
			caretPos = numBox.CaretIndex;
		}
		else if (numBox.SelectedText.Contains("."))
		{
			string[] leftAndRight = numBox.SelectedText.Split('.');
			int leftCount = leftAndRight[0].Length;
			int rightCount = leftAndRight[1].Length;
			numBox.Text = numBox.Text.Remove(caretPos, leftCount);
			int newDelimiterPos = Array.IndexOf(numBox.Text.ToCharArray(), '.');
			numBox.Text = numBox.Text.Remove(newDelimiterPos + 1, rightCount);
			e.Handled = true;
			return;
		}

		if (selectionLength > 0)
		{
			numBox.Text = numBox.Text.Remove(Math.Max(caretPos, 0), Math.Max(selectionLength, 1));
			numBox.CaretIndex = caretPos;
		}
		else
		{
			numBox.Text = numBox.Text.Remove(Math.Max(caretPos - 1, 0), Math.Max(selectionLength, 1));
			numBox.CaretIndex = caretPos - 1;
		}

		e.Handled = true;
		return;   
	}

	// Handle Digit Keys
	if (e.Key >= Key.D0 && e.Key <= Key.D9)
	{
		int caretPos = numBox.CaretIndex;
		string value = ((int)e.Key - (int)Key.D0).ToString();
		if (insertToggled)
		{
			if (numBox.Text[caretPos] == '.' || numBox.Text[caretPos] == ',')
			{
				numBox.Text = numBox.Text.Insert(Math.Clamp(caretPos + 1, 0, numBox.Text.Length), value);
				numBox.CaretIndex = caretPos + 2;
			}
			else
			{
				numBox.Text = numBox.Text.Remove(caretPos, 1);
				numBox.Text = numBox.Text.Insert(Math.Clamp(caretPos, 0, numBox.Text.Length), value);
				numBox.CaretIndex = caretPos + 1;
			}
			
			e.Handled = true;
			return;
		}
		numBox.Text = numBox.Text.Insert(caretPos, value);
		numBox.CaretIndex = caretPos + 1;
		e.Handled = true;
	}

	// Handle Numpad Keys
	if (e.Key >= Key.NumPad0 && e.Key <= Key.NumPad9)
	{
		int caretPos = numBox.CaretIndex;
		string value = ((int)e.Key - (int)Key.NumPad0).ToString();
		if (insertToggled)
		{
			if (numBox.Text[caretPos] == '.' || numBox.Text[caretPos] == ',')
			{
				numBox.Text = numBox.Text.Insert(Math.Clamp(caretPos + 1, 0, numBox.Text.Length), value);
				numBox.CaretIndex = caretPos + 2;
			}
			else
			{
				numBox.Text = numBox.Text.Remove(caretPos, 1);
				numBox.Text = numBox.Text.Insert(Math.Clamp(caretPos, 0, numBox.Text.Length), value);
				numBox.CaretIndex = caretPos + 1;
			}

			e.Handled = true;
			return;
		}
		numBox.Text = numBox.Text.Insert(caretPos, value);
		numBox.CaretIndex = caretPos + 1;
		e.Handled = true;
	}
}
1reaction
mikedmorcommented, Dec 8, 2022

I found a workaround to achieve the results i was looking for. It does not involve WPF UI however.

  1. Add a new Helper Class named Masking.cs
  2. Fill with the following (note you may need to wrap it in your namespace)
using System;
using System.Text.RegularExpressions;
using System.Windows.Input;
using System.Windows;
using System.Windows.Controls;

    /// <summary>
    /// Provides masking behavior for any <see cref="TextBox"/>.
    /// </summary>
    public static class Masking
    {
        private static readonly DependencyPropertyKey _maskExpressionPropertyKey = DependencyProperty.RegisterAttachedReadOnly("MaskExpression",
                typeof(Regex),
                typeof(Masking),
                new FrameworkPropertyMetadata());

        /// <summary>
        /// Identifies the <see cref="Mask"/> dependency property.
        /// </summary>
        public static readonly DependencyProperty MaskProperty = DependencyProperty.RegisterAttached("Mask",
                typeof(string),
                typeof(Masking),
                new FrameworkPropertyMetadata(OnMaskChanged));

        /// <summary>
        /// Identifies the <see cref="MaskExpression"/> dependency property.
        /// </summary>
        public static readonly DependencyProperty MaskExpressionProperty = _maskExpressionPropertyKey.DependencyProperty;

        /// <summary>
        /// Gets the mask for a given <see cref="TextBox"/>.
        /// </summary>
        /// <param name="textBox">
        /// The <see cref="TextBox"/> whose mask is to be retrieved.
        /// </param>
        /// <returns>
        /// The mask, or <see langword="null"/> if no mask has been set.
        /// </returns>
        public static string GetMask(TextBox textBox)
        {
            if (textBox == null)
            {
                throw new ArgumentNullException("textBox");
            }

            return textBox.GetValue(MaskProperty) as string;
        }

        /// <summary>
        /// Sets the mask for a given <see cref="TextBox"/>.
        /// </summary>
        /// <param name="textBox">
        /// The <see cref="TextBox"/> whose mask is to be set.
        /// </param>
        /// <param name="mask">
        /// The mask to set, or <see langword="null"/> to remove any existing mask from <paramref name="textBox"/>.
        /// </param>
        public static void SetMask(TextBox textBox, string mask)
        {
            if (textBox == null)
            {
                throw new ArgumentNullException("textBox");
            }

            textBox.SetValue(MaskProperty, mask);
        }

        /// <summary>
        /// Gets the mask expression for the <see cref="TextBox"/>.
        /// </summary>
        /// <remarks>
        /// This method can be used to retrieve the actual <see cref="Regex"/> instance created as a result of setting the mask on a <see cref="TextBox"/>.
        /// </remarks>
        /// <param name="textBox">
        /// The <see cref="TextBox"/> whose mask expression is to be retrieved.
        /// </param>
        /// <returns>
        /// The mask expression as an instance of <see cref="Regex"/>, or <see langword="null"/> if no mask has been applied to <paramref name="textBox"/>.
        /// </returns>
        public static Regex GetMaskExpression(TextBox textBox)
        {
            if (textBox == null)
            {
                throw new ArgumentNullException("textBox");
            }

            return textBox.GetValue(MaskExpressionProperty) as Regex;
        }

        private static void SetMaskExpression(TextBox textBox, Regex regex)
        {
            textBox.SetValue(_maskExpressionPropertyKey, regex);
        }

        private static void OnMaskChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
        {
            var textBox = dependencyObject as TextBox;
            var mask = e.NewValue as string;
            textBox.PreviewTextInput -= textBox_PreviewTextInput;
            textBox.PreviewKeyDown -= textBox_PreviewKeyDown;
            DataObject.RemovePastingHandler(textBox, Pasting);
            DataObject.RemoveCopyingHandler(textBox, NoDragCopy);
            CommandManager.RemovePreviewExecutedHandler(textBox, NoCutting);


            if (mask == null)
            {
                textBox.ClearValue(MaskProperty);
                textBox.ClearValue(MaskExpressionProperty);
            }
            else
            {
                textBox.SetValue(MaskProperty, mask);
                SetMaskExpression(textBox, new Regex(mask, RegexOptions.Compiled | RegexOptions.IgnorePatternWhitespace));
                textBox.PreviewTextInput += textBox_PreviewTextInput;
                textBox.PreviewKeyDown += textBox_PreviewKeyDown;
                DataObject.AddPastingHandler(textBox, Pasting);
                DataObject.AddCopyingHandler(textBox, NoDragCopy);
                CommandManager.AddPreviewExecutedHandler(textBox, NoCutting);
            }
        }

        private static void NoCutting(object sender, ExecutedRoutedEventArgs e)
        {
            if (e.Command == ApplicationCommands.Cut)
            {
                e.Handled = true;
            }
        }

        private static void NoDragCopy(object sender, DataObjectCopyingEventArgs e)
        {
            if (e.IsDragDrop)
            {
                e.CancelCommand();
            }
        }

        private static void textBox_PreviewTextInput(object sender, TextCompositionEventArgs e)
        {
            var textBox = sender as TextBox;
            //MessageBox.Show("TextInput: " + textBox.Text);
            var maskExpression = GetMaskExpression(textBox);
            if (maskExpression == null)
            {
                return;
            }

            var proposedText = GetProposedText(textBox, e.Text);

            if (!maskExpression.IsMatch(proposedText))
            {
                e.Handled = true;
            }
        }

        private static void textBox_PreviewKeyDown(object sender, KeyEventArgs e)
        {
            var textBox = sender as TextBox;
            //MessageBox.Show("KeyDown: " + textBox.Text);
            var maskExpression = GetMaskExpression(textBox);

            if (maskExpression == null)
            {
                return;
            }

            string proposedText = null;

            //pressing space doesn't raise PreviewTextInput, reasons here http://social.msdn.microsoft.com/Forums/en-US/wpf/thread/446ec083-04c8-43f2-89dc-1e2521a31f6b?prof=required
            if (e.Key == Key.Space)
            {
                proposedText = GetProposedText(textBox, " ");
            }
            // Same story with backspace
            else if (e.Key == Key.Back)
            {
                proposedText = GetProposedTextBackspace(textBox);
            }

            if (proposedText != null && proposedText != string.Empty && !maskExpression.IsMatch(proposedText))
            {
                e.Handled = true;
            }

        }

        private static void Pasting(object sender, DataObjectPastingEventArgs e)
        {
            var textBox = sender as TextBox;
            var maskExpression = GetMaskExpression(textBox);

            if (maskExpression == null)
            {
                return;
            }

            if (e.DataObject.GetDataPresent(typeof(string)))
            {
                var pastedText = e.DataObject.GetData(typeof(string)) as string;
                var proposedText = GetProposedText(textBox, pastedText);

                if (!maskExpression.IsMatch(proposedText))
                {
                    e.CancelCommand();
                }
            }
            else
            {
                e.CancelCommand();
            }
        }

        private static string GetProposedTextBackspace(TextBox textBox)
        {
            var text = GetTextWithSelectionRemoved(textBox);
            if (textBox.SelectionStart > 0 && textBox.SelectionLength == 0)
            {
                text = text.Remove(textBox.SelectionStart - 1, 1);
            }

            return text;
        }


        private static string GetProposedText(TextBox textBox, string newText)
        {
            var text = GetTextWithSelectionRemoved(textBox);
            text = text.Insert(textBox.CaretIndex, newText);

            return text;
        }

        private static string GetTextWithSelectionRemoved(TextBox textBox)
        {
            var text = textBox.Text;

            if (textBox.SelectionStart != -1)
            {
                text = text.Remove(textBox.SelectionStart, textBox.SelectionLength);
            }
            return text;
        }
    }
  1. Add a normal UI:Textbox to your xaml
<ui:TextBox x:Name="AddExpense_Cost" />
  1. Use as follows:
Masking.SetMask(AddExpense_Cost, "^\\$?\\-?([1-9]{1}[0-9]{0,2}(\\,\\d{3})*(\\.\\d{0,2})?|[1-9]{1}\\d{0,}(\\.\\d{0,2})?|0(\\.\\d{0,2})?|(\\.\\d{1,2}))$|^\\-?\\$?([1-9]{1}\\d{0,2}(\\,\\d{3})*(\\.\\d{0,2})?|[1-9]{1}\\d{0,}(\\.\\d{0,2})?|0(\\.\\d{0,2})?|(\\.\\d{1,2}))$|^\\(\\$?([1-9]{1}\\d{0,2}(\\,\\d{3})*(\\.\\d{0,2})?|[1-9]{1}\\d{0,}(\\.\\d{0,2})?|0(\\.\\d{0,2})?|(\\.\\d{1,2}))\\)$");

Note: Dont forget to add the Using statement to your helper class, Intellisense should help you with that if you do not know how.

Im still a noob at all this WPF stuff so there may be an easier way to accomplish this specifically, however this is how i managed to get what i needed. Source of this class can be found on StackOverflow

Read more comments on GitHub >

github_iconTop Results From Across the Web

NumberBox - The decimal point is not accepted in non- ...
Use case : entering a decimal number using the numerical keyboard, with a locale using comma as separator. In your demo, when the...
Read more >
can't input decimals into numberbox (2.0.1) · Issue #269
Describe the bug Can't input decimals into numberbox neither in demo ... NumberBox with DecimalPlaces>0 configured does not allow keyboard ...
Read more >
Number box - Windows apps
Number box is a control that can be used to display and edit numbers. ... that NumberBox will accept non-numeric characters through Text...
Read more >
Introducing the New WinUI NumberBox Control
By default, the WinUI NumberBox allows you to enter any kind of numerical input, but does not allow other inputs like alphabetic characters....
Read more >
[controlP5] numberBox typing in.
Hi, Is it possible to type into controlP5 numberBox? I think it's totally normal behaviour for standard number boxes (focus mouse on, ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found