Home > Enterprise >  How to bind a property both ways on a control inside a DataGridTemplateColumn?
How to bind a property both ways on a control inside a DataGridTemplateColumn?

Time:10-08

I have an ObservableCollection of DataPoint objects. That class has a Data property. I have a DataGrid. I have a custom UserControl called NumberBox, which is a wrapper for a TextBox but for numbers, with a Value property that is bind-able (or at least is intended to be).

I want the DataGrid to display my Data in a column NumberBox, so the value can be displayed and changed. I've used a DataGridTemplateColumn, bound the Value property to Data, set the ItemsSource to my ObservableCollection.

When the underlying Data is added or modified, the NumberBox updates just fine. However, when I input a value in the box, the Data doesn't update.

I've found answers suggesting to implement INotifyPropertyChanged. Firstly, not sure on what I should implement it. Secondly, I tried to implement it thusly on both my DataPoint and my NumberBox. I've found answers suggesting to add Mode=TwoWay, UpdateSourceTrigger=PropertyChange to my Value binding. I've tried both of these, separately, and together. Obviously, the problem remains.

Below is the bare minimum project I'm currently using to try to make this thing work. What am I missing?

MainWindow.xaml

<Window xmlns:BindingTest="clr-namespace:BindingTest" x:Class="BindingTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <DataGrid Name="Container" AutoGenerateColumns="False" CanUserSortColumns="False" CanUserResizeColumns="False" CanUserResizeRows="False" CanUserReorderColumns="False" CanUserAddRows="False" CanUserDeleteRows="True">
            <DataGrid.Columns>
                <DataGridTemplateColumn Header="Sample Text" Width="100" >
                    <DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <BindingTest:NumberBox Value="{Binding Data}"/>
                        </DataTemplate>
                    </DataGridTemplateColumn.CellTemplate>
                </DataGridTemplateColumn>
            </DataGrid.Columns>
        </DataGrid>
        <Button Name="BTN" Click="Button_Click" Height="30" VerticalAlignment="Bottom" Content="Check"/>
    </Grid>
</Window>

MainWindow.cs

public partial class MainWindow : Window
{
    private ObservableCollection<DataPoint> Liste { get; set; }

    public MainWindow()
    {
        InitializeComponent();

        Liste = new ObservableCollection<DataPoint>();

        Container.ItemsSource = Liste;

        DataPoint dp1 = new DataPoint(); dp1.Data = 1;
        DataPoint dp2 = new DataPoint(); dp2.Data = 2;
        Liste.Add(dp1);
        Liste.Add(dp2);
    }

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        BTN.Content = Liste[0].Data;
    }
}

DataPoint.cs

public class DataPoint
{
    public double Data { get; set; }
}

NumberBox.xaml

<UserControl x:Class="BindingTest.NumberBox"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             d:DesignHeight="28" d:DesignWidth="200">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="Auto"/>
        </Grid.ColumnDefinitions>

        <TextBox Name="Container" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" VerticalContentAlignment="Center"/>
    </Grid>
</UserControl>

NumberBox.cs

public partial class NumberBox : UserControl
{
    public event EventHandler ValueChanged;

    public NumberBox()
    {
        InitializeComponent();
    }

    private double _value;
    public double Value
    {
        get { return _value; }
        set
        {
            _value = value;
            Container.Text = value.ToString(CultureInfo.InvariantCulture);
            if (ValueChanged != null) ValueChanged(this, new EventArgs());
        }
    }

    public static readonly DependencyProperty ValueProperty = DependencyProperty.Register(
        "Value",
        typeof(double),
        typeof(NumberBox),
        new PropertyMetadata(OnValuePropertyChanged)
    );

    public static void OnValuePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        double? val = e.NewValue as double?;
        (d as NumberBox).Value = val.Value;
    }
}

CodePudding user response:

What am I missing?

Quite a few things actually.

  1. The CLR property of a dependency property should only get and set the value of the dependency property:

     public partial class NumberBox : UserControl
     {
         public NumberBox()
         {
             InitializeComponent();
         }
    
         public double Value
         {
             get => (double)GetValue(ValueProperty);
             set => SetValue(ValueProperty, value);
         }
    
         public static readonly DependencyProperty ValueProperty = DependencyProperty.Register(
             "Value",
             typeof(double),
             typeof(NumberBox),
             new PropertyMetadata(0.0)
         );
    
         private void Container_PreviewTextInput(object sender, TextCompositionEventArgs e)
         {
             Value = double.Parse(Container.Text, CultureInfo.InvariantCulture);
         }
     }
    
  2. The TextBox in the control should bind to the Value property:

     <TextBox Name="Container" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" VerticalContentAlignment="Center"
              Text="{Binding Value, RelativeSource={RelativeSource AncestorType=UserControl}}"
              PreviewTextInput="Container_PreviewTextInput"/>
    
  3. The mode of the binding between the Value and the Data property should be set to TwoWay and also, and this is because the input control is in the CellTemplate of the DataGrid, the UpdateSourceTrigger should be set to PropertyChanged:

     <local:NumberBox x:Name="nb" Value="{Binding Data, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
    

CodePudding user response:

mm8's answer pointed me in the right direction.

The two key missing elements were indeed: to change the TextBox in NumberBox.xaml such as

<TextBox Name="Container"
     Text="{Binding Value, RelativeSource={RelativeSource AncestorType=UserControl}}"
     HorizontalAlignment="Stretch" VerticalAlignment="Stretch" VerticalContentAlignment="Center"
    />

and to change the binding in MainWindow.xaml such as

...
<DataTemplate>
    <BindingTest:NumberBox 
        Value="{Binding Data, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
</DataTemplate>
...

With these two modifications, both the Value and Data are being updated two-ways inside my DataGridTemplaceColumn.

Binding to the text however proved problematic. WPF apparently uses en-US culture no matter how your system is setup, which is really bad as this control deals with numbers. Using this trick solves that problem, and now the correct decimal separator is recognised. In my case, I've added it in a static constructor for the same effect so NumberBox can be used in other applications as is.

static NumberBox()
{
    FrameworkElement.LanguageProperty.OverrideMetadata(
        typeof(FrameworkElement),
        new FrameworkPropertyMetadata(XmlLanguage.GetLanguage(CultureInfo.CurrentCulture.IetfLanguageTag)));
}

I've tested this in my test project, and then again tested the integration into my real project. As far as I can tell, it holds water, so I'll consider this question answered.

  • Related