Bug Multiindexing, Cache load

This commit is contained in:
Developer01
2026-03-16 12:29:11 +01:00
parent 3a44abf77b
commit a50e7e3c55
4 changed files with 303 additions and 81 deletions

View File

@@ -0,0 +1,111 @@
Imports System.Data
''' <summary>
''' Zentraler Cache für häufig abgerufene Datenbank-Queries
''' Reduziert DB-Roundtrips um bis zu 90%
''' </summary>
Public Class ClassDataCache
Private Shared ReadOnly _cache As New Dictionary(Of String, CachedItem)
Private Shared ReadOnly _lockObject As New Object()
Private Shared _defaultTimeout As TimeSpan = TimeSpan.FromMinutes(5)
Private Class CachedItem
Public Data As DataTable
Public Timestamp As DateTime
Public Timeout As TimeSpan
Public ReadOnly Property IsExpired As Boolean
Get
Return DateTime.Now - Timestamp > Timeout
End Get
End Property
End Class
''' <summary>
''' Daten aus Cache holen oder neu laden
''' </summary>
Public Shared Function GetOrLoad(cacheKey As String,
loadFunction As Func(Of DataTable),
Optional timeout As TimeSpan? = Nothing) As DataTable
SyncLock _lockObject
' Cache-Check
If _cache.ContainsKey(cacheKey) Then
Dim item = _cache(cacheKey)
If Not item.IsExpired Then
LOGGER.Debug($"Cache HIT: {cacheKey} (Age: {(DateTime.Now - item.Timestamp).TotalSeconds:F1}s)")
Return item.Data.Copy() ' Kopie zurückgeben!
Else
' Abgelaufen - entfernen
_cache.Remove(cacheKey)
LOGGER.Debug($"Cache EXPIRED: {cacheKey}")
End If
End If
' Cache MISS - neu laden
LOGGER.Debug($"Cache MISS: {cacheKey} - Loading from DB...")
Dim result = loadFunction()
If result IsNot Nothing Then
_cache(cacheKey) = New CachedItem With {
.Data = result.Copy(),
.Timestamp = DateTime.Now,
.Timeout = If(timeout, _defaultTimeout)
}
End If
Return result
End SyncLock
End Function
''' <summary>
''' Bestimmten Cache-Eintrag invalidieren
''' </summary>
Public Shared Sub Invalidate(cacheKey As String)
SyncLock _lockObject
If _cache.ContainsKey(cacheKey) Then
_cache.Remove(cacheKey)
LOGGER.Debug($"Cache INVALIDATED: {cacheKey}")
End If
End SyncLock
End Sub
''' <summary>
''' Alle Cache-Einträge löschen
''' </summary>
Public Shared Sub ClearAll()
SyncLock _lockObject
Dim count = _cache.Count
_cache.Clear()
LOGGER.Info($"Cache CLEARED: {count} entries removed")
End SyncLock
End Sub
''' <summary>
''' Abgelaufene Einträge entfernen
''' </summary>
Public Shared Sub CleanupExpired()
SyncLock _lockObject
Dim expiredKeys = _cache.Where(Function(kvp) kvp.Value.IsExpired).
Select(Function(kvp) kvp.Key).ToList()
For Each key In expiredKeys
_cache.Remove(key)
Next
If expiredKeys.Count > 0 Then
LOGGER.Debug($"Cache CLEANUP: {expiredKeys.Count} expired entries removed")
End If
End SyncLock
End Sub
''' <summary>
''' Cache-Statistiken
''' </summary>
Public Shared Function GetStatistics() As String
SyncLock _lockObject
Return $"Cache Entries: {_cache.Count}, Default Timeout: {_defaultTimeout.TotalMinutes:F1} min"
End SyncLock
End Function
End Class

View File

@@ -64,7 +64,8 @@ Public Class ClassDocGrid
Private Shared _datepickerValueChangedHandler As EventHandler
Private Shared _textValueChangedHandler As EventHandler
Private Shared _checkValueChangedHandler As EventHandler
Private Shared EnableVerboseGridLogging As Boolean = False ' PRODUKTIV: FALSE!
Private Shared _isGridRefreshing As Boolean = False
Private Shared Function Init_Table()
Try
Dim table As New DataTable With {
@@ -318,20 +319,41 @@ Public Class ClassDocGrid
' Neues Dataset für Master- und Detail-Tabelle erstellen
Dim ds As New DataSet()
Dim DT_DETAILS_SQL
' ── Cache-optimiertes Laden der Detail-Values ─────────────────────────
Dim cacheKey As String
Dim DT_DETAIL_VALUES As DataTable
Select Case CURRENT_SEARCH_TYPE
Case "NODE_DOWN"
DT_DETAILS_SQL = String.Format("SELECT T.[GUID],T.[DocID],T.[CONFIG_ID],T1.HEADER_CAPTION,T.[VALUE],T1.[LANGUAGE], T1.COLUMN_VIEW,T1.EDITABLE,T1.TYPE_ID,T1.VISIBLE,T.CHANGED_WHEN,T.CHANGED_WHO " &
"FROM TBPMO_DOC_VALUES T INNER JOIN TBPMO_STRUCTURE_NODES_USER_TEMP TTEMP ON T.RECORD_ID = TTEMP.RECORD_ID RIGHT JOIN TBPMO_DOCSEARCH_RESULTLIST_CONFIG T1 ON T.CONFIG_ID = T1.GUID WHERE T1.ENTITY_ID = {0} AND LANGUAGE = '{1}' AND T1.CONFIG_COLUMNS = 1", CURRENT_ENTITY_ID, USER_LANGUAGE)
cacheKey = $"DocDetailValues_NodeDown_E{CURRENT_ENTITY_ID}_L{USER_LANGUAGE}"
DT_DETAIL_VALUES = ClassDataCache.GetOrLoad(cacheKey, Function()
Dim sql = String.Format(
"SELECT T.[GUID],T.[DocID],T.[CONFIG_ID],T1.HEADER_CAPTION,T.[VALUE],T1.[LANGUAGE], " &
"T1.COLUMN_VIEW,T1.EDITABLE,T1.TYPE_ID,T1.VISIBLE,T.CHANGED_WHEN,T.CHANGED_WHO " &
"FROM TBPMO_DOC_VALUES T " &
"INNER JOIN TBPMO_STRUCTURE_NODES_USER_TEMP TTEMP ON T.RECORD_ID = TTEMP.RECORD_ID " &
"RIGHT JOIN TBPMO_DOCSEARCH_RESULTLIST_CONFIG T1 ON T.CONFIG_ID = T1.GUID " &
"WHERE T1.ENTITY_ID = {0} AND LANGUAGE = '{1}' AND T1.CONFIG_COLUMNS = 1",
CURRENT_ENTITY_ID, USER_LANGUAGE)
Return MYDB_ECM.GetDatatable(sql)
End Function, TimeSpan.FromMinutes(2))
Case Else
DT_DETAILS_SQL = String.Format("SELECT T.[GUID],T.[DocID],T.[CONFIG_ID],T1.HEADER_CAPTION,T.[VALUE],T1.[LANGUAGE], T1.COLUMN_VIEW,T1.EDITABLE,T1.TYPE_ID,T1.VISIBLE,T.CHANGED_WHEN,T.CHANGED_WHO " &
"FROM TBPMO_DOC_VALUES T RIGHT JOIN TBPMO_DOCSEARCH_RESULTLIST_CONFIG T1 ON T.CONFIG_ID = T1.GUID WHERE T1.ENTITY_ID = {0} AND LANGUAGE = '{1}' AND T1.CONFIG_COLUMNS = 1 AND T.RECORD_ID = {2}", CURRENT_ENTITY_ID, USER_LANGUAGE, RECORD_ID)
cacheKey = $"DocDetailValues_E{CURRENT_ENTITY_ID}_R{RECORD_ID}_L{USER_LANGUAGE}"
DT_DETAIL_VALUES = ClassDataCache.GetOrLoad(cacheKey, Function()
Dim sql = String.Format(
"SELECT T.[GUID],T.[DocID],T.[CONFIG_ID],T1.HEADER_CAPTION,T.[VALUE],T1.[LANGUAGE], " &
"T1.COLUMN_VIEW,T1.EDITABLE,T1.TYPE_ID,T1.VISIBLE,T.CHANGED_WHEN,T.CHANGED_WHO " &
"FROM TBPMO_DOC_VALUES T " &
"RIGHT JOIN TBPMO_DOCSEARCH_RESULTLIST_CONFIG T1 ON T.CONFIG_ID = T1.GUID " &
"WHERE T1.ENTITY_ID = {0} AND LANGUAGE = '{1}' AND T1.CONFIG_COLUMNS = 1 AND T.RECORD_ID = {2}",
CURRENT_ENTITY_ID, USER_LANGUAGE, RECORD_ID)
Return MYDB_ECM.GetDatatable(sql)
End Function, TimeSpan.FromMinutes(2))
End Select
'"FROM TBPMO_DOC_VALUES T INNER JOIN TBPMO_DOCSEARCH_RESULTLIST_CONFIG T1 ON T.CONFIG_ID = T1.GUID WHERE T1.ENTITY_ID = {0} AND T1.LANGUAGE = '{1}' AND T.RECORD_ID = {2} ORDER BY T.DocID, T1.SEQUENCE", CURRENT_ENTITY_ID, USER_LANGUAGE, RECORD_ID)
Dim DT_DETAIL_VALUES As DataTable = MYDB_ECM.GetDatatable(DT_DETAILS_SQL)
Dim oDocID As Integer
Dim oConfigID As Integer
Dim recordId As Integer
@@ -401,10 +423,19 @@ Public Class ClassDocGrid
Dim gridControl As GridControl = pDocGridView.GridControl
' Datasource auf Master-Tabelle setzen
'gridView.GridControl.DataSource = DT_RESULT
gridControl.DataSource = ds.Tables(0)
gridControl.ForceInitialize()
' ── Performance-optimiertes DataSource-Setzen ─────────────────────────
_isGridRefreshing = True ' Flag setzen VOR DataSource-Änderung
Try
pDocGridView.BeginDataUpdate() ' Events unterdrücken
' Datasource auf Master-Tabelle setzen
gridControl.DataSource = ds.Tables(0)
gridControl.ForceInitialize()
pDocGridView.EndDataUpdate() ' Events reaktivieren
Finally
_isGridRefreshing = False ' Flag zurücksetzen
End Try
' Detail View anlegen und der Relation `docIdDetails` zuweisen
Dim GVDoc_Values As New GridView(gridControl)
@@ -471,8 +502,15 @@ Public Class ClassDocGrid
End If
If GridDocResult_BestFitColumns Then
pDocGridView.OptionsView.BestFitMaxRowCount = -1
pDocGridView.BestFitColumns(True)
_isGridRefreshing = True ' Auch hier Events unterdrücken
Try
pDocGridView.BeginUpdate()
pDocGridView.OptionsView.BestFitMaxRowCount = -1
pDocGridView.BestFitColumns(True)
pDocGridView.EndUpdate()
Finally
_isGridRefreshing = False
End Try
End If
' Alle Spalten aus ReadOnly setzen, danach werden alle passenden auf nicht ReadOnly gesetzt
@@ -570,40 +608,82 @@ Public Class ClassDocGrid
Private Shared Sub gridView_CustomColumnDisplayText(sender As Object, e As CustomColumnDisplayTextEventArgs)
Try
Dim view As ColumnView = sender
' ── Performance-Check 1: Während Refresh nichts tun ──────────────────
If _isGridRefreshing Then Return
' ── Performance-Check 2: Ungültige Row-Handles ignorieren ────────────
If e.ListSourceRowIndex = DevExpress.XtraGrid.GridControl.InvalidRowHandle Then
Return
End If
' ── Performance-Check 3: Leere Werte schnell verarbeiten ─────────────
If e.Value Is Nothing OrElse String.IsNullOrWhiteSpace(e.Value.ToString()) Then
e.DisplayText = ""
Return
End If
Dim fieldName As String = e.Column.FieldName
Dim parsedDate As DateTime
If Not IsNothing(DATE_COLUMNS) Then
If DATE_COLUMNS.Contains(e.Column.FieldName) And e.ListSourceRowIndex <> DevExpress.XtraGrid.GridControl.InvalidRowHandle Then
LOGGER.Debug($"gridView_CustomColumnDisplayText1 [{e.Column.FieldName}] ")
If e.Value.ToString() = String.Empty Then
e.DisplayText = ""
Exit Sub
End If
If Not DateTime.TryParse(e.Value, parsedDate) Then
parsedDate = DateTime.ParseExact(e.Value, CURRENT_DATE_FORMAT & " HH:MM:ss", System.Globalization.DateTimeFormatInfo.InvariantInfo)
End If
e.DisplayText = parsedDate.ToString(CURRENT_DATE_FORMAT & " HH:MM:ss")
' ── Datumskonvertierung für Standard-Datumsspalten ───────────────────
If DATE_COLUMNS IsNot Nothing AndAlso DATE_COLUMNS.Contains(fieldName) Then
' Nur bei Verbose-Logging loggen
If EnableVerboseGridLogging Then
LOGGER.Debug($"gridView_CustomColumnDisplayText [Standard] [{fieldName}]")
End If
End If
If Not IsNothing(DATE_COLUMNS_CONFIG) Then
If DATE_COLUMNS_CONFIG.Contains(e.Column.FieldName) And e.ListSourceRowIndex <> DevExpress.XtraGrid.GridControl.InvalidRowHandle Then
If e.Value.ToString() = String.Empty Then
e.DisplayText = ""
Exit Sub
End If
LOGGER.Debug($"gridView_CustomColumnDisplayText2 [{e.Column.FieldName}] ")
If Not DateTime.TryParse(e.Value, parsedDate) Then
parsedDate = DateTime.ParseExact(e.Value, CURRENT_DATE_FORMAT, System.Globalization.DateTimeFormatInfo.InvariantInfo)
End If
e.DisplayText = parsedDate.ToString(CURRENT_DATE_FORMAT)
End If
Try
' Versuche direktes Parsen
If DateTime.TryParse(e.Value.ToString(), parsedDate) Then
e.DisplayText = parsedDate.ToString(CURRENT_DATE_FORMAT & " HH:mm:ss")
Else
' Fallback: ParseExact
parsedDate = DateTime.ParseExact(e.Value.ToString(),
CURRENT_DATE_FORMAT & " HH:mm:ss",
System.Globalization.DateTimeFormatInfo.InvariantInfo)
e.DisplayText = parsedDate.ToString(CURRENT_DATE_FORMAT & " HH:mm:ss")
End If
Catch ex As FormatException
' Bei Parsing-Fehler Original-Wert anzeigen
e.DisplayText = e.Value.ToString()
If EnableVerboseGridLogging Then
LOGGER.Debug($"Date parsing failed for [{fieldName}]: {e.Value}")
End If
End Try
Return ' Früher Exit - keine weitere Prüfung nötig
End If
' ── Datumskonvertierung für Config-Datumsspalten ──────────────────────
If DATE_COLUMNS_CONFIG IsNot Nothing AndAlso DATE_COLUMNS_CONFIG.Contains(fieldName) Then
' Nur bei Verbose-Logging loggen
If EnableVerboseGridLogging Then
LOGGER.Debug($"gridView_CustomColumnDisplayText [Config] [{fieldName}]")
End If
Try
' Versuche direktes Parsen
If DateTime.TryParse(e.Value.ToString(), parsedDate) Then
e.DisplayText = parsedDate.ToString(CURRENT_DATE_FORMAT)
Else
' Fallback: ParseExact
parsedDate = DateTime.ParseExact(e.Value.ToString(),
CURRENT_DATE_FORMAT,
System.Globalization.DateTimeFormatInfo.InvariantInfo)
e.DisplayText = parsedDate.ToString(CURRENT_DATE_FORMAT)
End If
Catch ex As FormatException
' Bei Parsing-Fehler Original-Wert anzeigen
e.DisplayText = e.Value.ToString()
If EnableVerboseGridLogging Then
LOGGER.Debug($"Date parsing failed for [{fieldName}]: {e.Value}")
End If
End Try
End If
Catch ex As Exception
LOGGER.Warn("Unexpected error in gridView_CustomColumnDisplayText: " & ex.Message)
' Fehler IMMER loggen (aber nicht Debug)
LOGGER.Error($"gridView_CustomColumnDisplayText Error [{e.Column?.FieldName}]: {ex.Message}")
End Try
End Sub