0
votes

I'm working on a WPF project where I have a DataGrid bounded to a ObservableCollection. The values are being bounded properly but the issue that I am running into is that I can't edit the columns with double values. It will not let me insert a period into the cell.

This is what I have for the XAML:

<DataGrid Name="dataGrid" AutoGenerateColumns="False" 
              CanUserResizeColumns="True" CanUserAddRows="False" CanUserSortColumns="True" ItemsSource="{Binding}"
              HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch" 
              ColumnWidth="*" Margin="0,51,186,58" 
              RowEditEnding="dataGrid_RowEditEnding">
        <DataGrid.Columns>
            <DataGridTextColumn Header="Field 1" Binding="{Binding Field1, UpdateSourceTrigger=PropertyChanged}" />
            <DataGridTextColumn Header="Field 2" Binding="{Binding Field2, UpdateSourceTrigger=PropertyChanged}" />
            <DataGridTextColumn Header="Field 3" Binding="{Binding Field3, UpdateSourceTrigger=PropertyChanged}" />
            <DataGridTextColumn Header="Field 4" Binding="{Binding Field4, UpdateSourceTrigger=PropertyChanged}" />
            <DataGridCheckBoxColumn Header="Field 5" Binding="{Binding Field5, UpdateSourceTrigger=PropertyChanged}" />
            <DataGridTextColumn Header="Field 6" Binding="{Binding Field6, UpdateSourceTrigger=PropertyChanged}" />
        </DataGrid.Columns>

<DataGrid>

And here is the class (Sorry for the weird class properties :/ I made it that way on purpose)

class FieldClass : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    private int _field1;
    public int Field1
    {
        get { return _field1; }
        set 
        {
            _field1 = value;
            OnPropertyChanged("Field1");
        }
    }

    private int _field2;
    public int Field2
    {
        get { return _field2; }
        set 
        {
            _field2 = value;
            OnPropertyChanged("Field2");
        }
    }

    private double _field3;
    public double Field3
    {
        get { return _field3; }
        set
        {
            _field3 = value;
            OnPropertyChanged("Field3");
        }
    }

    private double _field4;
    public double Field4
    {
        get { return _field4; }
        set 
        {
            _field4 = value;
            OnPropertyChanged("Field4");
        }
    }

    private bool _field5;
    public bool Field5
    {
        get { return _field5; }
        set 
        {
            _field5 = value;
            OnPropertyChanged("Field5");
        }
    }

    private double _field6;
    public double Field6
    {
        get { return _field6; }
        set 
        {
            _field6 = value;
            OnPropertyChanged("Field6");
        }
    }

    public FieldClass()
    {
        _field1 = 0;
        _field2 = 0;
        _field3 = 0.0;
        _field4 = 0.0;
        _field5 = false;
        _field6 = 0.0;
    }

    // Create the OnPropertyChanged method to raise the event 
    protected void OnPropertyChanged(string name)
    {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(name));
        }
    }
}

How can I make it so on the DataGrid, if I wanted to update the value of a column (lets say i want to update Field3 or any field that has a double), I can insert a double value like 2.1?

Is a data template needed? Not to sure how to go about that, still a beginner.

Thanks for the help!

3

3 Answers

1
votes

If you really want to obtain the behavior with PropertyChanged trigger you can try to use IsAsync=true of Binding, but I'm not sure that's the correct solution.

<DataGridTextColumn Header="Field 3" Binding="{Binding Field3, UpdateSourceTrigger=PropertyChanged, StringFormat=\{0:n\}, IsAsync=True}" />
1
votes

I would say that the solution would be to change DataGridTextColumn to DataGridTemplateColumn and use NumericUpDown in its template. NumericUpDown is supposed to handle such cases better than TextBox.

Here is an example with Extended WPF Toolkit control (Spin buttons can be hidden if necessary for it to look like TextBox)

<DataGridTemplateColumn Header="Field1" Width="200">
    <DataGridTemplateColumn.CellTemplate>
        <DataTemplate>
            <xctk:DoubleUpDown AllowSpin="False" ShowButtonSpinner="False" 
                               BorderThickness="0"
                               CultureInfo="en-US"
                               Value="{Binding Field1, UpdateSourceTrigger=PropertyChanged}"/>
        </DataTemplate>
    </DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>

License allows non-commercial use.


As a quick fix I would simply add Delay property (in milliseconds) to DataGridTextColumn bindings. It makes binding to update only after that amount of time has elapsed since the user stopped typing.

<DataGridTextColumn Header="Field1"
                    Width="200"
                    Binding="{Binding Field1, Delay=500, UpdateSourceTrigger=PropertyChanged}"/>

It is reasonable enough for me: if I type some number with decimal separator, I type the separator and next digit fast enough to fit the Delay.

Delay is available since .NET Framework 4.5.

1
votes

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