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.
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); } }
The
TextBox
in the control should bind to theValue
property:<TextBox Name="Container" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" VerticalContentAlignment="Center" Text="{Binding Value, RelativeSource={RelativeSource AncestorType=UserControl}}" PreviewTextInput="Container_PreviewTextInput"/>
The mode of the binding between the
Value
and theData
property should be set toTwoWay
and also, and this is because the input control is in theCellTemplate
of theDataGrid
, theUpdateSourceTrigger
should be set toPropertyChanged
:<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.