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
):
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!
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;
}
}
}