diff --git a/BackEnd/Data Access/Tables/DefaultTable.vb b/BackEnd/Data Access/Tables/DefaultTable.vb
index c8e448b..9b6398f 100644
--- a/BackEnd/Data Access/Tables/DefaultTable.vb
+++ b/BackEnd/Data Access/Tables/DefaultTable.vb
@@ -36,6 +36,11 @@ Public Class DefaultTable
Dim fileName As String = _table.GetStringProperty(TableProperty.FileName)
Dim filePath As String = GetFilePath(fileName)
+ ' Validate that the XML file exists before attempting to load
+ If Not System.IO.File.Exists(filePath) Then
+ Throw New System.IO.FileNotFoundException($"XML file not found: {filePath}", fileName)
+ End If
+
Dim sourceName As String = _table.GetStringProperty(TableProperty.SourceTableName)
If sourceName Is Nothing Then
'JMich Adding support for different rank levels of equipment
diff --git a/BackEnd/Data Access/XmlDB.vb b/BackEnd/Data Access/XmlDB.vb
index 0ba679a..10a49f2 100644
--- a/BackEnd/Data Access/XmlDB.vb
+++ b/BackEnd/Data Access/XmlDB.vb
@@ -154,25 +154,61 @@ Public Class XmlDB
Protected Sub EndInit()
ds.AcceptChanges()
+
+ ' Proactive validation before enforcing constraints
+ Dim validator As New XmlValidator(ds)
+ If Not validator.ValidateAll() Then
+ ' Show detailed validation errors
+ Dim errorMessage As String = validator.GetFormattedErrors()
+ ErrorHandler.ShowError(errorMessage, "XML Validation Failed", MessageBoxIcon.Error)
+ ErrorHandler.TriggerFatalError()
+ Return
+ End If
+
Try
ds.EnforceConstraints = True
Catch ex As ConstraintException
- ErrorHandler.ShowError("One or more of your files contain invalid data. Please fix the data and restart the editor.", "Error Loading Files", ex)
+ ' This is now a fallback - should rarely be hit due to proactive validation
+ Dim errMsg As New Text.StringBuilder()
+ errMsg.AppendLine("Constraint validation failed after proactive checks.")
+ errMsg.AppendLine("This may indicate a data race condition or complex constraint issue.")
+ errMsg.AppendLine()
+ errMsg.AppendLine("Error Details:")
+ errMsg.AppendLine(ex.Message)
+ errMsg.AppendLine()
+
For Each t As DataTable In ds.Tables
If t.HasErrors Then
- Dim errStr As New Text.StringBuilder("Details:" & vbCrLf & vbCrLf)
Dim fileName As String = t.GetStringProperty(TableProperty.FileName)
If Not String.IsNullOrEmpty(fileName) Then
- errStr.Append("File: " & fileName & vbCrLf)
+ errMsg.AppendLine("File: " & fileName)
Else
- errStr.Append("Table: " & t.TableName & vbCrLf)
+ errMsg.AppendLine("Table: " & t.TableName)
End If
- For i As Integer = 0 To t.GetErrors.GetUpperBound(0)
- errStr.Append(vbCrLf & t.GetErrors(i).RowError)
+
+ ' Enhanced error reporting with row numbers and column info
+ Dim errorRows As DataRow() = t.GetErrors()
+ For i As Integer = 0 To Math.Min(errorRows.GetUpperBound(0), 9) ' Limit to 10 errors
+ Dim row As DataRow = errorRows(i)
+ Dim rowIndex As Integer = t.Rows.IndexOf(row)
+ errMsg.AppendLine($" Row {rowIndex + 1}: {row.RowError}")
+
+ ' Show which columns have errors
+ For Each col As DataColumn In t.Columns
+ If Not String.IsNullOrEmpty(row.GetColumnError(col)) Then
+ errMsg.AppendLine($" Column '{col.ColumnName}': {row.GetColumnError(col)}")
+ End If
+ Next
Next
- ErrorHandler.ShowError(errStr.ToString, "Error Loading Files", MessageBoxIcon.Exclamation)
+
+ If errorRows.Length > 10 Then
+ errMsg.AppendLine($" ... and {errorRows.Length - 10} more error(s)")
+ End If
+ errMsg.AppendLine()
End If
Next
+
+ ErrorHandler.ShowError(errMsg.ToString(), "Error Loading Files", MessageBoxIcon.Error, ex)
ErrorHandler.TriggerFatalError()
End Try
ds.EndInit()
@@ -211,8 +247,19 @@ Public Class XmlDB
End If
If loadData Then
- RaiseEvent LoadingTable(Me, fileName)
- table.GetTableHandler.LoadData()
+ Try
+ RaiseEvent LoadingTable(Me, fileName)
+ table.GetTableHandler.LoadData()
+ Catch ex As System.IO.FileNotFoundException
+ ErrorHandler.ShowError($"Failed to load '{fileName}': File not found.{vbCrLf}{vbCrLf}Path: {ex.FileName}", "File Not Found", MessageBoxIcon.Error, ex)
+ Throw
+ Catch ex As Xml.XmlException
+ ErrorHandler.ShowError($"Failed to load '{fileName}': Invalid XML format.{vbCrLf}{vbCrLf}Line {ex.LineNumber}, Position {ex.LinePosition}: {ex.Message}", "XML Format Error", MessageBoxIcon.Error, ex)
+ Throw
+ Catch ex As Exception
+ ErrorHandler.ShowError($"Failed to load '{fileName}': {ex.Message}{vbCrLf}{vbCrLf}Table: {table.TableName}", "Error Loading Table", MessageBoxIcon.Error, ex)
+ Throw
+ End Try
End If
End If
diff --git a/BackEnd/Data Access/XmlValidator.vb b/BackEnd/Data Access/XmlValidator.vb
new file mode 100644
index 0000000..494ce74
--- /dev/null
+++ b/BackEnd/Data Access/XmlValidator.vb
@@ -0,0 +1,276 @@
+Imports System.Data
+Imports System.Text
+
+Public Class XmlValidator
+ Private _dataSet As DataSet
+ Private _validationErrors As New List(Of ValidationError)
+
+ Public Sub New(dataSet As DataSet)
+ _dataSet = dataSet
+ End Sub
+
+ Public ReadOnly Property HasErrors As Boolean
+ Get
+ Return _validationErrors.Count > 0
+ End Get
+ End Property
+
+ Public ReadOnly Property Errors As List(Of ValidationError)
+ Get
+ Return _validationErrors
+ End Get
+ End ReadOnly Property
+
+ '''
+ ''' Validates all data in the dataset before constraints are enforced
+ '''
+ Public Function ValidateAll() As Boolean
+ _validationErrors.Clear()
+
+ ' Check for null/empty required fields
+ ValidateRequiredFields()
+
+ ' Check for duplicate primary keys
+ ValidatePrimaryKeys()
+
+ ' Check for foreign key violations
+ ValidateForeignKeys()
+
+ ' Check for unique constraint violations
+ ValidateUniqueConstraints()
+
+ Return Not HasErrors
+ End Function
+
+ '''
+ ''' Validates a single table before loading
+ '''
+ Public Function ValidateTable(table As DataTable) As Boolean
+ Dim startErrorCount = _validationErrors.Count
+
+ ValidateTableRequiredFields(table)
+ ValidateTablePrimaryKeys(table)
+ ValidateTableForeignKeys(table)
+ ValidateTableUniqueConstraints(table)
+
+ Return _validationErrors.Count = startErrorCount
+ End Function
+
+ Private Sub ValidateRequiredFields()
+ For Each table As DataTable In _dataSet.Tables
+ ValidateTableRequiredFields(table)
+ Next
+ End Sub
+
+ Private Sub ValidateTableRequiredFields(table As DataTable)
+ For Each col As DataColumn In table.Columns
+ If Not col.AllowDBNull AndAlso Not col.AutoIncrement Then
+ For i As Integer = 0 To table.Rows.Count - 1
+ Dim row As DataRow = table.Rows(i)
+ If row.RowState <> DataRowState.Deleted AndAlso IsDBNull(row(col)) Then
+ AddError(table, i, col.ColumnName,
+ "Required field is empty",
+ ValidationErrorType.RequiredField)
+ End If
+ Next
+ End If
+ Next
+ End Sub
+
+ Private Sub ValidatePrimaryKeys()
+ For Each table As DataTable In _dataSet.Tables
+ ValidateTablePrimaryKeys(table)
+ Next
+ End Sub
+
+ Private Sub ValidateTablePrimaryKeys(table As DataTable)
+ If table.PrimaryKey.Length = 0 Then Return
+
+ Dim pkValues As New Dictionary(Of String, Integer)
+ Dim pkColumns As String = String.Join(", ", table.PrimaryKey.Select(Function(c) c.ColumnName))
+
+ For i As Integer = 0 To table.Rows.Count - 1
+ Dim row As DataRow = table.Rows(i)
+ If row.RowState = DataRowState.Deleted Then Continue For
+
+ Dim keyValue As String = GetPrimaryKeyValue(row)
+ If pkValues.ContainsKey(keyValue) Then
+ AddError(table, i, pkColumns,
+ $"Duplicate primary key value: {keyValue}. First occurrence at row {pkValues(keyValue) + 1}",
+ ValidationErrorType.DuplicatePrimaryKey)
+ Else
+ pkValues.Add(keyValue, i)
+ End If
+ Next
+ End Sub
+
+ Private Sub ValidateForeignKeys()
+ For Each table As DataTable In _dataSet.Tables
+ ValidateTableForeignKeys(table)
+ Next
+ End Sub
+
+ Private Sub ValidateTableForeignKeys(table As DataTable)
+ For Each fk As ForeignKeyConstraint In table.Constraints.OfType(Of ForeignKeyConstraint)()
+ Dim parentTable As DataTable = fk.RelatedTable
+ Dim childColumns As String = String.Join(", ", fk.Columns.Select(Function(c) c.ColumnName))
+ Dim parentColumns As String = String.Join(", ", fk.RelatedColumns.Select(Function(c) c.ColumnName))
+
+ For i As Integer = 0 To table.Rows.Count - 1
+ Dim row As DataRow = table.Rows(i)
+ If row.RowState = DataRowState.Deleted Then Continue For
+
+ ' Build foreign key value
+ Dim fkValues(fk.Columns.Length - 1) As Object
+ For j As Integer = 0 To fk.Columns.Length - 1
+ fkValues(j) = row(fk.Columns(j))
+ Next
+
+ ' Check if the value exists in parent table
+ Dim parentRow As DataRow = parentTable.Rows.Find(fkValues)
+ If parentRow Is Nothing AndAlso Not IsNullForeignKey(fkValues) Then
+ Dim fkValueStr As String = String.Join(", ", fkValues.Select(Function(v) If(v Is Nothing, "NULL", v.ToString())))
+ AddError(table, i, childColumns,
+ $"Foreign key violation: Value '{fkValueStr}' does not exist in table '{parentTable.TableName}' column(s) '{parentColumns}'",
+ ValidationErrorType.ForeignKeyViolation)
+ End If
+ Next
+ Next
+ End Sub
+
+ Private Sub ValidateUniqueConstraints()
+ For Each table As DataTable In _dataSet.Tables
+ ValidateTableUniqueConstraints(table)
+ Next
+ End Sub
+
+ Private Sub ValidateTableUniqueConstraints(table As DataTable)
+ For Each uc As UniqueConstraint In table.Constraints.OfType(Of UniqueConstraint)()
+ If uc.IsPrimaryKey Then Continue For ' Already checked
+
+ Dim uniqueValues As New Dictionary(Of String, Integer)
+ Dim columnNames As String = String.Join(", ", uc.Columns.Select(Function(c) c.ColumnName))
+
+ For i As Integer = 0 To table.Rows.Count - 1
+ Dim row As DataRow = table.Rows(i)
+ If row.RowState = DataRowState.Deleted Then Continue For
+
+ Dim keyValue As String = GetUniqueConstraintValue(row, uc.Columns)
+ If uniqueValues.ContainsKey(keyValue) Then
+ AddError(table, i, columnNames,
+ $"Unique constraint violation: Duplicate value '{keyValue}'. First occurrence at row {uniqueValues(keyValue) + 1}",
+ ValidationErrorType.UniqueConstraintViolation)
+ Else
+ uniqueValues.Add(keyValue, i)
+ End If
+ Next
+ Next
+ End Sub
+
+ Private Function GetPrimaryKeyValue(row As DataRow) As String
+ Dim values As New List(Of String)
+ For Each col As DataColumn In row.Table.PrimaryKey
+ values.Add(If(row(col) Is Nothing, "NULL", row(col).ToString()))
+ Next
+ Return String.Join("|", values)
+ End Function
+
+ Private Function GetUniqueConstraintValue(row As DataRow, columns As DataColumn()) As String
+ Dim values As New List(Of String)
+ For Each col As DataColumn In columns
+ values.Add(If(row(col) Is Nothing, "NULL", row(col).ToString()))
+ Next
+ Return String.Join("|", values)
+ End Function
+
+ Private Function IsNullForeignKey(values As Object()) As Boolean
+ For Each v In values
+ If v Is Nothing OrElse IsDBNull(v) Then Return True
+ Next
+ Return False
+ End Function
+
+ Private Sub AddError(table As DataTable, rowIndex As Integer, columnName As String, message As String, errorType As ValidationErrorType)
+ Dim fileName As String = table.GetStringProperty(TableProperty.FileName)
+ If String.IsNullOrEmpty(fileName) Then fileName = table.TableName
+
+ _validationErrors.Add(New ValidationError() With {
+ .FileName = fileName,
+ .TableName = table.TableName,
+ .RowIndex = rowIndex,
+ .ColumnName = columnName,
+ .Message = message,
+ .ErrorType = errorType
+ })
+ End Sub
+
+ Public Function GetFormattedErrors() As String
+ If Not HasErrors Then Return String.Empty
+
+ Dim sb As New StringBuilder()
+ sb.AppendLine("XML Validation Errors Found:")
+ sb.AppendLine()
+
+ ' Group errors by file
+ Dim errorsByFile = _validationErrors.GroupBy(Function(e) e.FileName)
+
+ For Each fileGroup In errorsByFile
+ sb.AppendLine($"File: {fileGroup.Key}")
+ sb.AppendLine(New String("-"c, 60))
+
+ ' Group by error type
+ Dim errorsByType = fileGroup.GroupBy(Function(e) e.ErrorType)
+
+ For Each typeGroup In errorsByType
+ sb.AppendLine($" {GetErrorTypeDescription(typeGroup.Key)} ({typeGroup.Count()} error(s)):")
+
+ Dim maxErrors As Integer = 10 ' Limit display to first 10 errors per type
+ Dim displayCount As Integer = Math.Min(typeGroup.Count(), maxErrors)
+
+ For i As Integer = 0 To displayCount - 1
+ Dim err = typeGroup.ElementAt(i)
+ sb.AppendLine($" Row {err.RowIndex + 1}, Column '{err.ColumnName}': {err.Message}")
+ Next
+
+ If typeGroup.Count() > maxErrors Then
+ sb.AppendLine($" ... and {typeGroup.Count() - maxErrors} more error(s)")
+ End If
+ sb.AppendLine()
+ Next
+ Next
+
+ Return sb.ToString()
+ End Function
+
+ Private Function GetErrorTypeDescription(errorType As ValidationErrorType) As String
+ Select Case errorType
+ Case ValidationErrorType.RequiredField
+ Return "Required Field Errors"
+ Case ValidationErrorType.DuplicatePrimaryKey
+ Return "Duplicate Primary Key Errors"
+ Case ValidationErrorType.ForeignKeyViolation
+ Return "Foreign Key Violations"
+ Case ValidationErrorType.UniqueConstraintViolation
+ Return "Unique Constraint Violations"
+ Case Else
+ Return "Other Errors"
+ End Select
+ End Function
+End Class
+
+Public Class ValidationError
+ Public Property FileName As String
+ Public Property TableName As String
+ Public Property RowIndex As Integer
+ Public Property ColumnName As String
+ Public Property Message As String
+ Public Property ErrorType As ValidationErrorType
+End Class
+
+Public Enum ValidationErrorType
+ RequiredField
+ DuplicatePrimaryKey
+ ForeignKeyViolation
+ UniqueConstraintViolation
+ Other
+End Enum