validationuwpxbind

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

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