I created a new project, very simple, to test only the Microsoft WPF DataGrid behavior. Nothing else involved, I only use the standard DataGrid:
<Window x:Class="MainWindow"
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="300" d:DesignWidth="300">
<DataGrid ItemsSource="{Binding Employees, Mode=TwoWay}"
x:Name="tlv"
AutoGenerateColumns="False"
SelectionMode="Extended"
CanUserAddRows="true"
SelectionUnit="CellOrRowHeader">
<DataGrid.Columns>
<DataGridTextColumn Header="First Name" Binding="{Binding FirstName, Mode=TwoWay}"/>
<DataGridTextColumn Header="Last Name" Binding="{Binding LastName, Mode=TwoWay}"/>
<DataGridTextColumn Header="Salary" Binding="{Binding Salary, Mode=TwoWay, ValidatesOnDataErrors=True, UpdateSourceTrigger=LostFocus}"/>
</DataGrid.Columns>
</DataGrid>
</Window>
Code behind:
Imports System.Collections.ObjectModel
Imports DataGridTest.Data
Class MainWindow
Sub New()
' This call is required by the designer.
InitializeComponent()
' Add any initialization after the InitializeComponent() call.
Me.DataContext = Me
Employees = New ObservableCollection(Of Employee)(EmployeeRepository.GetFlatListData())
BindableSelectedItems = New ObservableCollection(Of Object)
End Sub
Private _employees As ObservableCollection(Of Employee)
Public Property Employees() As ObservableCollection(Of Employee)
Get
Return _employees
End Get
Set(ByVal value As ObservableCollection(Of Employee))
_employees = value
End Set
End Property
Private _bindableSelectedItems As ObservableCollection(Of Object)
Public Property BindableSelectedItems() As ObservableCollection(Of Object)
Get
Return _bindableSelectedItems
End Get
Set(value As ObservableCollection(Of Object))
'Set the new value of BindableSelectedItems
_bindableSelectedItems = value
End Set
End Property
Private _selectedEmployeeForSelectedItemsSimulation As Employee
Public Property SelectedEmployeeForSelectedItemsSimulation() As Employee
Get
Return _selectedEmployeeForSelectedItemsSimulation
End Get
Set(value As Employee)
'Set the new value of SelectedEmployeeForSelectedItemsSimulation
_selectedEmployeeForSelectedItemsSimulation = value
If _selectedEmployeeForSelectedItemsSimulation IsNot Nothing AndAlso BindableSelectedItems IsNot Nothing Then
BindableSelectedItems.Clear()
BindableSelectedItems.Add(value)
End If
End Set
End Property
End Class
The Employee class, which implements IDataErrorInfo:
Imports Microsoft.Practices.Prism.ViewModel
Imports System.Collections.ObjectModel
Imports System.ComponentModel
Namespace Data
Public Class Employee
Inherits NotificationObject
Implements IDataErrorInfo
Public Sub New()
End Sub
Public Sub New(ByVal fName As String, ByVal lName As String, ByVal salary As Double)
FirstName = fName
LastName = lName
Me.Salary = salary
End Sub
Private _firstName As String
Public Property FirstName() As String
Get
Return _firstName
End Get
Set(value As String)
'Set the new value of FirstName
_firstName = value
'Warn any Observers that the FirstName have changed.
RaisePropertyChanged(Function() Me.FirstName)
End Set
End Property
Private _lastName As String
Public Property LastName() As String
Get
Return _lastName
End Get
Set(value As String)
'Set the new value of LastName
_lastName = value
'Warn any Observers that the LastName have changed.
RaisePropertyChanged(Function() Me.LastName)
End Set
End Property
Private _isSelected As Boolean
Public Property IsSelected() As Boolean
Get
Return _isSelected
End Get
Set(value As Boolean)
'Set the new value of IsSelected
_isSelected = value
'Warn any Observers that the IsSelected have changed.
RaisePropertyChanged(Function() Me.IsSelected)
End Set
End Property
Private _salary As Double
Public Property Salary() As Double
Get
Return _salary
End Get
Set(value As Double)
'Set the new value of Salary
_salary = value
'Warn any Observers that the Salary have changed.
RaisePropertyChanged(Function() Me.Salary)
End Set
End Property
Public ReadOnly Property [Error] As String Implements IDataErrorInfo.Error
Get
Return String.Empty
End Get
End Property
Default Public ReadOnly Property Item(columnName As String) As String Implements IDataErrorInfo.Item
Get
If Me.Salary <= 0 Then
Return "The salary must be positive."
End If
Return String.Empty
End Get
End Property
End Class
End Namespace
So far, so good. Then, here is what happens when I try to update the salary value after having added a new row:
The value resets to 0!
[EDIT] ... and WORKAROUND
I finally found what looks like a workaround, even if I still don't know the reason of what I now consider as a bug in the Microsoft DataGrid.
If a binding mode is specified in the datagrid column with a Validation, the bug occurs; if instead I do not specify the binding mode, everything is fine.
<DataGridTextColumn Header="Salary" Binding="{Binding Salary, ValidatesOnDataErrors=True, UpdateSourceTrigger=LostFocus}"/>
What I do not quite understand though, is that as far as I know the default binding mode is TwoWay... Well, at least I solved my problem.
I opened a bug report on Microsoft Connect, here
just to let you know, I got news from Microsoft, they say this is not important and they won't even try to solve the bug.
Have a look here
So, this is a bug that has been around for a while, and won't be fixed any time soon.
Assuming the DataGrid is bound I've previously created a solution using the CellEditEnding event (though not using reflection just a class implementing INotifyPropertyChanged). For it to work the UpdateSourceTrigger needs to be set Explicit for the column bindings
So, for example, assume your datagrid's itemssource is bound to an observablecollection(of Shape). Each shape has two properties ShapeName and NumberOfSides. To ensure a user doesn't enter 0 or less for the number of sides then
(roughly)
Public Class Shape
Implements ComponentModel.INotifyPropertyChanged
Dim _ShapeName As String
Dim _NumberOfSides As Int32
Public Property ShapeName As String
Get
Return _ShapeName
End Get
Set(value As String)
_ShapeName = value
Me.DataStateChanged("ShapeName")
End Set
End Property
Public Property NumberOfSides As Int32
Get
Return _NumberOfSides
End Get
Set(value As Int32)
If value > 0 Then
_NumberOfSides = value
End If
Me.DataStateChanged("NumberOfSides")
End Set
End Property
#Region "INotifyPropertyChanged Members"
Public Event PropertyChanged(ByVal sender As Object, ByVal e As ComponentModel.PropertyChangedEventArgs) _
Implements _
ComponentModel.INotifyPropertyChanged.PropertyChanged
Protected Sub DataStateChanged(ByVal propertyName As String)
' Raise the event
If Not String.IsNullOrEmpty(propertyName) Then
RaiseEvent PropertyChanged(Me, _
New ComponentModel.PropertyChangedEventArgs(propertyName))
End If
End Sub
#End Region
End Class
Then within the datagrid
private sub me_CellEditEnding(sender As System.Object, e As System.Windows.Controls.DataGridCellEditEndingEventArgs) Handles me.CellEditEnding
If e.EditAction = DataGridEditAction.Commit Then
Dim EditedItem As Shape = CType(e.Row.Item, Shape)
Dim ValueBefore As int32 = EditedItem.NumberOfSides
For Each currentExpression As BindingExpressionBase In e.EditingElement.BindingGroup.BindingExpressions
'the value is changed or not by the update within the property
currentExpression.UpdateSource()
Next
If ValueBefore = EditedItem.NumberOfSides then
'The value update was unsuccessful
End If
End If
End Sub
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With