XAML NumberBox not accepting letters

By Fons Sonnemans, posted on
1196 Views

Last week there was a discussion on X (former Twitter) about the usage of the NumberBox in WinUI apps. Should it accept letter inputs or not. The 'conlusion' was that it should because you can enter formulas. But there are also a few developers which don't like this. So I took up the challenge to find a solution for this problem. My first guess was that I should create an AttachedProperty or Behavior for this. I was wrong. AttachedProperties can't be used for this. You can't use them for handing events on controls. Behaviors can but they require you to add an extra NuGet package to the project. Something you might not want. So I decided to use compiled binding (x:Bind) to events, also known as Event Binding.

My Solution

WinUI and UWP NumberBox screenshot

There are a few 'Key' events you can use: KeyDown, KeyUp, PreviewKeyDown and PreviewKeyUp. All events use the KeyEventHandler delegate but it is the PreviewKeyDown you should use. It allows you to set the Handled property of the KeyRoutedEventArgs to true. This will cancel the input.

I'm using Event Binding instead of the more common solution in which you handle events in the code-behind of the Window, Page or UserControl. This makes my solution reusable which I always prefer. All code is just in a single class. This PreviewKeyDownHandler class has 3 public static methods to which you can bind to. Use the one you think works best for you.

  • RejectLetters, don't accept letters A to Z
  • AcceptNumbersOnly, only support numbers but off course also cursor & tab keys
  • AcceptNumbersAndOperators, use this when you have AcceptsExpression turned on.

This class is written using C# 9.0 so make sure you upgrade to this version in UWP project, set LangVersion to 9.0 or higher in your project file. I use conditional compilation for importing the correct namespaces and the GetKeyState function which are different in WinUI. I use the IsModifierKeyDown() method to handle special cases like a Ctrl+C or a Windows+P. Those should not be cancelled.

#if WINDOWS_UWP
using Windows.UI.Xaml.Input;
#else
using Microsoft.UI.Xaml.Input;
#endif
using System;
using Windows.System;
using Windows.UI.Core;

namespace Helpers {

    /// <summary>
    /// This helper class is used to avoid letter inputs for a NumberBox. 
    /// Make sure you upgrade your UWP project to at least C# 9.0, we use switch expressions and 
    /// logical pattern matching.
    /// <code>
    /// <NumberBox PreviewKeyDown="{x:Bind helpers:PreviewKeyDownHandler.AcceptNumbersOnly}" />
    /// </code>
    /// </summary>
    static class PreviewKeyDownHandler {

#if WINDOWS_UWP
        private static readonly Func<VirtualKey, CoreVirtualKeyStates> _keyStateFunction = CoreWindow.GetForCurrentThread().GetKeyState;
#else
        private static readonly Func<VirtualKey, CoreVirtualKeyStates> _keyStateFunction = Microsoft.UI.Input.InputKeyboardSource.GetKeyStateForCurrentThread;
#endif

        private static bool IsModifierKeyDown() {
            return _keyStateFunction(VirtualKey.Control).HasFlag(CoreVirtualKeyStates.Down) ||
                   _keyStateFunction(VirtualKey.LeftMenu).HasFlag(CoreVirtualKeyStates.Down) ||
                   _keyStateFunction(VirtualKey.RightMenu).HasFlag(CoreVirtualKeyStates.Down) ||
                   _keyStateFunction(VirtualKey.LeftWindows).HasFlag(CoreVirtualKeyStates.Down) ||
                   _keyStateFunction(VirtualKey.RightWindows).HasFlag(CoreVirtualKeyStates.Down);
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Must match KeyEventHandler delegate, don't remove sender")]
        public static void RejectLetters(object sender, KeyRoutedEventArgs e) {
            if (IsModifierKeyDown()) {
                return;
            }
            if (e.Key is >= VirtualKey.A and <= VirtualKey.Z) {
                e.Handled = true;
            }
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Must match KeyEventHandler delegate, don't remove sender")]
        public static void AcceptNumbersOnly(object sender, KeyRoutedEventArgs e) {
            if (IsModifierKeyDown()) {
                return;
            }

            e.Handled = e.Key switch {
                >= VirtualKey.Number0 and <= VirtualKey.Number9 => false,
                >= VirtualKey.NumberPad0 and <= VirtualKey.NumberPad9 => false,
                VirtualKey.Left => false,
                VirtualKey.Right => false,
                VirtualKey.Up => false,
                VirtualKey.Down => false,
                VirtualKey.PageUp => false,
                VirtualKey.PageDown => false,
                VirtualKey.Tab => false,
                VirtualKey.Home => false,
                VirtualKey.End => false,
                VirtualKey.Shift => false,
                VirtualKey.Back => false,
                VirtualKey.Delete => false,
                VirtualKey.Enter => false,
                VirtualKey.Decimal => false,
                (VirtualKey)188 => false, // Comma
                (VirtualKey)189 => false, // Minus
                (VirtualKey)190 => false, // Dot
                _ => true,
            };
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Must match KeyEventHandler delegate, don't remove sender")]
        public static void AcceptNumbersAndOperators(object sender, KeyRoutedEventArgs e) {
            if (IsModifierKeyDown()) {
                return;
            }

            AcceptNumbersOnly(sender, e);
            if (e.Handled) {
                e.Handled = e.Key switch {
                    VirtualKey.Multiply => false,
                    VirtualKey.Divide => false,
                    VirtualKey.Add => false,
                    VirtualKey.Subtract => false,
                    VirtualKey.Space => false,
                    (VirtualKey)187 => false, // +
                    _ => true,
                };
            }
        }
    }
}

WinUI3 usage

In a WinUI app you can use it like this. The x:Bind in the PreviewKeyDown event is all you need. You can set this on a NumberBox but also on a Panel which contain multiple NumberBox controls. This works becauses the PreviewKeyDown is a Routing Event which bubbles up to it's parent.

<?xml version="1.0" encoding="utf-8" ?>
<Window x:Class="WinUI3App1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:helpers="using:Helpers"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d">

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <StackPanel Width="208"
                    HorizontalAlignment="Center"
                    VerticalAlignment="Center"
                    Spacing="8">

            <NumberBox Header="A"
                       PreviewKeyDown="{x:Bind helpers:PreviewKeyDownHandler.RejectLetters}"
                       SpinButtonPlacementMode="Inline" />

            <NumberBox AcceptsExpression="True"
                       Header="B"
                       PreviewKeyDown="{x:Bind helpers:PreviewKeyDownHandler.AcceptNumbersAndOperators}" />

            <StackPanel Orientation="Horizontal"
                        PreviewKeyDown="{x:Bind helpers:PreviewKeyDownHandler.AcceptNumbersOnly}"
                        Spacing="8">
                <NumberBox Width="100"
                           Header="C" />
                <NumberBox Width="100"
                           Header="D" />
            </StackPanel>
        </StackPanel>
    </Grid>
</Window>

UWP usage

In a UWP app using WinUI2 you use it like this. The only real difference with WinUI 3 is the prefix of the winui: in front of the NumberBox. It refers to xmlns:winui namespace declaration in the Page element.

<Page x:Class="UwpApp1.MainPage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      xmlns:helpers="using:Helpers"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      xmlns:winui="using:Microsoft.UI.Xaml.Controls"
      Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
      mc:Ignorable="d">

    <StackPanel Width="208"
                HorizontalAlignment="Center"
                VerticalAlignment="Center"
                Spacing="8">

        <winui:NumberBox Header="A"
                         PreviewKeyDown="{x:Bind helpers:PreviewKeyDownHandler.RejectLetters}"
                         SpinButtonPlacementMode="Inline" />

        <winui:NumberBox AcceptsExpression="True"
                         Header="B"
                         PreviewKeyDown="{x:Bind helpers:PreviewKeyDownHandler.AcceptNumbersAndOperators}" />

        <StackPanel Orientation="Horizontal"
                    PreviewKeyDown="{x:Bind helpers:PreviewKeyDownHandler.AcceptNumbersOnly}"
                    Spacing="8">
            <winui:NumberBox Width="100"
                             Header="C" />
            <winui:NumberBox Width="100"
                             Header="D" />
        </StackPanel>
    </StackPanel>
</Page>

Closure

I hope you like this solution. I have published this solution containing the WinUI and UWP project in this GitHub repository

All postings/content on this blog are provided "AS IS" with no warranties, and confer no rights. All entries in this blog are my opinion and don't necessarily reflect the opinion of my employer or sponsors. The content on this site is licensed under a Creative Commons Attribution By license.

Leave a comment

Blog comments

0 responses