'x:bind and data validation for numeric field

I'm struggling a bit with UWP, x:Bind and data validation. I've got a very simple use case: I want the user to input an int in a TextBox and display the number in a TextBlock as soon as the user leaves the TextBox. I can set the InputScope="Number" for the TextBox, but that doesn't prevent someone who type with a keyboard to type an alpha char (or paste something). Problem is, when I bind a field with the Mode=TwoWay, it seems that you can't prevent a System.ArgumentException if the field that you bind is declared as int. I wanted to check in the set method if the input was a number, but the exception occurs just before that. My (very simple) ViewModel (no model here, I tried to keep it as simple as possible):

public class MyViewModel : INotifyPropertyChanged
{
    private int _MyFieldToValidate;
    public int MyFieldToValidate
    {
        get { return _MyFieldToValidate; }
        set
        {
            this.Set(ref this._MyFieldToValidate, value);
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    protected void RaisedPropertyChanged([CallerMemberName]string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    protected bool Set<T>(ref T storage, T value, [CallerMemberName]string propertyName = null)
    {
        if (Equals(storage, value))
        {
            return false;
        }
        else
        {
            storage = value;
            this.RaisedPropertyChanged(propertyName);
            return true;
        }
    }
}

My code behind:

public sealed partial class MainPage : Page
{
    public MyViewModel ViewModel { get; set; } = new MyViewModel() { MyFieldToValidate = 0 };

    public MainPage()
    {
        this.InitializeComponent();
    }
}

And my whole XAML:

<Page
    x:Class="SimpleFieldValidation.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:SimpleFieldValidation"
    xmlns:vm="using:SimpleFieldValidation.ViewModel"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <Grid.RowDefinitions>
            <RowDefinition Height="10*" />
            <RowDefinition Height="*" />
            <RowDefinition Height="10*" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>

        <TextBox Grid.Row="1" Grid.Column="0" Text="{x:Bind ViewModel.MyFieldToValidate, Mode=TwoWay}" x:Name="inputText" InputScope="Number" />
        <TextBlock Grid.Row="1" Grid.Column="1" Text="{x:Bind ViewModel.MyFieldToValidate, Mode=OneWay}" x:Name="textToDisplay" />
    </Grid>
</Page>

If I type a numeric char in the TextBox, everything's OK. But if I type a non-numeric value (say "d") (it doesn't even reach the breakpoint at the first bracket of the set method for MyFieldToValidate):

Imgur

Is there a best practice to do what I want to do? The simplest solution would be preventing the user to type other char than numeric in the first place, but I've been searching for hours without finding a simple way... Another solution would be to validate the data on leaving the field, but I didn't find something relevant for UWP and x:Bind (few things for WPF thought, but they can't be replicated with a UWP). Thanks!



Solution 1:[1]

As @RTDev said, your exception is caused by the system can not convert string to int.

You can create a class that allows you to convert the format of your data between the source and the target by inheriting from IValueConverter.

You should always implement Convert(Object, TypeName, Object, String) with a functional implementation, but it's fairly common to implement ConvertBack(Object, TypeName, Object, String) so that it reports a not-implemented exception. You only need a ConvertBack(Object, TypeName, Object, String) method in your converter if you are using the converter for two-way bindings, or using XAML for serialization.

For more info, see IValueConverter Interface.

For example:

<Page.Resources>
    <local:IntFormatter x:Key="IntConverter" />
</Page.Resources>
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <Grid.RowDefinitions>
        <RowDefinition Height="10*" />
        <RowDefinition Height="*" />
        <RowDefinition Height="10*" />
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="*" />
    </Grid.ColumnDefinitions>
    <TextBox Grid.Row="1" Grid.Column="0" Text="{x:Bind ViewModel.MyFieldToValidate, Mode=TwoWay,Converter={StaticResource IntConverter}}" x:Name="inputText" InputScope="Number" />
    <TextBlock Grid.Row="1" Grid.Column="1" Text="{x:Bind ViewModel.MyFieldToValidate, Mode=OneWay}" x:Name="textToDisplay" />
</Grid>

The IntFormatter class:

internal class IntFormatter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, string language)
    {
        if (value != null)
        {
            return value.ToString();
        }
        else
        {
            return null;
        }
    }

    public object ConvertBack(object value, Type targetType, object parameter, string language)
    {
        int n;
        bool isNumeric = int.TryParse(value.ToString(), out n);
        if (isNumeric)
        {
            return n;
        }
        else
        {
            return 0;
        }
    }
}

Solution 2:[2]

If you don't want the users to type alphanumerical characters, I think the most elegant solution is to create a new class NumberBox that inherits from the class InputBox and overload the OnKeyDown method to intercept the alphanumerical keystrokes, something like this:

using Windows.System;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Input;

namespace MyProject.Controls
{
    public sealed class NumberBox : TextBox
    {
        protected override void OnKeyDown(KeyRoutedEventArgs e)
        {
            if (e.Key >= VirtualKey.Number0 && e.Key <= VirtualKey.Number9 ||
                e.Key >= VirtualKey.NumberPad0 && e.Key <= VirtualKey.NumberPad9 ||
                e.Key >= VirtualKey.Left && e.Key <= VirtualKey.Down ||
                e.Key == VirtualKey.Delete ||
                e.Key == VirtualKey.Tab ||
                e.Key == VirtualKey.Back ||
                e.Key == VirtualKey.Enter)
                base.OnKeyDown(e);
            else
                e.Handled = true;
        }
    }
}

Then in your XAML, add a namespace to reference the namespace where your NumberBox class is, and then replace InputBox with control:NumberBox, something like this:

<Page
    x:Class="MyProject.View.CalibrarEnfoque"
    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:local="using:MyProject.View"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:controls="using:MyProject.Controls"
    mc:Ignorable="d">
    <Grid>
        <controls:NumberBox Text="{x:Bind ViewModel.MyValue, Mode=TwoWay}"/>
    </Grid>
</Page>

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Community
Solution 2 joseangelmt