Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

WPF DataGrid validation/binding mode bug

I created a new project, very simple, to test only the Microsoft WPF DataGrid behavior. Nothing else involved, I only use the standard DataGrid:

solution

<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:

enter image description here

The value resets to 0!

  1. Why is it doing this? Something wrong with the way I am using DataGrids?
  2. Is there a workaround?

[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

like image 735
CTZStef Avatar asked Oct 20 '22 06:10

CTZStef


2 Answers

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

enter image description here

So, this is a bug that has been around for a while, and won't be fixed any time soon.

like image 68
CTZStef Avatar answered Oct 23 '22 04:10

CTZStef


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
like image 38
user3910810 Avatar answered Oct 23 '22 04:10

user3910810