DataTable RowChanged Events raised for rows that have not changed when using DataTable.Merge(DataTable)

StackOverflow https://stackoverflow.com/questions/22102254

Pregunta

I am implementing a 3 tier application (WPF/UI layer, business layer, and data access layer).

In the Data Access layer I have a refresh method that queries the database and merges rows from that query into DataTables that contain information for the business layer objects.

The business layer objects need to detect when there were changes to the row that it corresponds with so that it can raise notify property events to let the UI know that the information has been changed & refresh the display.

I was thinking about using the DataTable.RowChanged Event for this process but what I have been finding is that when I execute the DataTable.Merge(DataTable) Method the RowChanged event is raised for every single row in the DataTable even when there were no changes.

I have simplified my problem use in memory DataTables for demonstration purposes and found the same thing occurs.

I have created a WPF application called TestingRowChangeAndMerge. There is one WPF window called "MainWindow" in this project and one class called "TableManager" that creates 2 tables that will be merged when the "Merge" method is called and has a property that indicates the number of RowChanged events that were raised during the merge process.

Here is my XAML for the MainWindow:

<Window x:Class="MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:TestingRowChangeAndMerge"
    Title="MainWindow" Height="350" Width="525">
<Window.Resources>
    <local:TableManger x:Key="TableManagerResource" />
</Window.Resources>
<Grid DataContext="{StaticResource TableManagerResource}">
    <Grid.RowDefinitions>
        <RowDefinition Height="*"/>
        <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>
    <DataGrid ItemsSource="{Binding Table1.DefaultView}" AutoGenerateColumns="True" >

    </DataGrid>
    <StackPanel Grid.Row="1">
        <TextBlock Text="{Binding ChangesDetected}" />
    <Button x:Name="MergeTables" Content="Merge" Click="MergeTables_Click"/>
    </StackPanel>
</Grid>

Here is the MainWindow.xaml.VB code for handling the button click event and also the TableManager class:

Class MainWindow 

Private Sub MergeTables_Click(sender As System.Object, e As System.Windows.RoutedEventArgs)
    Dim tbManager As TableManger = FindResource("TableManagerResource")
    tbManager.MergeTables()
End Sub

End Class

Class TableManger
Implements ComponentModel.INotifyPropertyChanged

Private _table1 As System.Data.DataTable
Private _table2 As System.Data.DataTable
Private _changesDetected As Integer = 0

Public ReadOnly Property Table1
    Get
        Return _table1
    End Get
End Property
Public ReadOnly Property ChangesDetected As Integer
    Get
        Return _changesDetected
    End Get
End Property

Public Sub New()
    _table1 = CreateTableWithData()
    _table1.AcceptChanges()

    AddHandler _table1.RowChanged, New System.Data.DataRowChangeEventHandler(AddressOf Row_Changed)
End Sub

Public Sub MergeTables()

    _table2 = _table1.Clone
    Dim tableRows As New List(Of System.Data.DataRow)
    For Each r In _table1.Rows
        Dim dr2 = _table2.NewRow
        For Each col As System.Data.DataColumn In _table1.Columns
            dr2(col.ColumnName) = r(col.ColumnName)
        Next
        _table2.Rows.Add(dr2)
        tableRows.Add(dr2)
    Next
    _table2.AcceptChanges()


    If _table2.Rows.Count > 0 Then
        _table2.Rows(0)(1) = "TB2 Changed"
    End If

    If _table1.Rows.Count > 0 Then
        '_table1.Rows(0)(1) = "TB1 Change"'
        _table1.Rows(1)(1) = "TB1 Change"
    End If

    _changesDetected = 0
    Dim perserveChanges As Boolean = True
    Dim msAction As System.Data.MissingSchemaAction = System.Data.MissingSchemaAction.Ignore

    Dim changes As System.Data.DataTable = _table1.GetChanges()
    If changes IsNot Nothing Then
        changes.Merge(_table2, perserveChanges, msAction)
        _table1.Merge(changes, False, msAction)
    Else
        _table1.Merge(_table2, False, msAction)
    End If


    MessageBox.Show(String.Format("Changes in Change Table: {0} {1}Changes Detected: {2}", If((changes Is Nothing), 0, changes.Rows.Count), System.Environment.NewLine, _changesDetected), "Testing")

    RaiseEvent PropertyChanged(Me, New ComponentModel.PropertyChangedEventArgs("Table1"))
    RaiseEvent PropertyChanged(Me, New ComponentModel.PropertyChangedEventArgs("ChangesDetected"))
End Sub

Private Sub Row_Changed(ByVal sender As Object, ByVal e As System.Data.DataRowChangeEventArgs)
    Select Case e.Action
        Case System.Data.DataRowAction.Change'
        '    If e.Row.RowState <> System.Data.DataRowState.Unchanged Then'
               _changesDetected += 1
        '    End If
    End Select
End Sub

Private Function CreateTableWithData() As System.Data.DataTable
    Dim newTable As New System.Data.DataTable
    Dim columnID As New System.Data.DataColumn("ID", GetType(Guid))
    Dim columnA As New System.Data.DataColumn("ColumnA", GetType(String))
    Dim columnB As New System.Data.DataColumn("ColumnB", GetType(String))
    newTable.Columns.AddRange({columnID, columnA, columnB})
    newTable.PrimaryKey = {newTable.Columns(0)}
    For i = 0 To 5
        Dim dr = newTable.NewRow
        dr("ID") = Guid.NewGuid
        dr("ColumnA") = String.Format("Column A Row {0}", i.ToString)
        dr("ColumnB") = String.Format("Column B Row {0}", i.ToString)
        newTable.Rows.Add(dr)
    Next
    Return newTable
End Function

Public Event PropertyChanged(sender As Object, e As System.ComponentModel.PropertyChangedEventArgs) Implements System.ComponentModel.INotifyPropertyChanged.PropertyChanged
End Class

When you click the button you will see that the number displayed above the button changes from 0 to 6 which reflects the number of RowChangedEvents raised when the table merge was performed.

How do I changes this so that I can detect when actual row changes have occurred??

Edit/Update:

I didn't call accept changes on table2 in my original code which meant that when I merged table1 and table2 together, all of the rows were being marked as modified. I have updated the code here to work properly.

After I fixed that problem, I changed the logic in the code that handles the Row_Change event so that it only increments the _changesDetected counter if the RowState of the row does not equal Unchanged because I thought that this would be an indication for when the rows changed.

But after answering Merge two identical DataTables results in DataRowState.Modified I could no longer use the Modified RowState because the RowStates no longer changed.

So I'm back to my original question/problem:

How do I know which rows were actually changed since all of the rows raise the "RowChanged" event in the merge???

¿Fue útil?

Solución

I had to accept the fact that the RowChange event is raised for every row during the DataTable Merge. This means that I couldn't use the RowChange Event to raise an event for my business classes to handle (which would then raise change notification events that refresh the UI when applicable).

To solve my problem I:

  • Copy the original table
  • Retrieve the data from the database and perform the necessary merge logic
  • Compared the original table rows to the rows retrieved from the database and selected any row's whose RowVersion column changed
  • Raised a "RowModified" event for any rows that were changed in the database and catch these events in my business classes.

The following code is a demonstration of how I solved my problem on a simpler scale based on my previous demo project I mentioned in my problem.

Here is the XAML markup for the MainWindow in my WPF sample/demo project:

<Window x:Class="MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:TestingRowChangeAndMerge"
    Title="MainWindow" Height="350" Width="525">
<Window.Resources>
    <local:TableManger x:Key="TableManagerResource" />
</Window.Resources>
<Grid DataContext="{StaticResource TableManagerResource}">
    <Grid.RowDefinitions>
        <RowDefinition Height="*"/>
        <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>
    <DataGrid ItemsSource="{Binding Table1.DefaultView}" AutoGenerateColumns="True" >

    </DataGrid>
    <StackPanel Grid.Row="1">
        <TextBlock Text="{Binding ChangesDetected}" />
    <Button x:Name="MergeTables" Content="Merge" Click="MergeTables_Click"/>
    </StackPanel>
</Grid>
</Window>

And here is the code for the Button Click event and the class that is used to demonstrate the merging of data.

MainWindow Code for Button Click event:

Class MainWindow

Private Sub MergeTables_Click(sender As System.Object, e As System.Windows.RoutedEventArgs)
    Dim tbManager As TableManger = FindResource("TableManagerResource")
    tbManager.MergeTables()
End Sub

End Class

TableManger Class:

Class TableManger
Implements ComponentModel.INotifyPropertyChanged

Public Event RowModified(ByVal id As Guid)
Private _table1 As System.Data.DataTable
Private _changesDetected As Integer = 0

Public ReadOnly Property Table1
    Get
        Return _table1
    End Get
End Property
Public ReadOnly Property ChangesDetected As Integer
    Get
        Return _changesDetected
    End Get
End Property

Public Sub New()
    _table1 = CreateTableWithData()
    _table1.AcceptChanges()

    AddHandler Me.RowModified, AddressOf RowModifiedHandler
End Sub

Public Sub MergeTables()
    _changesDetected = 0

    Dim tablePopulatedFromDB = SimulateGetDataFromDatabase()

    'The following is used test that changes are preserved during merging'
    'If _table1.Rows.Count > 0 Then'
    '    ' _table1.Rows(0)(1) = "TB1 Change"'
    '    _table1.Rows(1)(1) = "Change made by user"'
    'End If'

    Dim originalTableData = _table1.Copy
    Dim perserveChanges As Boolean = True
    Dim msAction As System.Data.MissingSchemaAction = System.Data.MissingSchemaAction.Ignore


    Dim changes As System.Data.DataTable = _table1.GetChanges()
    If changes IsNot Nothing Then
        changes.Merge(tablePopulatedFromDB, perserveChanges, msAction)
        _table1.Merge(changes, False, msAction)
    Else
        _table1.Merge(tablePopulatedFromDB, False, msAction)
    End If

    Dim changedRows = From tb1Row In _table1.Rows
                      Join origTBRow In originalTableData.Rows
                      On origTBRow("ID") Equals tb1Row("ID")
                      Where origTBRow("RowVersion") <> tb1Row("RowVersion")
                      Select tb1Row
    For Each changedRow In changedRows
        RaiseEvent RowModified(changedRow("ID"))
    Next

    RaiseEvent PropertyChanged(Me, New ComponentModel.PropertyChangedEventArgs("Table1"))
    RaiseEvent PropertyChanged(Me, New ComponentModel.PropertyChangedEventArgs("ChangesDetected"))

    MessageBox.Show(String.Format("Changes Detected: {0}", _changesDetected), "Testing")
End Sub

Private Sub RowModifiedHandler(ByVal id As Guid)
    _changesDetected += 1
End Sub

Private Function SimulateGetDataFromDatabase() As System.Data.DataTable
    Dim dbtable = _table1.Copy
    If dbtable.Rows.Count > 0 Then
        dbtable.Rows(0)(1) = String.Format("Change in Database...Old RowVersion: {0}", dbtable.Rows(0)("RowVersion")) 'simulating a change in data retrieved from the database for the first row

        Dim newRVersion As Integer 'Whenever a row is updated the row version is incremented in the database
        If dbtable.Rows.Count > dbtable.Rows(0)("RowVersion") Then
            newRVersion = dbtable.Rows.Count + 1
        Else
            newRVersion = dbtable.Rows(0)("RowVersion") + 1
        End If
        dbtable.Rows(0)(2) = newRVersion
    End If
    dbtable.AcceptChanges()

    Return dbtable
End Function

Private Function CreateTableWithData() As System.Data.DataTable
    Dim newTable As New System.Data.DataTable
    Dim columnID As New System.Data.DataColumn("ID", GetType(Guid))
    Dim columnA As New System.Data.DataColumn("ColumnA", GetType(String))
    Dim columnB As New System.Data.DataColumn("RowVersion", GetType(Integer))
    newTable.Columns.AddRange({columnID, columnA, columnB})
    newTable.PrimaryKey = {newTable.Columns(0)}
    For i = 0 To 5
        Dim dr = newTable.NewRow
        dr("ID") = Guid.NewGuid
        dr("ColumnA") = String.Format("Column A Row {0}", i.ToString)
        dr("RowVersion") = i
        newTable.Rows.Add(dr)
    Next
    Return newTable
End Function

Public Event PropertyChanged(sender As Object, e As System.ComponentModel.PropertyChangedEventArgs) Implements System.ComponentModel.INotifyPropertyChanged.PropertyChanged
End Class

Thank you @Bjørn-Roger Kringsjå for helping me figure out how to properly merge data tables in this thread: Merge two identical DataTables results in DataRowState.Modified

I really hope this helps others to understand this topic.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top