Search code examples
xamlwinui-3winuiwindows-app-sdkwinui-xaml

How to Use TextBoxExtensions.Mask for IP address input?


I am developing a WinUI 3 application and would like to use TextBoxExtensions.Mask from the Windows Community Toolkit to restrict input to IP addresses.

For example: 192.1.1.0 or 255.255.255.255.

I tried the following, but the numeric parts have a fixed width, and I haven't been able to specify a range of 1 to 3 characters correctly.

Is there a good way to achieve this?

<TextBox controls:TextBoxExtensions.Mask="999.999.999.999" />

Solution

  • You can also create an AttachedProperty:

    <TextBox local:TextBoxExtensions.EnableIpAddressMask="True" />
    
    public static partial class TextBoxExtensions
    {
        public static readonly DependencyProperty EnableIpAddressMaskProperty =
            DependencyProperty.RegisterAttached(
                "EnableIpAddressMask",
                typeof(bool),
                typeof(TextBoxExtensions),
                new PropertyMetadata(default, OnEnableIpAddressMaskChanged));
    
        public static readonly DependencyProperty PreviousTextProperty =
            DependencyProperty.RegisterAttached(
                "PreviousText",
                typeof(string),
                typeof(TextBoxExtensions),
                new PropertyMetadata(string.Empty));
    
        public static bool GetEnableIpAddressMask(DependencyObject obj) => (bool)obj.GetValue(EnableIpAddressMaskProperty);
    
        public static void SetEnableIpAddressMask(DependencyObject obj, bool value) => obj.SetValue(EnableIpAddressMaskProperty, value);
    
        public static string GetPreviousText(DependencyObject obj) => (string)obj.GetValue(PreviousTextProperty);
    
        public static void SetPreviousText(DependencyObject obj, string value) => obj.SetValue(PreviousTextProperty, value);
    
        private static void OnEnableIpAddressMaskChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            if (d is not TextBox textBox)
            {
                return;
            }
    
            textBox.Text = "...";
    
            textBox.PreviewKeyDown -= TextBox_PreviewKeyDown;
            textBox.PreviewKeyDown += TextBox_PreviewKeyDown;
            textBox.TextChanged -= TextBox_TextChanged;
            textBox.TextChanged += TextBox_TextChanged;
        }
    
        private static void TextBox_TextChanged(object sender, TextChangedEventArgs e)
        {
            if (sender is not TextBox textBox)
            {
                return;
            }
    
            string previousText = GetPreviousText(textBox);
    
            if (textBox.Text.Length < previousText.Length)
            {
                return;
            }
    
            var segments = textBox.Text.Split('.');
            int currentSegmentIndex = textBox.Text[..textBox.SelectionStart].Count(c => c == '.');
    
            if (textBox.Text.ElementAtOrDefault(textBox.SelectionStart) is '.' &&
                segments[currentSegmentIndex].Length == 3)
            {
                textBox.SelectionStart++;
            }
    
            SetPreviousText(textBox, textBox.Text);
        }
    
        private static void TextBox_PreviewKeyDown(object sender, Microsoft.UI.Xaml.Input.KeyRoutedEventArgs e)
        {
            if (sender is not TextBox textBox)
            {
                return;
            }
    
            if (IsInvalidKey(e.Key) is true ||
                IsRemovingSeparator(e.Key, textBox.Text, textBox.SelectionStart) is true)
            {
                e.Handled = true;
                return;
            }
    
            if (TryGetNumber(e.Key, out string number) is false)
            {
                return;
            }
    
            var nextText = textBox.Text.Insert(textBox.SelectionStart, number);
            var segments = nextText.Split('.');
    
            foreach (var segment in segments)
            {
                if (int.TryParse(segment, out int segmentValue) is false)
                {
                    continue;
                }
    
                if (segmentValue < 0 || segmentValue > 255)
                {
                    e.Handled = true;
                    return;
                }
            }
        }
    
        private static bool IsRemovingSeparator(VirtualKey key, string text, int selectionStart)
        {
            if (key == VirtualKey.Back &&
                text.ElementAtOrDefault(selectionStart - 1) is '.')
            {
                return true;
            }
    
            if (key == VirtualKey.Delete &&
                text.ElementAtOrDefault(selectionStart) is '.')
            {
                return true;
            }
    
            return false;
        }
    
        private static bool IsInvalidKey(VirtualKey key)
        {
            return key switch
            {
                VirtualKey.Number0 or
                VirtualKey.Number1 or
                VirtualKey.Number2 or
                VirtualKey.Number3 or
                VirtualKey.Number4 or
                VirtualKey.Number5 or
                VirtualKey.Number6 or
                VirtualKey.Number7 or
                VirtualKey.Number8 or
                VirtualKey.Number9 or
                VirtualKey.NumberPad0 or
                VirtualKey.NumberPad1 or
                VirtualKey.NumberPad2 or
                VirtualKey.NumberPad3 or
                VirtualKey.NumberPad4 or
                VirtualKey.NumberPad5 or
                VirtualKey.NumberPad6 or
                VirtualKey.NumberPad7 or
                VirtualKey.NumberPad8 or
                VirtualKey.NumberPad9 or
                VirtualKey.Back or
                VirtualKey.Delete or
                VirtualKey.Left or
                VirtualKey.Right or
                VirtualKey.Home or
                VirtualKey.End => false,
                _ => true,
            };
        }
    
        private static bool TryGetNumber(VirtualKey key, out string number)
        {
            number = key switch
            {
                VirtualKey.Number0 or VirtualKey.NumberPad0 => "0",
                VirtualKey.Number1 or VirtualKey.NumberPad1 => "1",
                VirtualKey.Number2 or VirtualKey.NumberPad2 => "2",
                VirtualKey.Number3 or VirtualKey.NumberPad3 => "3",
                VirtualKey.Number4 or VirtualKey.NumberPad4 => "4",
                VirtualKey.Number5 or VirtualKey.NumberPad5 => "5",
                VirtualKey.Number6 or VirtualKey.NumberPad6 => "6",
                VirtualKey.Number7 or VirtualKey.NumberPad7 => "7",
                VirtualKey.Number8 or VirtualKey.NumberPad8 => "8",
                VirtualKey.Number9 or VirtualKey.NumberPad9 => "9",
                _ => string.Empty,
            };
    
            return number.Length > 0;
        }
    }