The WPF DataGrid (.NET 4.5.1) doesn't automatically destroy old editing elements. Consider the following mini example:
XAML:
<Window ....
xmlns:wa="clr-namespace:WpfApplication1">
....
<DataGrid Name="dataGrid" AutoGenerateColumns="False" Height="200" CanUserAddRows="False">
<DataGrid.Columns>
<DataGridTemplateColumn Header="testCol">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding TestText}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<wa:MyTextBox Text="{Binding TestText}" />
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
CODE:
private void Window_Loaded(object sender, RoutedEventArgs e)
{
List<TestClass> tclist = new List<TestClass>();
tclist.Add(new TestClass());
tclist.Add(new TestClass());
this.dataGrid.ItemsSource = tclist;
}
public class TestClass
{
public string TestText { get; set; }
}
public class MyTextBox : TextBox
{
internal static int IntPool = 0;
private int _ID = -1;
public MyTextBox()
{
IntPool++;
_ID = IntPool;
System.Diagnostics.Trace.WriteLine("_ID = " + _ID.ToString());
}
protected override void OnTextChanged(TextChangedEventArgs e)
{
base.OnTextChanged(e);
System.Diagnostics.Trace.WriteLine("txtChanged _ID = " + _ID.ToString());
}
}
When you run the application and begin editing the first cell, "_ID = 1" appears in the trace window. Type a letter and "txtChanged _ID = 1" appears. Click outside of the DataGrid to end editing. Repeat these steps and once you have finished editing you'll notice that both "txtChanged _ID = 2" and "txtChanged _ID = 1" are added to the trace window. A sample trace would look like:
_ID = 1 // editing element created
txtChanged _ID = 1 // typed the letter A
_ID = 2 // editing element created
txtChanged _ID = 2 // TextBox empty string replaced with letter A via binding
txtChanged _ID = 2 // typed the letter B
txtChanged _ID = 1 // this old editing element still firing!!!
Repeat this more, and you'll see that multiple old editing elements keep firing their TextChanged events.
This is causing problems for me, because I have two columns - Amount
and AmountUnit
. Amount
has a NumericUpDown for it's editing element and AmountUnit
will specify the number of decimal places for the NumericUpDown. The NumericUpDown of course has a CoerceValue method that will constrain the value to the specified decimal places. The problem is, that all the old NumericUpDown editing elements are stepping over eachothers tows and re-writing the Amount
value when they should instead be garbage collected at this point.
So my question is, how do I get old editing elements to be immediately destroied or garbage collected once the cell has finished editing? Or what would be a (nice simple) workaround for this problem?
I figured it out :)
The problem is that DataGridTemplateColumn doesn't automatically clear the bindings for its elements after they have been unloaded. Perhaps, because you can define very complex elements and it would be very difficult and time consuming to do it automatically.
Looking at DataGridTextColumn with a reflector, I concluded that I have to do it manually, because DataGridTextColumn also clears the binding internally.
The problem I now faced, was that none of the standard methods for clearing a binding worked:
// For instance, I Added a MyTextBox_Unloaded event handler and tried calling:
private void MyTextBox_Unloaded(object sender, RoutedEventArgs e)
{
// none of these worked
(sender as MyTextBox).ClearValue(MyTextBox.TextProperty); // this is what DataGridTextColumn internally uses
BindingOperations.ClearAllBindings((sender as MyTextBox));
(sender as MyTextBox).SetValue(MyTextBox.TextProperty, null);
(sender as MyTextBox).Text = null;
BindingOperations.ClearBinding((sender as MyTextBox), MyTextBox.TextProperty);
}
// I also tried, this:
private void dataGrid_CellEditEnding(object sender, DataGridCellEditEndingEventArgs e)
{
var cp = e.EditingElement as ContentPresenter;
var child = (VisualTreeHelper.GetChild(cp, 0) as UIElement);
BindingOperations.ClearBinding(cp.BindingGroup.BindingExpressions[0].Target, cp.BindingGroup.BindingExpressions[0].TargetProperty);
(child as MyTextBox).ClearValue(MyTextBox.TextProperty);
BindingOperations.ClearAllBindings(child as MyTextBox);
cp.BindingGroup.BindingExpressions.Clear();
}
But finally I stumbled upon this, which does work:
private void dataGrid_CellEditEnding(object sender, DataGridCellEditEndingEventArgs e)
{
if (e.Column is DataGridTemplateColumn)
{
BindingOperations.ClearAllBindings(e.EditingElement as ContentPresenter);
}
}