kinton
kinton

Reputation: 193

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" />

Upvotes: 1

Views: 60

Answers (2)

Andrew KeepCoding
Andrew KeepCoding

Reputation: 13666

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;
    }
}

Upvotes: 1

Andrew KeepCoding
Andrew KeepCoding

Reputation: 13666

Unfortunately, it seems that you can't achieve it with the CommunityToolkit's TextBoxExtensions.

The following control might not be perfect but should give something to work on:

public partial class IPAddressBox : TextBox
{
    public IPAddressBox()
    {
        PreviewKeyDown += IPAddressBox_PreviewKeyDown;
        TextChanged += IPAddressBox_TextChanged;
        Text = "...";
    }

    private string PreviousText { get; set; } = string.Empty;

    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;
    }

    private void IPAddressBox_PreviewKeyDown(object sender, Microsoft.UI.Xaml.Input.KeyRoutedEventArgs e)
    {
        if (IsInvalidKey(e.Key) is true ||
            IsRemovingSeparator(e.Key) is true)
        {
            e.Handled = true;
            return;
        }

        if (TryGetNumber(e.Key, out string number) is false)
        {
            return;
        }

        var nextText = Text.Insert(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 bool IsRemovingSeparator(VirtualKey key)
    {
        if (key == VirtualKey.Back &&
            Text.ElementAtOrDefault(SelectionStart - 1) is '.')
        {
            return true;
        }

        if (key == VirtualKey.Delete &&
            Text.ElementAtOrDefault(SelectionStart) is '.')
        {
            return true;
        }

        return false;
    }

    private void IPAddressBox_TextChanged(object sender, TextChangedEventArgs e)
    {
        if (Text.Length < PreviousText.Length)
        {
            return;
        }

        var segments = Text.Split('.');
        int currentSegmentIndex = Text[..SelectionStart].Count(c => c == '.');

        if (Text.ElementAtOrDefault(SelectionStart) is '.' &&
            segments[currentSegmentIndex].Length == 3)
        {
            SelectionStart++;
        }

        PreviousText = Text;
    }
}

Upvotes: 1

Related Questions