A summary of the article "Not an easy solution for a simple common problem. Binding a TextBox to a numeric property".
1) Description of the problem.
The main one solved in the topic is the problem with entering a decimal point in a TextBox bound to a non-integer property. It manifests itself when using the UpdateSourceTrigger = PropertyChanged binding in TwoWay mode.
In addition to a dot, it is also impossible to enter insignificant zeros.
While this is rarely needed for leading zeros (000123), trailing zeros (123.000) pose a problem.
Let's say entering the number 123.0004, with a trivial binding, will not work.
The second, incidentally solved and often required, task is to restrict the user to enter "only numbers".
It is solved in a more general form: by restricting the entry of only numerical values. Signs are allowed for signed types, decimal point for non-integer numbers, separators of digit groups are allowed in a given culture, signs of scientific presentation.
2) Causes of the problem. Internal logic implemented in Binding.
Consider what happens when you enter a number with a decimal point and non-significant zeros:
- The number
123.000
is entered;
- The assignment of the value to the source property (UpdateSource) is triggered;
- For assignment, the string is converted to 123.0;
- The number is transferred to Reflection;
- Reflection assigns the passed number and calls the binding to update the target (UpdateTarget);
- The binding converts the number 123.0 to its string representation. And here the point and insignificant zeros are discarded! The string "123" is obtained from the number 123.0;
- This string is passed to the DependencyObject to be assigned to the Text property. BUT in the Text property there is a string "123.000". And since it is not equal to the string "123", the value "123" is assigned to the Text property - the point and zeros are gone!
In order for the assignment not to occur, it is necessary to compare the new value not just with the current one, but convert both values to the numeric type of the source property and compare these numbers.
But DependencyObject doesn't "know" anything about bindings. Doesn't even know if this property has bindings.
To solve the problem, you need to make a decision analyzing the current value of the Text property even before converting the number to a string. And if the same number can be obtained from Text, then the assignment should not occur.
3) Applying a multi converter to bind Double properties.
First, let's implement the most obvious solution through a multi-converter.
The algorithm of its work is very simple - it receives two values: binding to the source property and binding to the target property.
By comparing two values, it can return either a string representation of the number, or it can undo the assignment by returning Binding.DoNothing.
Converting a number to a string and back is carried out taking into account the culture transferred to the converter.
For debugging added output to Debug, and in the Control Window showing this output.
Two more validators have been added to the XAML in the binding to demonstrate type conversion.
Full code of MultiConverter:
using System;
using System.Diagnostics;
using System.Globalization;
using System.Windows.Data;
namespace BindingStringToNumeric
{
/// <summary>Сравнивает полученное <see cref="Double"/> число со <see cref="String"/> текстом.<br/>
/// Если из текста получается такое же число, то присвоение значение по привязке отменяется.</summary>
/// <remarks>Значения должны приходить в массиве в параметре values метода <see cref="IMultiValueConverter.Convert(object[], Type, object, CultureInfo)"/><br/>
/// В массиве должо быть два значения:<br/>
/// 0 - значение источника в <see cref="Double"/> типе,<br/>
/// 1 - <see cref="String"/> Text с которым надо сравнить число.</remarks>
public class DoubleConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
Debug.Write(GetType() + $".Convert.values: {values[0]}, \"{values[1]}\"");
double source = (double)values[0];
string text = (string)values[1];
object ret;
// Получение из текста числа (в переданной культуре) и сравнение его с числом источника.
// Если они равны, то отменяется присвоение значения.
if (double.TryParse(text, NumberStyles.Any, culture, out double target) && target == source)
ret = Binding.DoNothing;
// Иначе число источника переводится в строку в заданнной культуре и возвращается.
else
ret = source.ToString(culture);
Debug.WriteLine($"; return: {ret ?? "null"}");
return ret; ;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
Debug.Write(GetType() + $".ConvertBack.value: \"{value}\" to ");
object ret = null;
string text = (string)value;
// Если строка пустая, то это считается эквивалентом нуля.
if (string.IsNullOrWhiteSpace(text))
ret = 0.0;
// Иначе проверяется возвожность перевода строки в число в заданной культуре.
// Если перевод возможен, то возвращается полученное число.
else if (double.TryParse(text, NumberStyles.Any, culture, out double target))
ret = target;
Debug.WriteLine($"return: {ret ?? "null"}");
// Если ret значение не присваивалось, то значит строка некорректна
// Тогда возвращается null, что вызывает ошибку валидации.
if (ret == null)
return null;
// Иначе возвращается массив с одним элементом: полученным числом.
return new object[] { ret };
}
}
}
Full XAML Windows:
<Window x:Class="AppBindingToNumeric.DoubleConverterWindow"
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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:AppBindingToNumeric"
xmlns:dgn="clr-namespace:WpfCustomControls.Diagnostics;assembly=WpfCustomControls"
xmlns:bnd="clr-namespace:BindingStringToNumeric;assembly=BindingToNumeric"
mc:Ignorable="d" FontSize="20"
Title="Example #2: Binding to Double Property with MultiConverter"
Height="450" Width="1000">
<FrameworkElement.Resources>
<bnd:DoubleConverter x:Key="DoubleConverter"/>
<local:Numbers x:Key="Numbers"/>
</FrameworkElement.Resources>
<FrameworkElement.DataContext>
<Binding Mode="OneWay" Source="{StaticResource Numbers}"/>
</FrameworkElement.DataContext>
<Grid Background="LightGreen">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<UniformGrid Background="LightBlue" Columns="2">
<TextBlock Text="TextBlock"/>
<TextBox Margin="5" Text="{Binding DoubleValue}" IsEnabled="False"/>
<TextBlock Text="BindingMode=TwoWay"/>
<TextBox x:Name="tbValidate" Margin="5">
<TextBox.Text>
<MultiBinding Converter="{StaticResource DoubleConverter}" UpdateSourceTrigger="PropertyChanged">
<MultiBinding.ValidationRules>
<local:DebugValidationRule Title="MultiBinding"/>
</MultiBinding.ValidationRules>
<Binding Path="DoubleValue">
<Binding.ValidationRules>
<local:DebugValidationRule Title="Binding"/>
</Binding.ValidationRules>
</Binding>
<Binding Path="Text" ElementName="tbValidate"/>
</MultiBinding>
</TextBox.Text>
</TextBox>
<TextBlock Text="BindingMode=OneTime"/>
<TextBox x:Name="tbDogitsOnly" Margin="5">
<TextBox.Text>
<MultiBinding Converter="{StaticResource DoubleConverter}" UpdateSourceTrigger="PropertyChanged">
<Binding Path="DoubleValue"/>
<Binding Path="Text" ElementName="tbDogitsOnly" Mode="OneTime"/>
</MultiBinding>
</TextBox.Text>
</TextBox>
</UniformGrid>
<dgn:DebugBox Grid.Row="1" Margin="10" FontSize="18"
IsOutputsText="{Binding IsActive,
RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type Window}}}"/>
</Grid>
</Window>
Video test of the converter: https://youtu.be/TauKTs7279Y
4) A universal multiconverter for binding to a numerical property of any type.
For a full-fledged generic converter, we first need to create a generic method for obtaining a validating parser for any numeric type.
I solved this by creating parsers that return an object for each numeric type, a dictionary that stores these parsers, and a method that returns a parser by type.
Everything is implemented in a static class.
The logic is very simple, so there are no detailed comments. Only XML documentation tags are specified.
The source codes are posted on GitHub (link to the repository at the end of the answer), so I don't post them here.
Video test of the converter: https://youtu.be/0LFHlgxvQso
The big drawback of this solution is the difficulty of applying it in XAML. And an error in one of the bindings or in its parameter will cause the converter to work incorrectly.
To simplify the use, need to encapsulate this converter in a markup extension and implement the transfer of the old value of the Text property to the converter in the same place.
5) Extending the markup, including creating an attached property, a private converter and a private MultiBinding.
The entire code is over 700 lines long.
It has been published on GitHub and there is no point in republishing it here.
Here is just an example of its use:
<TextBox Margin="5" Text="{bnd:BindToNumeric DoubleValue}"/>
<TextBox Margin="5" Text="{bnd:BindToNumeric DecimalValue, IsNumericOnly=False}"/>
For reference, also the complete XAML of the тest Window:
<Window x:Class="AppBindingToNumeric.MarkupExtensionWindow"
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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:AppBindingToNumeric"
xmlns:bnd="clr-namespace:BindingStringToNumeric;assembly=BindingToNumeric"
xmlns:dgn="clr-namespace:WpfCustomControls.Diagnostics;assembly=WpfCustomControls"
mc:Ignorable="d"
Title="Example #4: Binding to a Numeric Properties with the Markup Extension"
Height="450" Width="1000" FontSize="20">
<FrameworkElement.Resources>
<local:Numbers x:Key="Numbers" DoubleValue="123" DecimalValue="456" IntegerValue="799"/>
</FrameworkElement.Resources>
<FrameworkElement.DataContext>
<Binding Mode="OneWay" Source="{StaticResource Numbers}"/>
</FrameworkElement.DataContext>
<Grid Background="LightGreen">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<UniformGrid Background="LightBlue" Columns="3">
<TextBlock Text="To Double - Numeric Only"/>
<TextBox Margin="5" Text="{bnd:BindToNumeric DoubleValue}"/>
<TextBox Margin="5" Text="{Binding DoubleValue}" IsEnabled="False"/>
<TextBlock Text="To Decimal - Any Value"/>
<TextBox Margin="5" Text="{bnd:BindToNumeric DecimalValue, IsNumericOnly=False}"/>
<TextBox Margin="5" Text="{Binding DecimalValue}" IsEnabled="False"/>
<TextBlock Text="To Integer - Numeric Only"/>
<TextBox Margin="5" Text="{bnd:BindToNumeric IntegerValue}"/>
<TextBox Margin="5" Text="{Binding IntegerValue}" IsEnabled="False"/>
</UniformGrid>
<dgn:DebugBox Grid.Row="1" Margin="10" FontSize="18"
IsOutputsText="{Binding IsActive,
RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type Window}}}"/>
</Grid>
</Window>
Source codes for GitHub: https://github.com/EldHasp/CyberForumMyCycleRepos/tree/master/BindingToNumber