Optimierung von SQL-Caching und dynamischen Editoren

Ein neuer Shared-Cache (`_ResolvedSqlCache`) wurde eingeführt, um die Performance bei der Verarbeitung von SQL-Platzhaltern zu verbessern. Die Methode `ResolveSqlTemplate` nutzt diesen Cache, um SQL-Templates effizienter zu verarbeiten.

Die neue Hilfsfunktion `ConvertToSqlValue` ermöglicht eine sichere und wiederverwendbare Konvertierung von Zellwerten in SQL-kompatible Strings.

Die visuelle Kennzeichnung dynamischer Editoren wurde durch den `CustomDrawCell`-Eventhandler verbessert, indem leere Zellen grau eingefärbt werden. Änderungen in dynamischen Spalten führen nun zu gezielter Cache-Invalidierung und asynchronem Grid-Refresh.

Die Logging-Strategie wurde überarbeitet, um präzisere Informationen zu liefern. Fehlerbehandlung und Benutzerkommunikation wurden erweitert, um die Stabilität und Nachvollziehbarkeit zu erhöhen.

Zusätzlich wurden allgemeine Code-Optimierungen vorgenommen, um die Lesbarkeit und Wartbarkeit zu verbessern.
This commit is contained in:
Developer01
2026-06-16 08:10:16 +02:00
parent 3e7d700536
commit 0cc7fe45d3
2 changed files with 1231 additions and 4118 deletions

View File

@@ -30,6 +30,7 @@ Namespace ControlCreator
Private _FormulaSqlColumns As New Dictionary(Of String, FormulaSqlDefinition)(StringComparer.OrdinalIgnoreCase)
Private _DynamicEditorColumns As New HashSet(Of String)(StringComparer.OrdinalIgnoreCase)
Private Shared _DynamicEditorCacheShared As New Dictionary(Of String, RepositoryItem)
Private Shared _ResolvedSqlCache As New Dictionary(Of String, String)
Private _isRefreshingFormula As Boolean = False ' *** NEU: Flag für Formel-Refresh ***
Private _currencySymbol As String = ""
@@ -82,45 +83,63 @@ Namespace ControlCreator
''' </summary>
Private Function ResolveSqlTemplate(sqlTemplate As String, pView As GridView, rowHandle As Integer) As String
Dim resolvedSql As String = sqlTemplate
' *** SCHRITT 1: {#TBCOL#...} Platzhalter ersetzen (BESTEHENDER CODE) ***
' *** SCHRITT 1: {#TBCOL#...} Platzhalter ersetzen ***
Dim pattern As String = "\{#TBCOL#([^}]+)\}"
Dim matches = Regex.Matches(sqlTemplate, pattern)
For Each match As Match In matches
Dim colName = match.Groups(1).Value
Dim cellValue = pView.GetRowCellValue(rowHandle, colName)
Dim safeValue As String
If cellValue Is Nothing OrElse IsDBNull(cellValue) Then
safeValue = "NULL"
ElseIf TypeOf cellValue Is String Then
' SQL-Injection-Schutz: Einfache Anführungszeichen escapen
safeValue = cellValue.ToString().Replace("'", "''")
ElseIf TypeOf cellValue Is Boolean Then
safeValue = If(CBool(cellValue), "1", "0")
Else
' Numerische Werte: Invariant-Format (Punkt als Dezimaltrenner)
safeValue = Convert.ToString(cellValue, CultureInfo.InvariantCulture)
End If
Dim safeValue As String = ConvertToSqlValue(cellValue) ' Hilfsfunktion
resolvedSql = resolvedSql.Replace(match.Value, safeValue)
_Logger.Debug("Resolved SQL placeholder [{0}] with value [{1}] → {2}", match.Value, cellValue, safeValue)
Next
' *** SCHRITT 2: Weitere Patterns via clsPatterns ersetzen (NEU) ***
If _ParentControl IsNot Nothing Then
Try
_Logger.Debug("Applying clsPatterns.ReplaceAllValues() to SQL: {0}", resolvedSql)
resolvedSql = clsPatterns.ReplaceAllValues(resolvedSql, _ParentControl, True)
_Logger.Debug("After clsPatterns: {0}", resolvedSql)
Catch ex As Exception
_Logger.Warn("⚠️ clsPatterns.ReplaceAllValues() failed: {0}", ex.Message)
_Logger.Error(ex)
End Try
Else
_Logger.Debug("ParentControl is Nothing skipping clsPatterns.ReplaceAllValues()")
' *** SCHRITT 2: {#CTRL#...} via clsPatterns - MIT CACHE ***
If _ParentControl IsNot Nothing AndAlso resolvedSql.Contains("{#CTRL#") Then
' Cache-Key: Hash aus SQL + Control-Werten
Dim cacheKey = GenerateCacheKey(resolvedSql, _ParentControl)
SyncLock _ResolvedSqlCache
If _ResolvedSqlCache.ContainsKey(cacheKey) Then
resolvedSql = _ResolvedSqlCache(cacheKey)
' Kein Log → spart 200+ Zeilen
Else
' Nur bei Cache-Miss ReplaceAllValues aufrufen
resolvedSql = clsPatterns.ReplaceAllValues(resolvedSql, _ParentControl, True)
_ResolvedSqlCache(cacheKey) = resolvedSql
If LOG_HOTSPOTS Then _Logger.Debug("[ResolveSqlTemplate] ReplaceAllValues-Cache MISS")
End If
End SyncLock
End If
_Logger.Debug("Final resolved SQL: {0}", resolvedSql)
Return resolvedSql
End Function
''' <summary>
''' Konvertiert einen Zellwert in einen SQL-sicheren String (für {#TBCOL#...} Platzhalter).
''' </summary>
Private Function ConvertToSqlValue(cellValue As Object) As String
If cellValue Is Nothing OrElse IsDBNull(cellValue) Then
Return "NULL"
ElseIf TypeOf cellValue Is String Then
' SQL-Injection-Schutz: Einfache Anführungszeichen escapen
Return cellValue.ToString().Replace("'", "''")
ElseIf TypeOf cellValue Is Boolean Then
Return If(CBool(cellValue), "1", "0")
ElseIf TypeOf cellValue Is DateTime Then
' ISO-Format für SQL
Return CDate(cellValue).ToString("yyyy-MM-dd HH:mm:ss")
Else
' Numerische Werte: Invariant-Format (Punkt als Dezimaltrenner)
Return Convert.ToString(cellValue, CultureInfo.InvariantCulture)
End If
End Function
''' <summary>
''' Placeholder für Control-Value-Hashing.
''' </summary>
Private Function GenerateCacheKey(sql As String, parent As Control) As String
' Cache-Key basiert NUR auf aufgelöstem SQL (Control-Werte sind bereits drin)
Return sql.GetHashCode().ToString()
End Function
Public Sub New(pLogConfig As LogConfig, pGridTables As Dictionary(Of Integer, Dictionary(Of String, RepositoryItem)), pCurrencySymbol As String, pParentControl As Control)
_LogConfig = pLogConfig
_Logger = pLogConfig.GetLogger()
@@ -784,10 +803,9 @@ Namespace ControlCreator
If _DynamicEditorCacheShared.ContainsKey(cacheKey) Then
' ✅ CACHE HIT: Editor wiederverwenden
e.RepositoryItem = _DynamicEditorCacheShared(cacheKey)
_Logger.Debug("[CustomRowCellEdit] Using CACHED editor for [{0}] (CacheKey=[{1}])", e.Column.FieldName, cacheKey)
Else
' ❌ CACHE MISS: Neuen Editor erstellen
_Logger.Debug("[CustomRowCellEdit] Creating NEW row-specific editor for [{0}]", e.Column.FieldName)
_Logger.Info("[CustomRowCellEdit] 🆕 MISS: Creating editor for [{0}]", e.Column.FieldName)
Dim realEditor = CreateRowSpecificEditor(e.Column.FieldName, resolvedSql, oConnectionId, oIsAdvancedLookup)
@@ -795,7 +813,7 @@ Namespace ControlCreator
' *** IN CACHE SPEICHERN ***
_DynamicEditorCacheShared(cacheKey) = realEditor
e.RepositoryItem = realEditor
_Logger.Debug("[CustomRowCellEdit] CACHED new editor (Type=[{0}], CacheKey=[{1}])", realEditor.GetType().Name, cacheKey)
_Logger.Info("[CustomRowCellEdit] ✓ Cached [{0}] editor (Type=[{1}])", e.Column.FieldName, realEditor.GetType().Name)
Else
_Logger.Warn("[CustomRowCellEdit] CreateRowSpecificEditor returned Nothing for [{0}]", e.Column.FieldName)
End If
@@ -817,7 +835,35 @@ Namespace ControlCreator
_Logger.Error(ex)
End Try
End Sub
' *** NEU: Visuelle Kennzeichnung für dynamische Editoren ***
AddHandler pGridView.CustomDrawCell,
Sub(sender As Object, e As RowCellCustomDrawEventArgs)
Try
' Nur für dynamische Editor-Spalten
If Not _DynamicEditorColumns.Contains(e.Column.FieldName) Then Return
' Zellwert abrufen
Dim cellValue = pGridView.GetRowCellValue(e.RowHandle, e.Column.FieldName)
' Wenn Zelle LEER ist → grau einfärben
If cellValue Is Nothing OrElse IsDBNull(cellValue) OrElse String.IsNullOrWhiteSpace(cellValue.ToString()) Then
' Helles Grau als Hintergrund
e.Appearance.BackColor = Color.FromArgb(240, 240, 240)
e.Appearance.ForeColor = Color.Gray
Else
' Wert vorhanden → Standardfarbe (nur bei Fokus-Wechsel)
' WICHTIG: Nicht überschreiben, wenn Zelle selektiert ist!
If Not e.Appearance.BackColor.Equals(SystemColors.Highlight) Then
e.Appearance.BackColor = Color.White
e.Appearance.ForeColor = Color.Black
End If
End If
Catch ex As Exception
_Logger.Error("[CustomDrawCell] Error: {0}", ex.Message)
_Logger.Error(ex)
End Try
End Sub
AddHandler pGridView.ValidatingEditor, Sub(sender As Object, e As BaseContainerValidateEditorEventArgs)
Dim oRow As DataRowView = pGridView.GetRow(pGridView.FocusedRowHandle)
Dim oColumnName = pGridView.FocusedColumn.FieldName
@@ -1001,15 +1047,127 @@ CheckNewItemRow:
AddHandler pGridView.CellValueChanged,
Sub(sender As Object, e As CellValueChangedEventArgs)
' *** HandleInheritedColumnValue MUSS zuerst aufgerufen werden ***
' *** 1. HandleInheritedColumnValue ZUERST ***
Try
HandleInheritedColumnValue(TryCast(sender, GridView), pColumnTable, e)
Catch ex As Exception
_Logger.Error(ex)
End Try
' *** FORMULA_EXPRESSION-Refresh via CellValueChanged (FALLBACK) ***
' (EditValueChanged macht das normalerweise schon LIVE)
' *** 2. Cache-Invalidierung für dynamische Editoren ***
Try
Dim oView As GridView = TryCast(sender, GridView)
If oView Is Nothing OrElse e.Column Is Nothing Then Return
' Finde alle dynamischen Spalten, die die geänderte Spalte via {#TBCOL#...} referenzieren
Dim oDependentDynamicColumns As New List(Of String)
For Each dynColName In _DynamicEditorColumns
Dim oColDef As DataRow = pColumnTable.
Select($"SPALTENNAME = '{dynColName}'").
FirstOrDefault()
If oColDef Is Nothing Then Continue For
Dim oSqlCommand As String = oColDef.ItemEx("SQL_COMMAND", "")
' Prüfe ob SQL {#TBCOL#<ColumnName>} enthält
If oSqlCommand.Contains($"{{#TBCOL#{e.Column.FieldName}}}") Then
oDependentDynamicColumns.Add(dynColName)
_Logger.Debug("[CellValueChanged] Spalte [{0}] referenziert via #TBCOL# [{1}] → Cache invalidieren",
dynColName, e.Column.FieldName)
End If
Next
' Cache-Einträge für ALLE betroffenen Zeilen löschen
If oDependentDynamicColumns.Count > 0 Then
SyncLock _DynamicEditorCacheShared
Dim oKeysToRemove As New List(Of String)
For Each cacheKey In _DynamicEditorCacheShared.Keys
For Each depCol In oDependentDynamicColumns
' Cache-Key-Format: "ColumnName|HashCode"
If cacheKey.StartsWith(depCol & "|") Then
oKeysToRemove.Add(cacheKey)
Exit For
End If
Next
Next
' Cache-Keys löschen
For Each keyToRemove In oKeysToRemove
_DynamicEditorCacheShared.Remove(keyToRemove)
_Logger.Info("[CellValueChanged] ♻️ Cache invalidiert: [{0}]", keyToRemove)
Next
If oKeysToRemove.Count > 0 Then
_Logger.Info("[CellValueChanged] ♻️ Gesamt: {0} Cache-Einträge für [{1}] gelöscht",
oKeysToRemove.Count, e.Column.FieldName)
End If
End SyncLock
End If
Catch ex As Exception
_Logger.Error("[CellValueChanged] Cache-Invalidierung fehlgeschlagen: {0}", ex.Message)
_Logger.Error(ex)
End Try
' *** Block 2.5: Grid-Invalidierung für dynamische Editoren ***
Try
Dim oView As GridView = TryCast(sender, GridView)
If oView Is Nothing OrElse e.Column Is Nothing Then Return
' Finde alle dynamischen Spalten, die die geänderte Spalte via {#TBCOL#...} referenzieren
Dim oDependentDynamicColumns As New List(Of String)
For Each dynColName In _DynamicEditorColumns
Dim oColDef As DataRow = pColumnTable.
Select($"SPALTENNAME = '{dynColName}'").
FirstOrDefault()
If oColDef Is Nothing Then Continue For
Dim oSqlCommand As String = oColDef.ItemEx("SQL_COMMAND", "")
' Prüfe ob SQL {#TBCOL#<ColumnName>} enthält
If oSqlCommand.Contains($"{{#TBCOL#{e.Column.FieldName}}}") Then
oDependentDynamicColumns.Add(dynColName)
End If
Next
' *** KERN-FIX: Grid-Refresh OHNE Cache-Duplikat ***
If oDependentDynamicColumns.Count > 0 Then
_Logger.Debug("[CellValueChanged] Invalidiere {0} Zeilen für abhängige Spalten: [{1}]",
oView.DataRowCount, String.Join(", ", oDependentDynamicColumns))
' BeginInvoke: Grid-Refresh NACH allen CellValueChanged-Events
oView.GridControl.BeginInvoke(New Action(
Sub()
Try
For rowHandle As Integer = 0 To oView.DataRowCount - 1
For Each depCol In oDependentDynamicColumns
Dim oGridColumn = oView.Columns.ColumnByFieldName(depCol)
If oGridColumn IsNot Nothing Then
oView.RefreshRowCell(rowHandle, oGridColumn)
End If
Next
Next
_Logger.Info("[CellValueChanged] ✓ Grid-Invalidierung abgeschlossen: {0} Zeilen, {1} Spalten",
oView.DataRowCount, oDependentDynamicColumns.Count)
Catch ex As Exception
_Logger.Error("[CellValueChanged] Grid-Invalidierung fehlgeschlagen: {0}", ex.Message)
_Logger.Error(ex)
End Try
End Sub))
End If
Catch ex As Exception
_Logger.Error("[CellValueChanged] Grid-Invalidierung fehlgeschlagen: {0}", ex.Message)
_Logger.Error(ex)
End Try
' *** 3. FORMULA_EXPRESSION-Refresh via CellValueChanged (FALLBACK) ***
Try
Dim oView As GridView = TryCast(sender, GridView)
If oView Is Nothing OrElse e.Column Is Nothing Then Return
@@ -1031,7 +1189,7 @@ CheckNewItemRow:
Next
If oFormulaColumnsToRefresh.Count = 0 Then
' Kein FORMULA_EXPRESSION-Refresh nötig weiter zu FORMULA_SQL
' Kein FORMULA_EXPRESSION-Refresh nötig
Else
Dim oRowHandle As Integer = e.RowHandle
_Logger.Debug("[FormulaRefresh] CellValueChanged FALLBACK refreshing EXPRESSION columns for row [{0}] after column [{1}] changed.", oRowHandle, e.Column.FieldName)
@@ -1049,7 +1207,7 @@ CheckNewItemRow:
_Logger.Debug("[FormulaRefresh] FALLBACK DisplayText for [{0}]: [{1}]",
oFormulaColumnName, oView.GetRowCellDisplayText(oRowHandle, oGridColumn))
Next
' Dies fängt den Fall ab, dass eine SQL-Spalte eine Expression-Spalte referenziert
' SQL-Formeln die Expression-Spalten referenzieren auch triggern
TriggerSqlFormulasAfterExpressionUpdate(oView, oRowHandle, oFormulaColumnsToRefresh)
Catch ex As Exception
_Logger.Error(ex)
@@ -1061,8 +1219,7 @@ CheckNewItemRow:
_Logger.Error(ex)
End Try
' *** FORMULA_SQL-Refresh via CellValueChanged ***
' SQL wird NUR hier ausgeführt (nicht in EditValueChanged) um DB-Roundtrips zu minimieren
' *** 4. FORMULA_SQL-Refresh via CellValueChanged ***
Try
Dim oView As GridView = TryCast(sender, GridView)
If oView Is Nothing OrElse e.Column Is Nothing Then Return
@@ -1082,7 +1239,7 @@ CheckNewItemRow:
_Logger.Debug("[FormulaSql] CellValueChanged column [{0}] triggers SQL refresh for: [{1}]",
e.Column.FieldName, String.Join(", ", oSqlColumnsToRefresh))
' BeginInvoke: UI nicht blockieren, GridView in stabilem Zustand
' BeginInvoke: UI nicht blockieren
oView.GridControl.BeginInvoke(New Action(
Sub()
Try
@@ -1110,7 +1267,9 @@ CheckNewItemRow:
Try
Dim oResultTable As DataTable = DatabaseFallback.GetDatatable(
New GetDatatableOptions(resolvedSql, DatabaseType.ECM))
New GetDatatableOptions(resolvedSql, DatabaseType.ECM) With {
.ConnectionId = oDefinition.ConnectionId
})
If oResultTable IsNot Nothing AndAlso oResultTable.Rows.Count > 0 Then
Dim oResult = oResultTable.Rows(0).Item(0)
@@ -1141,6 +1300,18 @@ CheckNewItemRow:
Catch ex As Exception
_Logger.Error(ex)
End Try
' *** 5. Dynamische Editor-Invalidierung (visuell) ***
Try
Dim oView As GridView = TryCast(sender, GridView)
If oView IsNot Nothing AndAlso e.Column IsNot Nothing Then
If _DynamicEditorColumns.Contains(e.Column.FieldName) Then
oView.InvalidateRow(e.RowHandle)
End If
End If
Catch ex As Exception
_Logger.Error(ex)
End Try
End Sub
End Sub
''' <summary>
@@ -1382,6 +1553,11 @@ CheckNewItemRow:
End If
If isApplyingInheritedValue OrElse pArgs.RowHandle = DevExpress.XtraGrid.GridControl.InvalidRowHandle Then
If isApplyingInheritedValue Then
_Logger.Debug("Currently applying inherited value for column {0} skipping to prevent recursion.", pArgs.Column.FieldName)
Else
_Logger.Debug("Invalid RowHandle ({0}) skipping HandleInheritedColumnValue.", pArgs.RowHandle)
End If
Return
End If
@@ -1425,9 +1601,9 @@ CheckNewItemRow:
affectedRowsCount)
If USER_LANGUAGE <> "de-DE" Then
confirmMessage = String.Format(
"Do you want to inherit the value '{0}' to {1} subsequent row(s)?",
valueToApply,
affectedRowsCount)
"Do you want to inherit the value '{0}' to {1} subsequent row(s)?",
valueToApply,
affectedRowsCount)
End If
Dim confirmTitle As String = "Wertevererbung bestätigen"
If USER_LANGUAGE <> "de-DE" Then
@@ -1483,9 +1659,14 @@ CheckNewItemRow:
_Logger.Info("Skipping confirmation dialog (already confirmed {0} times)", confirmationEntry.Count)
End If
' *** NEU: Cache-Keys sammeln WÄHREND der Vererbung ***
Dim oCacheKeysToInvalidate As New List(Of String)
isApplyingInheritedValue = True
Try
_Logger.Info(String.Format("Inherit Value is active for column. So inheritting the value [{0}]...", valueToApply))
_Logger.Info(String.Format("Inherit Value is active for column [{0}]. Inheriting value [{1}] to {2} rows...",
pArgs.Column.FieldName, valueToApply, affectedRowsCount))
For dataIndex As Integer = listIndex + 1 To pView.DataRowCount - 1
Dim targetHandle = pView.GetRowHandle(dataIndex)
If targetHandle = DevExpress.XtraGrid.GridControl.InvalidRowHandle OrElse pView.IsGroupRow(targetHandle) Then
@@ -1501,12 +1682,62 @@ CheckNewItemRow:
Continue For
End If
' *** SCHRITT 1: Wert setzen ***
pView.SetRowCellValue(targetHandle, pArgs.Column.FieldName, valueToApply)
' *** SCHRITT 2: Cache-Keys für DIESE Zeile sammeln ***
' Finde alle dynamischen Spalten, die die geänderte Spalte via {#TBCOL#...} referenzieren
For Each dynColName In _DynamicEditorColumns
Dim oColDef As DataRow = pColumnDefinition.
Select($"SPALTENNAME = '{dynColName}'").
FirstOrDefault()
If oColDef Is Nothing Then Continue For
Dim oSqlCommand As String = oColDef.ItemEx("SQL_COMMAND", "")
' Prüfe ob SQL {#TBCOL#<ColumnName>} enthält
If oSqlCommand.Contains($"{{#TBCOL#{pArgs.Column.FieldName}}}") Then
' SQL auflösen mit NEUEM Wert (targetHandle!)
Try
Dim resolvedSql = ResolveSqlTemplate(oSqlCommand, pView, targetHandle)
Dim cacheKey = $"{dynColName}|{resolvedSql.GetHashCode()}"
If Not oCacheKeysToInvalidate.Contains(cacheKey) Then
oCacheKeysToInvalidate.Add(cacheKey)
_Logger.Debug("[HandleInheritedColumnValue] Marked for invalidation: [{0}] (Row {1})",
cacheKey, targetHandle)
End If
Catch ex As Exception
_Logger.Error("[HandleInheritedColumnValue] Failed to resolve SQL for column [{0}], row {1}: {2}",
dynColName, targetHandle, ex.Message)
End Try
End If
Next
Next
' *** SCHRITT 3: Cache SOFORT invalidieren BEVOR Grid refresht wird ***
If oCacheKeysToInvalidate.Count > 0 Then
_Logger.Info("[HandleInheritedColumnValue] ♻️ Invalidating {0} cache entries BEFORE grid refresh...",
oCacheKeysToInvalidate.Count)
SyncLock _DynamicEditorCacheShared
For Each keyToRemove In oCacheKeysToInvalidate
If _DynamicEditorCacheShared.ContainsKey(keyToRemove) Then
_DynamicEditorCacheShared.Remove(keyToRemove)
_Logger.Debug("[HandleInheritedColumnValue] ✓ Cache invalidated (IMMEDIATE): [{0}]", keyToRemove)
End If
Next
End SyncLock
_Logger.Info("[HandleInheritedColumnValue] ✓ Cache invalidation complete. User can now click cells safely.")
End If
Finally
isApplyingInheritedValue = False
End Try
End Sub
Private Sub SetInheritanceConfirmationCount(columnName As String, newCount As Integer)
Dim entries = UserInheritance_ConfirmationByColumn
If entries Is Nothing Then

File diff suppressed because it is too large Load Diff