Files
TaskFlow/app/TaskFlow/ControlCreator/GridControl.vb
2026-03-25 15:20:39 +01:00

1454 lines
80 KiB
VB.net
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
Imports DevExpress.Utils
Imports DevExpress.XtraEditors
Imports DevExpress.XtraEditors.Repository
Imports DevExpress.XtraGrid.Columns
Imports DevExpress.XtraGrid.Views.Grid
Imports DigitalData.Controls.LookupGrid
Imports DigitalData.Modules.EDMI.API.Constants
Imports DigitalData.Modules.EDMI.API.DatabaseWithFallback
Imports DigitalData.Modules.Logging
Imports DigitalData.Modules.Base
Imports System.ComponentModel
Imports DevExpress.XtraEditors.Controls
Imports DevExpress.XtraGrid.Views.Base
Imports System.Text.RegularExpressions
Imports System.Globalization
Imports DevExpress.Xpo.Helpers.AssociatedCollectionCriteriaHelper
Imports DevExpress.XtraEditors.Mask
Imports System.Windows.Forms
Namespace ControlCreator
Public Class GridControl
Private ReadOnly _LogConfig As LogConfig
Private ReadOnly _Logger As Logger
Private ReadOnly _GridTables As Dictionary(Of Integer, Dictionary(Of String, RepositoryItem))
Private newRowModified As Boolean
Private isApplyingInheritedValue As Boolean
Private _FormulaColumnNames As New HashSet(Of String)(StringComparer.OrdinalIgnoreCase)
Private _FormulaSqlColumns As New Dictionary(Of String, FormulaSqlDefinition)(StringComparer.OrdinalIgnoreCase)
Private _isRefreshingFormula As Boolean = False ' *** NEU: Flag für Formel-Refresh ***
Private _currencySymbol As String = ""
''' <summary>
''' SHARED Dictionary: Speichert das aktuelle Währungssymbol PRO GridView (via Name).
''' Dies ist notwendig, weil UpdateCurrencyFormat auf einer NEUEN GridControl-Instanz
''' aufgerufen wird, der CustomColumnDisplayText-Handler aber auf der URSPRÜNGLICHEN
''' Instanz registriert wurde. Durch das Shared Dictionary können alle Instanzen
''' auf das aktuelle Symbol zugreifen.
''' </summary>
Private Shared _CurrencySymbolByGridName As New Dictionary(Of String, String)(StringComparer.OrdinalIgnoreCase)
''' <summary>
''' Definiert eine SQL-basierte Formelspalte mit allen nötigen Metadaten.
''' </summary>
Private Class FormulaSqlDefinition
Public Property SqlTemplate As String
Public Property ReferencedColumns As List(Of String)
End Class
''' <summary>
''' Extrahiert alle {#TBCOL#ColumnName}-Platzhalter aus einem SQL-Template.
''' </summary>
Private Function GetReferencedSqlColumnNames(sqlTemplate As String) As List(Of String)
Dim columnNames As New List(Of String)
Dim pattern As String = "\{#TBCOL#([^}]+)\}"
Dim matches = Regex.Matches(sqlTemplate, pattern)
For Each match As Match In matches
Dim colName = match.Groups(1).Value
If Not columnNames.Contains(colName, StringComparer.OrdinalIgnoreCase) Then
columnNames.Add(colName)
End If
Next
Return columnNames
End Function
''' <summary>
''' Ersetzt alle {#TBCOL#ColumnName}-Platzhalter durch die aktuellen Zeilenwerte.
''' Gibt den ausführbaren SQL-String zurück.
''' </summary>
Private Function ResolveSqlTemplate(sqlTemplate As String, pView As GridView, rowHandle As Integer) As String
Dim resolvedSql As String = sqlTemplate
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
resolvedSql = resolvedSql.Replace(match.Value, safeValue)
_Logger.Debug("Resolved SQL placeholder [{0}] with value [{1}] → {2}", match.Value, cellValue, safeValue)
Next
_Logger.Debug("Final resolved SQL: {0}", resolvedSql)
Return resolvedSql
End Function
Public Sub New(pLogConfig As LogConfig, pGridTables As Dictionary(Of Integer, Dictionary(Of String, RepositoryItem)), pCurrencySymbol As String)
_LogConfig = pLogConfig
_Logger = pLogConfig.GetLogger()
_GridTables = pGridTables
_currencySymbol = pCurrencySymbol
End Sub
''' <summary>
''' Setzt den Shared Currency-Cache zurück. Muss beim Laden eines neuen Dokuments
''' aufgerufen werden, bevor UpdateCurrencyFormat die neuen Werte schreibt.
''' Verhindert, dass ein veraltetes Währungssymbol aus einem vorherigen Dokument
''' durch den CustomColumnDisplayText-Handler verwendet wird.
''' </summary>
Public Shared Sub ResetCurrencySymbolCache()
SyncLock _CurrencySymbolByGridName
_CurrencySymbolByGridName.Clear()
End SyncLock
End Sub
Public Function CreateGridColumns(pColumnTable As DataTable) As DataTable
Dim oDataTable As New DataTable
Dim columnsWithExpressions As New List(Of Tuple(Of DataColumn, String))
For Each oRow As DataRow In pColumnTable.Rows
' Create Columns in Datatable
Dim oColumn = New DataColumn() With {
.ColumnName = oRow.Item("SPALTENNAME"),
.Caption = oRow.Item("SPALTEN_HEADER_LANG"),
.ReadOnly = False
}
Select Case oRow.Item("TYPE_COLUMN")
Case Constants.CONTROL_TYPE_TEXT
oColumn.DataType = GetType(String)
Case Constants.CONTROL_TYPE_INTEGER
oColumn.DataType = GetType(Integer)
Case Constants.CONTROL_TYPE_DOUBLE
oColumn.DataType = GetType(Double)
Case Constants.CONTROL_TYPE_CURRENCY
oColumn.DataType = GetType(Double)
Case Constants.CONTROL_TYPE_BOOLEAN
oColumn.DataType = GetType(Boolean)
Case Else
oColumn.DataType = GetType(String)
End Select
Dim oFormulaExpression = ObjectEx.NotNull(oRow.Item("FORMULA_EXPRESSION"), String.Empty)
Dim oFormulaSql = ObjectEx.NotNull(oRow.Item("FORMULA_SQL"), String.Empty)
' *** VALIDIERUNG: Beides gleichzeitig ist nicht erlaubt ***
If oFormulaExpression <> String.Empty AndAlso oFormulaSql <> String.Empty Then
_Logger.Warn("⚠️ [CreateGridColumns] Column [{0}] has BOTH FORMULA_EXPRESSION and FORMULA_SQL FORMULA_SQL will be ignored.", oColumn.ColumnName)
' FORMULA_EXPRESSION hat Vorrang → nur Expression wird als DataTable.Expression gesetzt
End If
If oFormulaExpression <> String.Empty Then
' FORMULA_EXPRESSION: Expression merken, aber erst später setzen (DataTable.Expression)
columnsWithExpressions.Add(New Tuple(Of DataColumn, String)(oColumn, oFormulaExpression))
' HINWEIS: FORMULA_SQL-Spalten bekommen KEINE DataTable.Expression,
' da deren Werte programmatisch via SetRowCellValue gesetzt werden.
End If
Try
oDataTable.Columns.Add(oColumn)
Catch ex As Exception
_Logger.Warn("⚠️ Could not add column {0} to DataTable", oColumn.ColumnName)
_Logger.Error(ex)
End Try
Next
' Jetzt alle Expressions setzen, nachdem alle Spalten existieren
For Each columnExpressionPair In columnsWithExpressions
Dim oColumn = columnExpressionPair.Item1
Dim oExpression = columnExpressionPair.Item2
Try
_Logger.Debug("Setting expression for column [{0}]: {1}", oColumn.ColumnName, oExpression)
' Prüfe, ob alle referenzierten Spalten existieren
Dim referencedColumns = GetReferencedColumnNames(oExpression)
For Each refCol In referencedColumns
If Not oDataTable.Columns.Contains(refCol) Then
_Logger.Warn("⚠️ Referenced column [{0}] does not exist in DataTable!", refCol)
MsgBox(String.Format("Referenced column [{0}] does not exist in DataTable!", refCol), MsgBoxStyle.Exclamation)
Else
_Logger.Debug("Referenced column [{0}] exists with DataType: {1}", refCol, oDataTable.Columns(refCol).DataType.Name)
End If
Next
oColumn.Expression = oExpression
oColumn.ReadOnly = True
_Logger.Info("✓ Expression successfully set for column [{0}]: {1}", oColumn.ColumnName, oColumn.Expression)
Catch ex As Exception
_Logger.Warn("⚠️ Invalid FORMULA_EXPRESSION for column {0}: {1}", oColumn.ColumnName, oExpression)
_Logger.Error(ex)
MsgBox(String.Format("The column '{0}' inlcudes an invalid formula: {1}. Please check the FORMULA_EXPRESSION in the table designer." & vbCrLf &
"Error: {2}", oColumn.ColumnName, oExpression, ex.Message), MsgBoxStyle.Exclamation, "Ungültige Formel")
End Try
Next
Return oDataTable
End Function
Private Function GetReferencedColumnNames(expression As String) As List(Of String)
Dim columnNames As New List(Of String)
Dim pattern As String = "\[([^\]]+)\]"
Dim matches = Regex.Matches(expression, pattern)
For Each match As Match In matches
columnNames.Add(match.Groups(1).Value)
Next
Return columnNames
End Function
Public Function FillGridTables(pColumnTable As DataTable, pControlId As Integer, pControlName As String) As Dictionary(Of Integer, Dictionary(Of String, RepositoryItem))
For Each oRow As DataRow In pColumnTable.Rows
' Fetch and cache Combobox results
Dim oConnectionId As Integer = oRow.ItemEx("CONNECTION_ID", 0)
Dim oSqlCommand As String = oRow.ItemEx("SQL_COMMAND", "")
If oSqlCommand <> "" Then
Try
Dim oComboboxDataTable As DataTable = Nothing
Dim oColumnName As String = oRow.Item("SPALTENNAME")
_Logger.Debug("Working on SQL for Column[{0}]...", oColumnName)
If Not clsPatterns.HasComplexPatterns(oSqlCommand) Then
_Logger.Debug("SQL has no complex patterns!")
'oComboboxDataTable = ClassDatabase.Return_Datatable_ConId(oSqlCommand, oConnectionId)
oComboboxDataTable = DatabaseFallback.GetDatatable(New GetDatatableOptions(oSqlCommand, DatabaseType.ECM) With {
.ConnectionId = oConnectionId
})
Else
_Logger.Debug("...has complex patterns!!")
End If
Dim oRepositoryItem = GridTables_GetRepositoryItemForColumn(oColumnName, oComboboxDataTable, oRow.Item("ADVANCED_LOOKUP"))
If _GridTables.Item(pControlId).ContainsKey(oColumnName) Then
_GridTables.Item(pControlId).Item(oColumnName) = oRepositoryItem
Else
_GridTables.Item(pControlId).Add(oColumnName, oRepositoryItem)
End If
Catch ex As Exception
_Logger.Warn("⚠️ Could not load data for column {0} in control {1}", oRow.Item("SPALTENNAME"), pControlName)
_Logger.Error(ex)
End Try
End If
Next
Return _GridTables
End Function
Private Function GridTables_GetRepositoryItemForColumn(pColumnName As String, pDataTable As DataTable, pIsAdvancedLookup As Boolean) As RepositoryItem
If pIsAdvancedLookup Then
Dim oEditor = New RepositoryItemLookupControl3
If pDataTable IsNot Nothing Then
oEditor.DisplayMember = pDataTable.Columns.Item(0).ColumnName
oEditor.ValueMember = pDataTable.Columns.Item(0).ColumnName
oEditor.DataSource = pDataTable
End If
Return oEditor
Else
Dim oEditor = New RepositoryItemComboBox()
Dim oItems As New List(Of String)
AddHandler oEditor.Validating, Sub(_sender As ComboBoxEdit, _e As CancelEventArgs)
If oItems.Contains(_sender.EditValue) Then
_e.Cancel = False
Else
_e.Cancel = True
End If
End Sub
If pDataTable IsNot Nothing Then
For Each oRow2 As DataRow In pDataTable.Rows
Dim oValue = oRow2.Item(0)
Try
If oRow2.ItemArray.Length > 1 Then
oValue &= $" | {oRow2.Item(1)}"
End If
Catch ex As Exception
End Try
oEditor.Items.Add(oValue)
oItems.Add(oValue)
Next
End If
Return oEditor
End If
End Function
''' <summary>
''' Setzt das Währungssymbol für ein bestimmtes Grid im Shared Cache.
''' Muss bei JEDEM Dokumentwechsel aufgerufen werden (auch bei EUR),
''' damit der CustomColumnDisplayText-Handler sofort den korrekten Wert hat.
''' </summary>
Public Shared Sub SetCurrencySymbolForGrid(gridName As String, currencySymbol As String)
SyncLock _CurrencySymbolByGridName
_CurrencySymbolByGridName(gridName) = currencySymbol
End SyncLock
End Sub
' Hilfsroutine: passt NUR das Summary-Item an (ohne FormatInfo)
Private Sub ApplyCurrencySummaryFormat(oCol As GridColumn)
oCol.SummaryItem.SummaryType = DevExpress.Data.SummaryItemType.Sum
_Logger.Debug("Applying currency summary format for column [{0}] with symbol [{1}]", oCol.FieldName, _currencySymbol)
' Variante B: Kulturunabhängig, Symbol explizit anhängen
oCol.SummaryItem.DisplayFormat = $"{{0:N2}} {_currencySymbol}"
End Sub
''' <summary>
''' Aktualisiert die Währungsformatierung für alle CURRENCY-Spalten mit neuem Währungssymbol.
''' Betrifft DisplayFormat, ColumnEdit (für editierbare Spalten) und Summary-Footer.
''' </summary>
''' <summary>
'''
Public Sub UpdateCurrencyFormat(pColumnTable As DataTable, pGridView As GridView, pGrid As DevExpress.XtraGrid.GridControl, pNewCurrencySymbol As String)
Try
_Logger.Info("[UpdateCurrencyFormat] *** START *** [{0}] → [{1}] für [{2}]", _currencySymbol, pNewCurrencySymbol, pGrid.Name)
' *** KERN-FIX: Speichere Symbol im SHARED Dictionary ***
Dim gridName As String = pGrid.Name
SyncLock _CurrencySymbolByGridName
If _CurrencySymbolByGridName.ContainsKey(gridName) Then
_CurrencySymbolByGridName(gridName) = pNewCurrencySymbol
Else
_CurrencySymbolByGridName.Add(gridName, pNewCurrencySymbol)
End If
End SyncLock
_Logger.Debug("[UpdateCurrencyFormat] Shared Dictionary updated: [{0}] = [{1}]", gridName, pNewCurrencySymbol)
_currencySymbol = pNewCurrencySymbol
Dim oCultureInfo As CultureInfo = New CultureInfo("de-DE")
oCultureInfo.NumberFormat.CurrencySymbol = _currencySymbol
Dim riTextEdit As New RepositoryItemTextEdit()
riTextEdit.MaskSettings.Configure(Of MaskSettings.Numeric)(
Sub(settings)
settings.MaskExpression = "c"
settings.Culture = oCultureInfo
End Sub)
riTextEdit.UseMaskAsDisplayFormat = False
riTextEdit.DisplayFormat.FormatType = FormatType.Custom
riTextEdit.DisplayFormat.FormatString = $"#,##0.00 {_currencySymbol}"
_Logger.Debug("[UpdateCurrencyFormat] riTextEdit: DisplayFormat=[{0}]",
riTextEdit.DisplayFormat.FormatString)
pGridView.BeginUpdate()
Try
' Schritt 1: Altes RepositoryItem entfernen (ohne vorher ColumnEdit=Nothing)
Dim oldItems = pGrid.RepositoryItems.OfType(Of RepositoryItemTextEdit)().
Where(Function(item) item.MaskSettings.MaskExpression = "c").ToList()
For Each oldItem In oldItems
_Logger.Debug("[UpdateCurrencyFormat] Removing old riTextEdit: DisplayFormat=[{0}]",
oldItem.DisplayFormat.FormatString)
pGrid.RepositoryItems.Remove(oldItem)
Next
' Schritt 2: CURRENCY-Spalten konfigurieren
For Each oCol As GridColumn In pGridView.Columns
Dim oColumnData As DataRow = pColumnTable.
Select($"SPALTENNAME = '{oCol.FieldName}'").
FirstOrDefault()
If oColumnData Is Nothing Then Continue For
If ObjectEx.NotNull(oColumnData.Item("TYPE_COLUMN"), String.Empty).ToString() <> "CURRENCY" Then Continue For
Dim oIsFormulaExpression As Boolean =
ObjectEx.NotNull(oColumnData.Item("FORMULA_EXPRESSION"), String.Empty) <> String.Empty
Dim oIsFormulaSql As Boolean =
ObjectEx.NotNull(oColumnData.Item("FORMULA_SQL"), String.Empty) <> String.Empty
' Entweder/Oder: Beide gleichzeitig → Expression gewinnt, SQL ignoriert
If oIsFormulaExpression AndAlso oIsFormulaSql Then
_Logger.Warn("[UpdateCurrencyFormat] Column [{0}] has BOTH FORMULA_EXPRESSION and FORMULA_SQL treating as EXPRESSION only.", oCol.FieldName)
oIsFormulaSql = False
End If
' Spalte ist eine Formel-Spalte (Expression ODER SQL) → ReadOnly, kein ColumnEdit
Dim oIsAnyFormula As Boolean = oIsFormulaExpression OrElse oIsFormulaSql
' DisplayFormat immer aktualisieren
oCol.DisplayFormat.FormatType = FormatType.Custom
oCol.DisplayFormat.FormatString = $"#,##0.00 {_currencySymbol}"
If Not oIsAnyFormula AndAlso oCol.OptionsColumn.AllowEdit Then
' Nur editierbare Nicht-Formel-Spalten bekommen ein ColumnEdit
oCol.ColumnEdit = riTextEdit
_Logger.Debug("[UpdateCurrencyFormat] ColumnEdit=[{0}] für [{1}]",
DirectCast(oCol.ColumnEdit, RepositoryItemTextEdit).DisplayFormat.FormatString,
oCol.FieldName)
Else
_Logger.Debug("[UpdateCurrencyFormat] [{0}]: ReadOnly/Formula nur DisplayFormat aktualisiert (IsExpression=[{1}], IsSql=[{2}])",
oCol.FieldName, oIsFormulaExpression, oIsFormulaSql)
End If
If ObjectEx.NotNull(oColumnData.Item("SUMMARY_FUNCTION"), String.Empty) =
Constants.AGGREGATE_TOTAL_CURRENCY Then
oCol.SummaryItem.SummaryType = DevExpress.Data.SummaryItemType.Sum
oCol.SummaryItem.DisplayFormat = $"{{0:N2}} {_currencySymbol}"
End If
Next
Finally
pGridView.EndUpdate()
End Try
pGridView.LayoutChanged()
For i As Integer = 0 To pGridView.DataRowCount - 1
pGridView.InvalidateRow(i)
Next
pGridView.UpdateTotalSummary()
_Logger.Info("[UpdateCurrencyFormat] *** END *** _currencySymbol=[{0}]", _currencySymbol)
Catch ex As Exception
_Logger.Error("[UpdateCurrencyFormat] Fehler: {0}", ex.Message)
_Logger.Error(ex)
End Try
End Sub
Public Sub ConfigureViewColumns(pColumnTable As DataTable, pGridView As GridView, pGrid As DevExpress.XtraGrid.GridControl)
Dim oShouldDisplayFooter As Boolean = False
For Each oCol As GridColumn In pGridView.Columns
Dim oColumnData As DataRow = pColumnTable.
Select($"SPALTENNAME = '{oCol.FieldName}'").
FirstOrDefault()
If oColumnData Is Nothing Then
Continue For
End If
Dim oSequence As Integer = oColumnData.Item("SEQUENCE")
oCol.VisibleIndex = oSequence
' READ_ONLY Eigenschaft verarbeiten
Dim oIsReadOnly As Boolean = False
Dim oReadOnlyValue = ObjectEx.NotNull(oColumnData.Item("READ_ONLY"), False)
If TypeOf oReadOnlyValue Is Boolean Then
oIsReadOnly = CBool(oReadOnlyValue)
Else
Dim oReadOnlyInt As Integer
oIsReadOnly = Integer.TryParse(oReadOnlyValue.ToString(), oReadOnlyInt) AndAlso oReadOnlyInt = 1
End If
Dim oFormulaExpression = ObjectEx.NotNull(oColumnData.Item("FORMULA_EXPRESSION"), String.Empty)
If oFormulaExpression <> String.Empty Then
oIsReadOnly = True
End If
Dim oSQLExpression = ObjectEx.NotNull(oColumnData.Item("FORMULA_SQL"), String.Empty)
If oSQLExpression <> String.Empty Then
oIsReadOnly = True
End If
oCol.OptionsColumn.AllowEdit = Not oIsReadOnly
Dim oColumnType As String = oColumnData.Item("TYPE_COLUMN")
Select Case oColumnType
Case "INTEGER"
oCol.DisplayFormat.FormatType = FormatType.Custom
oCol.DisplayFormat.FormatString = "N0"
Case "DOUBLE"
oCol.DisplayFormat.FormatType = FormatType.Custom
oCol.DisplayFormat.FormatString = "N2"
Case "CURRENCY"
' *** DisplayFormat wird NICHT hier gesetzt ***
' ConfigureViewColumnsCurrency übernimmt die CURRENCY-Formatierung
' mit dem korrekten _currencySymbol. Dieses Standardformat würde
' später von UpdateCurrencyFormat überschrieben werden müssen
' für Formel-Spalten (kein ColumnEdit) greift aber nur DisplayFormat,
' weshalb ein falscher Initialwert hier persistent bleibt.
_Logger.Debug("CURRENCY column [{0}]: DisplayFormat wird von ConfigureViewColumnsCurrency gesetzt", oCol.FieldName)
End Select
Dim oSummaryFunction As String = oColumnData.Item("SUMMARY_FUNCTION")
Select Case oSummaryFunction
Case Constants.AGGREGATE_TOTAL_INTEGER
oCol.SummaryItem.SummaryType = DevExpress.Data.SummaryItemType.Sum
oCol.SummaryItem.DisplayFormat = "{0:N0}"
oShouldDisplayFooter = True
Case Constants.AGGREGATE_TOTAL_FLOAT
oCol.SummaryItem.SummaryType = DevExpress.Data.SummaryItemType.Sum
oCol.SummaryItem.DisplayFormat = "{0:N2}"
oShouldDisplayFooter = True
Case Constants.AGGREGATE_TOTAL_CURRENCY
_Logger.Debug(Of String)("Applying currency summary format for column [{0}]", oCol.FieldName)
ApplyCurrencySummaryFormat(oCol)
oShouldDisplayFooter = True
Case Constants.AGGREGATE_TOTAL_AVG
oCol.SummaryItem.SummaryType = DevExpress.Data.SummaryItemType.Average
oCol.SummaryItem.DisplayFormat = "AVG: {0}"
oShouldDisplayFooter = True
Case Constants.AGGREGATE_TOTAL_MAX
oCol.SummaryItem.SummaryType = DevExpress.Data.SummaryItemType.Max
oCol.SummaryItem.DisplayFormat = "MAX: {0}"
oShouldDisplayFooter = True
Case Constants.AGGREGATE_TOTAL_MIN
oCol.SummaryItem.SummaryType = DevExpress.Data.SummaryItemType.Min
oCol.SummaryItem.DisplayFormat = "MIN: {0}"
oShouldDisplayFooter = True
Case Constants.AGGREGATE_TOTAL_COUNT
oCol.SummaryItem.SummaryType = DevExpress.Data.SummaryItemType.Count
oCol.SummaryItem.DisplayFormat = "NUM: {0}"
oShouldDisplayFooter = True
End Select
Next
pGridView.OptionsView.ShowFooter = oShouldDisplayFooter
If oShouldDisplayFooter Then
With pGridView.Appearance.FooterPanel
.Options.UseFont = True
.Font = New Font(.Font.FontFamily, 8.0F, FontStyle.Bold)
End With
End If
End Sub
Public Sub ConfigureViewColumnsCurrency(pColumnTable As DataTable, pGridView As GridView, pGrid As DevExpress.XtraGrid.GridControl)
Dim oCultureInfo As CultureInfo = New CultureInfo("de-DE")
oCultureInfo.NumberFormat.CurrencySymbol = _currencySymbol
Dim riTextEdit As RepositoryItemTextEdit = New RepositoryItemTextEdit()
riTextEdit.MaskSettings.Configure(Of MaskSettings.Numeric)(
Sub(settings)
settings.MaskExpression = "c"
settings.Culture = oCultureInfo
End Sub)
riTextEdit.UseMaskAsDisplayFormat = False
riTextEdit.DisplayFormat.FormatType = FormatType.Custom
riTextEdit.DisplayFormat.FormatString = $"#,##0.00 {_currencySymbol}"
_Logger.Debug("[ConfigureViewColumnsCurrency] riTextEdit erstellt: DisplayFormat=[{0}], HashCode=[{1}]",
riTextEdit.DisplayFormat.FormatString, riTextEdit.GetHashCode())
_Logger.Debug("[ConfigureViewColumnsCurrency] pGrid.RepositoryItems.Count VOR Schleife=[{0}]",
pGrid.RepositoryItems.Count)
For Each oCol As GridColumn In pGridView.Columns
Dim oColumnData As DataRow = pColumnTable.
Select($"SPALTENNAME = '{oCol.FieldName}'").
FirstOrDefault()
If oColumnData Is Nothing Then Continue For
Dim oColumnType As String = ObjectEx.NotNull(oColumnData.Item("TYPE_COLUMN"), String.Empty).ToString()
If oColumnType <> "CURRENCY" Then Continue For
Dim oFormulaExpression = ObjectEx.NotNull(oColumnData.Item("FORMULA_EXPRESSION"), String.Empty)
Dim oFormulaSql = ObjectEx.NotNull(oColumnData.Item("FORMULA_SQL"), String.Empty)
' Entweder/Oder: Beide gleichzeitig → Expression gewinnt, SQL ignoriert
If oFormulaExpression <> String.Empty AndAlso oFormulaSql <> String.Empty Then
_Logger.Warn("[ConfigureViewColumnsCurrency] Column [{0}] has BOTH FORMULA_EXPRESSION and FORMULA_SQL treating as EXPRESSION only.", oCol.FieldName)
oFormulaSql = String.Empty
End If
' Spalte ist eine Formel-Spalte (Expression ODER SQL) → nur DisplayFormat, kein ColumnEdit
Dim oIsAnyFormula As Boolean = oFormulaExpression <> String.Empty OrElse oFormulaSql <> String.Empty
If oIsAnyFormula Then
' Formel-Spalten (Expression oder SQL): nur DisplayFormat setzen
oCol.DisplayFormat.FormatType = FormatType.Custom
oCol.DisplayFormat.FormatString = $"#,##0.00 {_currencySymbol}"
_Logger.Debug("[ConfigureViewColumnsCurrency] Formel-Spalte [{0}] (IsExpression=[{1}], IsSql=[{2}]): DisplayFormat=[{3}], RepositoryItems.Count=[{4}]",
oCol.FieldName,
oFormulaExpression <> String.Empty,
oFormulaSql <> String.Empty,
oCol.DisplayFormat.FormatString,
pGrid.RepositoryItems.Count)
ElseIf oCol.OptionsColumn.AllowEdit Then
_Logger.Debug("[ConfigureViewColumnsCurrency] [{0}] VOR ColumnEdit: RepositoryItems.Count=[{1}]",
oCol.FieldName, pGrid.RepositoryItems.Count)
oCol.ColumnEdit = riTextEdit
_Logger.Debug("[ConfigureViewColumnsCurrency] [{0}] NACH ColumnEdit: RepositoryItems.Count=[{1}]",
oCol.FieldName, pGrid.RepositoryItems.Count)
Dim assignedEdit = TryCast(oCol.ColumnEdit, RepositoryItemTextEdit)
_Logger.Debug("[ConfigureViewColumnsCurrency] [{0}]: IsSameObject=[{1}], ColumnEdit.DisplayFormat=[{2}], ColumnEdit.HashCode=[{3}]",
oCol.FieldName,
Object.ReferenceEquals(assignedEdit, riTextEdit),
If(assignedEdit IsNot Nothing, assignedEdit.DisplayFormat.FormatString, "N/A"),
If(assignedEdit IsNot Nothing, assignedEdit.GetHashCode(), -1))
For i As Integer = 0 To pGrid.RepositoryItems.Count - 1
_Logger.Debug("[ConfigureViewColumnsCurrency] RepositoryItems[{0}]: Type=[{1}], HashCode=[{2}]",
i, pGrid.RepositoryItems(i).GetType().Name, pGrid.RepositoryItems(i).GetHashCode())
Next
End If
Next
Dim oTestFired As Boolean = False
AddHandler pGridView.CustomColumnDisplayText,
Sub(sender As Object, e As CustomColumnDisplayTextEventArgs)
If e.Column Is Nothing OrElse e.Value Is Nothing OrElse IsDBNull(e.Value) Then
Return
End If
Dim oColumnData As DataRow = pColumnTable.
Select($"SPALTENNAME = '{e.Column.FieldName}'").
FirstOrDefault()
If oColumnData IsNot Nothing AndAlso
oColumnData.Item("TYPE_COLUMN").ToString() = "CURRENCY" Then
Try
Dim currentSymbol As String = _currencySymbol
Dim gridName As String = pGrid.Name
SyncLock _CurrencySymbolByGridName
If _CurrencySymbolByGridName.ContainsKey(gridName) Then
currentSymbol = _CurrencySymbolByGridName(gridName)
End If
End SyncLock
Dim oValue As Double
If TypeOf e.Value Is Double OrElse TypeOf e.Value Is Decimal Then
oValue = Convert.ToDouble(e.Value)
ElseIf TypeOf e.Value Is String Then
Dim oStringValue As String = e.Value.ToString().Trim()
Dim oDeCulture As CultureInfo = New CultureInfo("de-DE")
If Double.TryParse(oStringValue, NumberStyles.Currency Or NumberStyles.Number, oDeCulture, oValue) Then
ElseIf Double.TryParse(oStringValue, NumberStyles.Currency Or NumberStyles.Number, CultureInfo.InvariantCulture, oValue) Then
Else
oValue = Convert.ToDouble(oStringValue, CultureInfo.CurrentCulture)
End If
Else
oValue = Convert.ToDouble(e.Value)
End If
Dim oDeCultureInfo As CultureInfo = New CultureInfo("de-DE")
e.DisplayText = oValue.ToString("N2", oDeCultureInfo) & " " & currentSymbol
_Logger.Debug("[CustomColumnDisplayText] CURRENCY [{0}]: DisplayText=[{1}], Symbol=[{2}] (from Shared Dict in ConfigureViewColumnsCurrency)",
e.Column.FieldName, e.DisplayText, currentSymbol)
Catch ex As Exception
_Logger.Warn("⚠️ Could not format currency value [{0}] for column [{1}]: {2}",
e.Value, e.Column.FieldName, ex.Message)
Dim fallbackSymbol As String = _currencySymbol
SyncLock _CurrencySymbolByGridName
If _CurrencySymbolByGridName.ContainsKey(pGrid.Name) Then
fallbackSymbol = _CurrencySymbolByGridName(pGrid.Name)
End If
End SyncLock
e.DisplayText = e.Value.ToString() & " " & fallbackSymbol
End Try
End If
End Sub
End Sub
Public Sub ConfigureViewEvents(pColumnTable As DataTable, pGridView As GridView, pControl As Windows.Forms.Control, pControlId As Integer)
' *** Formel-Spalten einmalig cachen + Validierung: Entweder EXPRESSION oder SQL, nie beides ***
_FormulaColumnNames.Clear()
_FormulaSqlColumns.Clear()
For Each r As DataRow In pColumnTable.Rows
Dim oColName = r.Item("SPALTENNAME").ToString()
Dim oExpr = ObjectEx.NotNull(r.Item("FORMULA_EXPRESSION"), String.Empty).ToString()
Dim oSql = ObjectEx.NotNull(r.Item("FORMULA_SQL"), String.Empty).ToString()
' *** VALIDIERUNG: Beides gleichzeitig ist nicht erlaubt ***
If oExpr <> String.Empty AndAlso oSql <> String.Empty Then
_Logger.Warn("⚠️ Column [{0}] has BOTH FORMULA_EXPRESSION and FORMULA_SQL this is not allowed! FORMULA_SQL will be ignored.", oColName)
MsgBox(String.Format(
"Die Spalte '{0}' enthält sowohl eine FORMULA_EXPRESSION als auch eine FORMULA_SQL." & vbCrLf &
"Es darf nur eine der beiden Formeln gesetzt sein." & vbCrLf &
"FORMULA_SQL wird ignoriert. Bitte korrigieren Sie die Konfiguration im Tabellen-Designer.",
oColName), MsgBoxStyle.Exclamation, "Ungültige Spalten-Konfiguration")
' FORMULA_EXPRESSION hat Vorrang → SQL ignorieren
oSql = String.Empty
End If
If oExpr <> String.Empty Then
_FormulaColumnNames.Add(oColName)
_Logger.Debug("[ConfigureViewEvents] Column [{0}] registered as FORMULA_EXPRESSION column.", oColName)
ElseIf oSql <> String.Empty Then
Dim oConnectionId As Integer = r.ItemEx("CONNECTION_ID", 0)
_FormulaSqlColumns(oColName) = New FormulaSqlDefinition() With {
.SqlTemplate = oSql,
.ReferencedColumns = GetReferencedSqlColumnNames(oSql)
}
' SQL-Spalten auch in _FormulaColumnNames aufnehmen → Editor-Blockade + ReadOnly
_FormulaColumnNames.Add(oColName)
_Logger.Debug("[ConfigureViewEvents] Column [{0}] registered as FORMULA_SQL column. ReferencedColumns=[{1}]",
oColName, String.Join(", ", GetReferencedSqlColumnNames(oSql)))
End If
Next
AddHandler pGridView.InitNewRow, Sub(sender As Object, e As InitNewRowEventArgs)
Try
_Logger.Debug("Initialzing new row")
For Each oColumnData As DataRow In pColumnTable.Rows
For Each oGridColumn As GridColumn In pGridView.Columns
If oGridColumn.FieldName <> oColumnData.Item("SPALTENNAME") Then
Continue For
End If
Dim oDefaultValue = ObjectEx.NotNull(oColumnData.Item("DEFAULT_VALUE"), String.Empty)
If oDefaultValue <> String.Empty Then
_Logger.Debug("Setting default value [{0}] for column [{1}]", oDefaultValue, oGridColumn.FieldName)
pGridView.SetRowCellValue(e.RowHandle, oGridColumn.FieldName, oDefaultValue)
End If
Next
Next
Catch ex As Exception
_Logger.Error(ex)
Finally
newRowModified = False
End Try
End Sub
' *** NEU: CustomColumnDisplayText für robuste CURRENCY-Formatierung ***
AddHandler pGridView.CustomColumnDisplayText,
Sub(sender As Object, e As CustomColumnDisplayTextEventArgs)
If e.Column Is Nothing OrElse e.Value Is Nothing OrElse IsDBNull(e.Value) Then
Return
End If
' Prüfe ob Spalte vom Typ CURRENCY ist
Dim oColumnData As DataRow = pColumnTable.
Select($"SPALTENNAME = '{e.Column.FieldName}'").
FirstOrDefault()
If oColumnData IsNot Nothing AndAlso
oColumnData.Item("TYPE_COLUMN").ToString() = "CURRENCY" Then
Try
' *** KERN-FIX: Hole Symbol aus SHARED Dictionary statt Instanz-Feld ***
Dim currentSymbol As String = _currencySymbol ' Fallback
Dim gridName As String = pControl.Name
SyncLock _CurrencySymbolByGridName
If _CurrencySymbolByGridName.ContainsKey(gridName) Then
currentSymbol = _CurrencySymbolByGridName(gridName)
End If
End SyncLock
Dim oValue As Double
' *** KRITISCH: Robustes Parsing unabhängig vom Dezimaltrenner ***
If TypeOf e.Value Is Double OrElse TypeOf e.Value Is Decimal Then
oValue = Convert.ToDouble(e.Value)
ElseIf TypeOf e.Value Is String Then
Dim oStringValue As String = e.Value.ToString().Trim()
' Versuche zuerst deutsches Format (1.234,56)
Dim oDeCulture As CultureInfo = New CultureInfo("de-DE")
If Double.TryParse(oStringValue, NumberStyles.Currency Or NumberStyles.Number, oDeCulture, oValue) Then
' Erfolgreich mit deutschem Format geparst
ElseIf Double.TryParse(oStringValue, NumberStyles.Currency Or NumberStyles.Number, CultureInfo.InvariantCulture, oValue) Then
' Erfolgreich mit invariantem Format (Punkt als Dezimaltrenner)
Else
' Fallback: Systemkultur
oValue = Convert.ToDouble(oStringValue, CultureInfo.CurrentCulture)
End If
Else
oValue = Convert.ToDouble(e.Value)
End If
' Formatierung IMMER mit deutscher Kultur (Komma als Dezimaltrenner)
Dim oDeCultureInfo As CultureInfo = New CultureInfo("de-DE")
e.DisplayText = oValue.ToString("N2", oDeCultureInfo) & " " & currentSymbol
_Logger.Debug("[CustomColumnDisplayText] CURRENCY [{0}]: DisplayText=[{1}], Symbol=[{2}] (from Shared Dict in ConfigureViewEvents)",
e.Column.FieldName, e.DisplayText, currentSymbol)
Catch ex As Exception
_Logger.Warn("⚠️ Could not format currency value [{0}] for column [{1}]: {2}",
e.Value, e.Column.FieldName, ex.Message)
' Fallback: Original-Wert + Symbol
Dim fallbackSymbol As String = _currencySymbol
SyncLock _CurrencySymbolByGridName
If _CurrencySymbolByGridName.ContainsKey(pControl.Name) Then
fallbackSymbol = _CurrencySymbolByGridName(pControl.Name)
End If
End SyncLock
e.DisplayText = e.Value.ToString() & " " & fallbackSymbol
End Try
End If
End Sub
AddHandler pGridView.CustomRowCellEdit, Sub(sender As Object, e As CustomRowCellEditEventArgs)
Try
For Each oRow As DataRow In pColumnTable.Rows
Dim oColumnName As String = oRow.Item("SPALTENNAME").ToString()
If oColumnName <> e.Column.FieldName Then Continue For
Dim oEditorExists = GridTables_TestEditorExistsByControlAndColumn(pControlId, oColumnName)
If oEditorExists Then
' Combobox/Lookup-Editor aus GridTables: immer zuweisen
Dim oEditor = _GridTables.Item(pControlId).Item(oColumnName)
_Logger.Debug("Assigning Editor to Column [{0}]", oColumnName)
e.RepositoryItem = oEditor
Else
Dim oColumnType As String = ObjectEx.NotNull(oRow.Item("TYPE_COLUMN"), String.Empty).ToString()
If oColumnType = "CURRENCY" Then
If _FormulaColumnNames.Contains(oColumnName) Then
_Logger.Debug("CURRENCY column [{0}] is formula/readonly CustomColumnDisplayText handles display", oColumnName)
Else
_Logger.Debug("CURRENCY column [{0}] NO e.RepositoryItem set. Display via CustomColumnDisplayText, Edit via GridColumn.ColumnEdit", oColumnName)
End If
Else
_Logger.Debug("Editor for Column [{0}] does not exist", oColumnName)
End If
End If
Exit For
Next
Catch ex As Exception
_Logger.Warn("⚠️ Error in CustomRowCellEdit for [{0}]", e.CellValue)
_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
_Logger.Debug("Validating Editor for Column [{0}]", oColumnName)
GridTables_ValidateColumn(pGridView, pColumnTable, oColumnName, e.Value, e.Valid, e.ErrorText)
End Sub
AddHandler pGridView.PopupMenuShowing, AddressOf View_PopupMenuShowing
AddHandler pGridView.InvalidRowException, AddressOf View_InvalidRowException
AddHandler pGridView.ValidatingEditor, AddressOf View_ValidatingEditor
AddHandler pGridView.ShownEditor,
Sub(sender As Object, e As EventArgs)
Dim view As GridView = TryCast(sender, GridView)
If view.IsNewItemRow(view.FocusedRowHandle) Then
_Logger.Debug("Attaching Modified Handler.")
AddHandler view.ActiveEditor.Modified, Sub()
_Logger.Debug("Row was modified.")
newRowModified = True
End Sub
End If
' *** LIVE-REFRESH bei JEDER Eingabe (nur für FORMULA_EXPRESSION!) ***
' FORMULA_SQL wird NICHT hier behandelt SQL-Refresh erfolgt nur über CellValueChanged
If view.FocusedColumn IsNot Nothing AndAlso view.ActiveEditor IsNot Nothing Then
Dim oFocusedColumnName As String = view.FocusedColumn.FieldName
' Prüfen ob diese Spalte von FORMULA_EXPRESSION-Spalten referenziert wird
Dim oFormulaColumnsToRefresh As New List(Of String)
For Each oColumnData As DataRow In pColumnTable.Rows
Dim oColName = oColumnData.Item("SPALTENNAME").ToString()
Dim oExpr = ObjectEx.NotNull(oColumnData.Item("FORMULA_EXPRESSION"), String.Empty).ToString()
' Nur FORMULA_EXPRESSION FORMULA_SQL wird über CellValueChanged behandelt
If oExpr = String.Empty Then
Continue For
End If
' Spalte darf keine FORMULA_SQL haben (wurde oben validiert, Sicherheitsprüfung)
Dim oSqlExpr = ObjectEx.NotNull(oColumnData.Item("FORMULA_SQL"), String.Empty).ToString()
If oSqlExpr <> String.Empty Then
_Logger.Debug("[FormulaRefresh] Column [{0}] has both FORMULA_EXPRESSION and FORMULA_SQL skipping live refresh.", oColName)
Continue For
End If
Dim referencedColumns = GetReferencedColumnNames(oExpr)
If referencedColumns.Any(Function(col) String.Equals(col, oFocusedColumnName, StringComparison.OrdinalIgnoreCase)) Then
oFormulaColumnsToRefresh.Add(oColName)
End If
Next
If oFormulaColumnsToRefresh.Count > 0 Then
_Logger.Debug("[FormulaRefresh] Attaching EditValueChanged handler for LIVE formula updates on column [{0}].", oFocusedColumnName)
' Handler für LIVE Aktualisierung während JEDER Eingabe
AddHandler view.ActiveEditor.EditValueChanged,
Sub(editorSender As Object, editorArgs As EventArgs)
Try
Dim oRowHandle As Integer = view.FocusedRowHandle
If Not view.IsValidRowHandle(oRowHandle) Then Return
Dim oEditor As DevExpress.XtraEditors.BaseEdit = TryCast(editorSender, DevExpress.XtraEditors.BaseEdit)
If oEditor Is Nothing Then Return
Dim isNewItemRow As Boolean = view.IsNewItemRow(oRowHandle)
Try
Dim oValue As Object = oEditor.EditValue
If TypeOf oEditor Is DevExpress.XtraEditors.TextEdit Then
Dim oTextEdit As DevExpress.XtraEditors.TextEdit = DirectCast(oEditor, DevExpress.XtraEditors.TextEdit)
If oTextEdit.Properties.MaskSettings IsNot Nothing Then
oValue = oEditor.EditValue
End If
End If
If isNewItemRow Then
_Logger.Debug("[FormulaRefresh] EditValueChanged (NewItemRow) setting value for [{0}] = [{1}].", oFocusedColumnName, oValue)
_isRefreshingFormula = True
Try
' Wert setzen
view.SetRowCellValue(oRowHandle, oFocusedColumnName, If(oValue Is Nothing, DBNull.Value, oValue))
' *** KRITISCH: DoEvents() damit SetRowCellValue processed wird ***
view.UpdateCurrentRow()
' Formel-Spalten SOFORT refreshen
For Each oFormulaColumnName As String In oFormulaColumnsToRefresh
Dim oGridColumn As GridColumn = view.Columns.ColumnByFieldName(oFormulaColumnName)
If oGridColumn IsNot Nothing Then
view.RefreshRowCell(oRowHandle, oGridColumn)
_Logger.Debug("[FormulaRefresh] (NewItemRow) Refreshed [{0}], current value: [{1}]",
oFormulaColumnName, view.GetRowCellValue(oRowHandle, oGridColumn))
End If
Next
Finally
_isRefreshingFormula = False
End Try
Else
' Bestehende Row
Dim oDataRow As DataRow = view.GetDataRow(oRowHandle)
If oDataRow IsNot Nothing Then
_Logger.Debug("[FormulaRefresh] EditValueChanged setting value for [{0}] in DataTable.", oFocusedColumnName)
oDataRow.Item(oFocusedColumnName) = If(oValue Is Nothing, DBNull.Value, oValue)
For Each oFormulaColumnName As String In oFormulaColumnsToRefresh
Dim oGridColumn As GridColumn = view.Columns.ColumnByFieldName(oFormulaColumnName)
If oGridColumn IsNot Nothing Then
view.RefreshRowCell(oRowHandle, oGridColumn)
Dim currentValue = view.GetRowCellValue(oRowHandle, oGridColumn)
_Logger.Debug("[FormulaRefresh] Current value for [{0}]: [{1}]", oFormulaColumnName, currentValue)
End If
Next
End If
End If
Catch parseEx As Exception
_Logger.Debug("[FormulaRefresh] Parse error during EditValueChanged: {0}", parseEx.Message)
End Try
Catch ex As Exception
_Logger.Error(ex)
End Try
End Sub
End If
End If
End Sub
AddHandler pGridView.ValidateRow, AddressOf View_ValidateRow
AddHandler pControl.LostFocus, AddressOf Control_LostFocus
AddHandler pGridView.ShowingEditor,
Sub(sender As Object, e As CancelEventArgs)
Try
Dim oView As GridView = TryCast(sender, GridView)
If oView Is Nothing Then Return
_Logger.Debug("Showing editor.")
' Formel-Spalten dürfen keinen Editor öffnen (gilt für EXPRESSION UND SQL)
If oView.FocusedColumn IsNot Nothing Then
Dim oFieldName As String = oView.FocusedColumn.FieldName
If _FormulaColumnNames.Contains(oFieldName) Then
_Logger.Debug("Cancelling editor column [{0}] is a formula column (Expression or SQL).", oFieldName)
e.Cancel = True
Return
End If
End If
If oView.IsNewItemRow(oView.FocusedRowHandle) AndAlso Not newRowModified Then
_Logger.Debug("Adding new row.")
oView.AddNewRow()
End If
Catch ex As Exception
_Logger.Error(ex)
End Try
End Sub
AddHandler pGridView.FocusedColumnChanged,
Sub(sender As Object, e As FocusedColumnChangedEventArgs)
Try
Dim oView As GridView = TryCast(sender, GridView)
If oView Is Nothing Then Return
Dim oRowHandle As Integer = oView.FocusedRowHandle
If oView.IsNewItemRow(oRowHandle) Then Return
If Not oView.IsValidRowHandle(oRowHandle) Then Return
If oView.FocusedColumn IsNot Nothing AndAlso
_FormulaColumnNames.Contains(oView.FocusedColumn.FieldName) Then
_Logger.Debug("[FormulaRefresh] FocusedColumnChanged closing editor on formula column [{0}].", oView.FocusedColumn.FieldName)
oView.CloseEditor()
End If
Catch ex As Exception
_Logger.Error(ex)
End Try
End Sub
AddHandler pGridView.CellValueChanged,
Sub(sender As Object, e As CellValueChangedEventArgs)
' *** HandleInheritedColumnValue MUSS zuerst aufgerufen werden ***
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)
Try
Dim oView As GridView = TryCast(sender, GridView)
If oView Is Nothing OrElse e.Column Is Nothing Then Return
Dim oFormulaColumnsToRefresh As New List(Of String)
For Each oColumnData As DataRow In pColumnTable.Rows
Dim oExpr = ObjectEx.NotNull(oColumnData.Item("FORMULA_EXPRESSION"), String.Empty).ToString()
If oExpr = String.Empty Then Continue For
' Nur FORMULA_EXPRESSION kein SQL
Dim oSqlExpr = ObjectEx.NotNull(oColumnData.Item("FORMULA_SQL"), String.Empty).ToString()
If oSqlExpr <> String.Empty Then Continue For
Dim referencedColumns = GetReferencedColumnNames(oExpr)
If referencedColumns.Any(Function(col) String.Equals(col, e.Column.FieldName, StringComparison.OrdinalIgnoreCase)) Then
oFormulaColumnsToRefresh.Add(oColumnData.Item("SPALTENNAME").ToString())
End If
Next
If oFormulaColumnsToRefresh.Count = 0 Then
' Kein FORMULA_EXPRESSION-Refresh nötig weiter zu FORMULA_SQL
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)
oView.GridControl.BeginInvoke(New Action(
Sub()
Try
If Not oView.IsValidRowHandle(oRowHandle) Then Return
For Each oFormulaColumnName As String In oFormulaColumnsToRefresh
Dim oGridColumn As GridColumn = oView.Columns.ColumnByFieldName(oFormulaColumnName)
If oGridColumn Is Nothing Then Continue For
oView.RefreshRowCell(oRowHandle, oGridColumn)
_Logger.Debug("[FormulaRefresh] FALLBACK DisplayText for [{0}]: [{1}]",
oFormulaColumnName, oView.GetRowCellDisplayText(oRowHandle, oGridColumn))
Next
Catch ex As Exception
_Logger.Error(ex)
End Try
End Sub))
End If
Catch ex As Exception
_Logger.Error(ex)
End Try
' *** FORMULA_SQL-Refresh via CellValueChanged ***
' SQL wird NUR hier ausgeführt (nicht in EditValueChanged) um DB-Roundtrips zu minimieren
Try
Dim oView As GridView = TryCast(sender, GridView)
If oView Is Nothing OrElse e.Column Is Nothing Then Return
' Finde alle SQL-Formelspalten, die die geänderte Spalte referenzieren
Dim oSqlColumnsToRefresh As New List(Of String)
For Each kvp In _FormulaSqlColumns
If kvp.Value.ReferencedColumns.Any(
Function(col) String.Equals(col, e.Column.FieldName, StringComparison.OrdinalIgnoreCase)) Then
oSqlColumnsToRefresh.Add(kvp.Key)
End If
Next
If oSqlColumnsToRefresh.Count = 0 Then Return
Dim oRowHandle As Integer = e.RowHandle
_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
oView.GridControl.BeginInvoke(New Action(
Sub()
Try
If Not oView.IsValidRowHandle(oRowHandle) Then Return
For Each oSqlColumnName As String In oSqlColumnsToRefresh
Dim oDefinition = _FormulaSqlColumns(oSqlColumnName)
' Prüfen ob ALLE referenzierten Spalten einen Wert haben
Dim allValuesPresent As Boolean = True
For Each refCol In oDefinition.ReferencedColumns
Dim cellVal = oView.GetRowCellValue(oRowHandle, refCol)
If cellVal Is Nothing OrElse IsDBNull(cellVal) Then
_Logger.Debug("[FormulaSql] Column [{0}] has NULL value skipping SQL for [{1}]", refCol, oSqlColumnName)
allValuesPresent = False
Exit For
End If
Next
If Not allValuesPresent Then Continue For
' Pattern ersetzen und SQL ausführen
Dim resolvedSql = ResolveSqlTemplate(oDefinition.SqlTemplate, oView, oRowHandle)
_Logger.Debug("[FormulaSql] Executing SQL for [{0}]: [{1}]", oSqlColumnName, resolvedSql)
Try
Dim oResultTable As DataTable = DatabaseFallback.GetDatatable(
New GetDatatableOptions(resolvedSql, DatabaseType.ECM))
If oResultTable IsNot Nothing AndAlso oResultTable.Rows.Count > 0 Then
Dim oResult = oResultTable.Rows(0).Item(0)
_Logger.Debug("[FormulaSql] Result for [{0}]: [{1}]", oSqlColumnName, oResult)
_isRefreshingFormula = True
Try
oView.SetRowCellValue(oRowHandle, oSqlColumnName,
If(oResult Is Nothing OrElse IsDBNull(oResult), DBNull.Value, oResult))
oView.RefreshRowCell(oRowHandle, oView.Columns.ColumnByFieldName(oSqlColumnName))
Finally
_isRefreshingFormula = False
End Try
Else
_Logger.Warn("[FormulaSql] No result returned for [{0}]", oSqlColumnName)
End If
Catch sqlEx As Exception
_Logger.Warn("⚠️ [FormulaSql] SQL execution failed for [{0}]: {1}", oSqlColumnName, sqlEx.Message)
_Logger.Error(sqlEx)
End Try
Next
Catch ex As Exception
_Logger.Error(ex)
End Try
End Sub))
Catch ex As Exception
_Logger.Error(ex)
End Try
End Sub
End Sub
Private Sub HandleInheritedColumnValue(pView As GridView, pColumnDefinition As DataTable, pArgs As CellValueChangedEventArgs)
If pView Is Nothing OrElse pArgs Is Nothing OrElse pArgs.Column Is Nothing Then
Return
End If
' *** NEU: Bei Formel-Refresh überspringen ***
If _isRefreshingFormula Then
_Logger.Debug("Skipping HandleInheritedColumnValue during formula refresh.")
Return
End If
If isApplyingInheritedValue OrElse pArgs.RowHandle = DevExpress.XtraGrid.GridControl.InvalidRowHandle Then
Return
End If
Dim oColumnData As DataRow = pColumnDefinition.
Select($"SPALTENNAME = '{pArgs.Column.FieldName}'").
FirstOrDefault()
If oColumnData Is Nothing Then
Return
End If
Dim inheritRaw = ObjectEx.NotNull(oColumnData.Item("INHERIT_VALUE"), 0)
Dim inheritEnabled As Boolean
If TypeOf inheritRaw Is Boolean Then
inheritEnabled = CBool(inheritRaw)
Else
Dim inheritInt As Integer
inheritEnabled = Integer.TryParse(inheritRaw.ToString(), inheritInt) AndAlso inheritInt = 1
End If
If Not inheritEnabled Then
Return
End If
Dim listIndex = pView.GetDataSourceRowIndex(pArgs.RowHandle)
If listIndex = DevExpress.XtraGrid.GridControl.InvalidRowHandle Then
Return
End If
' Benutzerbestätigung für Wertvererbung
Dim valueToApply = pArgs.Value
Dim affectedRowsCount = pView.DataRowCount - listIndex - 1
Dim confirmationEntry = GetInheritanceConfirmationEntry(pArgs.Column.FieldName)
If affectedRowsCount > 0 AndAlso confirmationEntry.Count < InheritanceMsgAmount Then
Dim confirmMessage As String = String.Format(
"Möchten Sie den Wert '{0}' an {1} nachfolgende Zeile(n) vererben?",
valueToApply,
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)
End If
Dim confirmTitle As String = "Wertevererbung bestätigen"
If USER_LANGUAGE <> "de-DE" Then
confirmTitle = "Confirm Value Inheritance"
End If
Dim result = MessageBox.Show(
confirmMessage,
confirmTitle,
MessageBoxButtons.YesNo,
MessageBoxIcon.Question,
MessageBoxDefaultButton.Button1)
If result <> DialogResult.Yes Then
_Logger.Debug("User cancelled value inheritance")
Return
End If
Dim newCount = confirmationEntry.Count + 1
confirmationEntry.Count = newCount
SetInheritanceConfirmationCount(confirmationEntry.ColumnName, newCount)
_Logger.Info("User confirmed value inheritance. Confirmation count: {0}", newCount)
ElseIf affectedRowsCount > 0 AndAlso confirmationEntry.Count = InheritanceMsgAmount Then
' Schwellenwert erreicht - User fragen, ob er weiterhin gefragt werden möchte
Dim continueAskingMessage As String = "Sie haben diese Bestätigung bereits mehrfach durchgeführt. Möchten Sie in Zukunft weiterhin gefragt werden?"
If USER_LANGUAGE <> "de-DE" Then
continueAskingMessage = "You have confirmed this action multiple times. Do you want to continue being asked in the future?"
End If
Dim continueAskingTitle As String = "Bestätigungen fortsetzen?"
If USER_LANGUAGE <> "de-DE" Then
continueAskingTitle = "Continue Confirmations?"
End If
Dim continueResult = MessageBox.Show(
continueAskingMessage,
continueAskingTitle,
MessageBoxButtons.YesNo,
MessageBoxIcon.Question,
MessageBoxDefaultButton.Button2)
If continueResult = DialogResult.Yes Then
' User möchte weiterhin gefragt werden - Counter zurücksetzen
confirmationEntry.Count = 0
SetInheritanceConfirmationCount(confirmationEntry.ColumnName, 0)
_Logger.Info("User wants to continue being asked. Counter reset to 0.")
Else
' User möchte nicht mehr gefragt werden - Counter erhöhen
Dim newCount = confirmationEntry.Count + 1
confirmationEntry.Count = newCount
SetInheritanceConfirmationCount(confirmationEntry.ColumnName, newCount)
_Logger.Info("User does not want to be asked anymore. Counter increased to {0}.", newCount)
End If
ElseIf affectedRowsCount > 0 Then
_Logger.Info("Skipping confirmation dialog (already confirmed {0} times)", confirmationEntry.Count)
End If
isApplyingInheritedValue = True
Try
_Logger.Info(String.Format("Inherit Value is active for column. So inheritting the value [{0}]...", valueToApply))
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
Continue For
End If
If pView.IsNewItemRow(targetHandle) Then
Exit For
End If
Dim existingValue = pView.GetRowCellValue(targetHandle, pArgs.Column.FieldName)
If Equals(existingValue, valueToApply) Then
Continue For
End If
pView.SetRowCellValue(targetHandle, pArgs.Column.FieldName, valueToApply)
Next
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
Return
End If
Dim entryIndex = entries.FindIndex(Function(item) String.Equals(item.ColumnName, columnName, StringComparison.OrdinalIgnoreCase))
If entryIndex < 0 Then
Return
End If
Dim entry = entries(entryIndex)
entry.Count = newCount
entries(entryIndex) = entry
End Sub
Private Function GetInheritanceConfirmationEntry(columnName As String) As UserInheritanceConfirmation
Dim entries = UserInheritance_ConfirmationByColumn
If entries Is Nothing Then
entries = New List(Of UserInheritanceConfirmation)()
UserInheritance_ConfirmationByColumn = entries
End If
Dim entry = entries.FirstOrDefault(Function(item) String.Equals(item.ColumnName, columnName, StringComparison.OrdinalIgnoreCase))
If entry Is Nothing Then
entry = New UserInheritanceConfirmation With {
.ColumnName = columnName,
.Count = 0
}
entries.Add(entry)
End If
Return entry
End Function
Private Sub View_PopupMenuShowing(sender As Object, e As PopupMenuShowingEventArgs)
Dim view As GridView = TryCast(sender, GridView)
Dim oFocusedColumn As GridColumn = view.FocusedColumn
If IsNothing(oFocusedColumn) Then
MsgBox("Please focus a column first.", MsgBoxStyle.Information, "No Column focused")
Exit Sub
End If
Dim oColumnType As Type = oFocusedColumn.ColumnType
Dim oColumnName As String = oFocusedColumn.FieldName
If e.MenuType = GridMenuType.Column AndAlso oColumnType Is GetType(Boolean) Then
e.Menu.Items.Add(New DevExpress.Utils.Menu.DXMenuItem(
"Alle Zeilen anhaken",
Sub(_sender As Object, _e As EventArgs)
SetCellValuesForBooleanColumn(view, oColumnName, True)
End Sub,
My.Resources.itemtypechecked,
DevExpress.Utils.Menu.DXMenuItemPriority.Normal))
e.Menu.Items.Add(New DevExpress.Utils.Menu.DXMenuItem(
"Alle Zeilen abhaken",
Sub(_sender As Object, _e As EventArgs)
SetCellValuesForBooleanColumn(view, oColumnName, False)
End Sub,
My.Resources.itemtypechecked,
DevExpress.Utils.Menu.DXMenuItemPriority.Normal))
End If
End Sub
Private Sub SetCellValuesForBooleanColumn(pView As GridView, pColumnName As String, pValue As Boolean)
Dim oRowHandle = 0
While (pView.IsValidRowHandle(oRowHandle))
Dim oRow = pView.GetDataRow(oRowHandle)
Dim oValue = oRow.ItemEx(pColumnName, False)
oRow.Item(pColumnName) = pValue
oRowHandle += 1
End While
End Sub
Private Sub Control_LostFocus(sender As DevExpress.XtraGrid.GridControl, e As EventArgs)
Dim oView2 As GridView = sender.FocusedView
' 19.08.2022:
' Calling UpdateCurrentRow when newRowModified Is true
' leads to some weird jumping of focus in the current cell.
' This seems to fix it.
If newRowModified = False Then
oView2.UpdateCurrentRow()
End If
End Sub
Private Sub View_ValidateRow(sender As Object, e As ValidateRowEventArgs)
Dim view As GridView = TryCast(sender, GridView)
' RowHandle für RowUpdated merken, bevor er sich nach dem Commit ändert
If view.IsNewItemRow(e.RowHandle) AndAlso Not newRowModified Then
_Logger.Debug("Deleting unused row")
view.DeleteRow(e.RowHandle)
End If
_Logger.Debug("Validating row. Resetting Modified.")
newRowModified = False
End Sub
Private Sub View_ValidatingEditor(sender As Object, e As BaseContainerValidateEditorEventArgs)
Dim oValue As String = ObjectEx.NotNull(e.Value, "")
If oValue.Contains(" | ") Then
oValue = oValue.Split(" | ").ToList().First()
e.Value = oValue
End If
End Sub
Private Sub View_InvalidRowException(sender As Object, e As InvalidRowExceptionEventArgs)
e.ExceptionMode = ExceptionMode.NoAction
End Sub
Private Function GridTables_TestEditorExistsByControlAndColumn(oControlId As Integer, pColumn As String) As Boolean
If _GridTables.ContainsKey(oControlId) Then
Dim oContainsKey = _GridTables.Item(oControlId).ContainsKey(pColumn)
If oContainsKey AndAlso _GridTables.Item(oControlId).Item(pColumn) IsNot Nothing Then
Return True
Else
Return False
End If
Else
Return False
End If
End Function
Private Function GridTables_ValidateColumn(pView As GridView, pColumnDefinition As DataTable, ColumnName As String, pValue As Object, ByRef pIsValid As Boolean, ByRef pErrorText As String) As Boolean
Dim oColumn As DataRow = (From r As DataRow In pColumnDefinition.Rows
Where r.Item("SPALTENNAME") = ColumnName
Select r).FirstOrDefault()
Dim oGridColumn As GridColumn = (From c As GridColumn In pView.Columns
Where c.FieldName = ColumnName
Select c).FirstOrDefault()
Dim oIsRequired = oColumn.Item("VALIDATION")
Try
Dim oRegex = ObjectEx.NotNull(oColumn.Item("REGEX_MATCH"), String.Empty)
Dim oRegexMessage = ObjectEx.NotNull(oColumn.Item("REGEX_MESSAGE_DE"), String.Empty)
If oRegex <> String.Empty Then
Dim oMatch = New Regex(oRegex).IsMatch(pValue.ToString)
Dim oDefaultMessage = "Wert entspricht nicht dem gefordertem Format!"
Dim oMessage = IIf(oRegexMessage <> String.Empty, oRegexMessage, oDefaultMessage)
If oMatch = False Then
pErrorText = oMessage
pIsValid = False
Return False
End If
End If
Catch ex As Exception
_Logger.Error(ex)
End Try
If oIsRequired And (pValue IsNot Nothing AndAlso pValue.ToString = "") Then
pErrorText = "Spalte muss ausgefüllt werden!"
pIsValid = False
Return False
End If
Return True
End Function
End Class
End Namespace