From 4f09e678d2dbacf62351f0bcd88a61891f2d56f1 Mon Sep 17 00:00:00 2001 From: Developer01 Date: Mon, 12 Oct 2026 15:50:31 +0200 Subject: [PATCH] Neue Projekte & Optimierungen in TaskFlow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Es wurden zwei neue Projekte (`EDMI.API` und `EDMIService`) zur Solution hinzugefügt und in die Build-Konfiguration integriert. Die Benutzeroberfläche (`frmAdmin_AppserviceTest.Designer.vb`) wurde angepasst, einschließlich Änderungen an Button-Größen, Positionen und `NumericUpDown1`-Werten. Die Logik für Benutzer- und Sitzungsinformationen wurde verbessert, um Null-Checks und die Anzeige kürzlich aktiver Benutzer zu unterstützen. Der Lasttest wurde optimiert: Verwendung von `ConcurrentBag`, Einführung von `IProgress` für UI-Feedback und Begrenzung der Parallelität. Beim Schließen des Validators (`frmMain.vb`) wurde ein DevExpress-Overlay hinzugefügt, um den Ladezustand anzuzeigen, und die Performance beim Neuaufbau des Grids verbessert. Allgemeine Verbesserungen umfassen explizite Typkonvertierungen, bessere Fehlerbehandlung und Logging. --- app/TaskFlow.sln | 44 ++++++ .../frmAdmin_AppserviceTest.Designer.vb | 16 +- app/TaskFlow/frmAdmin_AppserviceTest.vb | 140 ++++++++++++------ app/TaskFlow/frmMain.vb | 29 +++- 4 files changed, 176 insertions(+), 53 deletions(-) diff --git a/app/TaskFlow.sln b/app/TaskFlow.sln index b61a2d2..8c23611 100644 --- a/app/TaskFlow.sln +++ b/app/TaskFlow.sln @@ -9,6 +9,10 @@ Project("{F184B08F-C81C-45F6-A57F-5ABD9991F28F}") = "taskFLOW", "TaskFlow\taskFL EndProject Project("{F184B08F-C81C-45F6-A57F-5ABD9991F28F}") = "SnapPanel", "..\..\..\2_DLL Projekte\DDMonorepo\Controls.SnapPanel\SnapPanel.vbproj", "{3E19C413-3197-4635-944E-558FC04C3475}" EndProject +Project("{F184B08F-C81C-45F6-A57F-5ABD9991F28F}") = "EDMI.API", "..\..\..\2_DLL Projekte\DDModules\EDMIAPI\EDMI.API.vbproj", "{25017513-0D97-49D3-98D7-BA76D9B251B0}" +EndProject +Project("{F184B08F-C81C-45F6-A57F-5ABD9991F28F}") = "EDMIService", "..\..\..\2_DLL Projekte\DDMonorepo\Service.EDMIService\EDMIService.vbproj", "{A8C3F298-76AB-4359-AB3C-986E313B4336}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution CD_ROM|Any CPU = CD_ROM|Any CPU @@ -83,6 +87,46 @@ Global {3E19C413-3197-4635-944E-558FC04C3475}.SingleImage|Any CPU.Build.0 = Release|Any CPU {3E19C413-3197-4635-944E-558FC04C3475}.SingleImage|x86.ActiveCfg = Release|Any CPU {3E19C413-3197-4635-944E-558FC04C3475}.SingleImage|x86.Build.0 = Release|Any CPU + {25017513-0D97-49D3-98D7-BA76D9B251B0}.CD_ROM|Any CPU.ActiveCfg = Release|Any CPU + {25017513-0D97-49D3-98D7-BA76D9B251B0}.CD_ROM|Any CPU.Build.0 = Release|Any CPU + {25017513-0D97-49D3-98D7-BA76D9B251B0}.CD_ROM|x86.ActiveCfg = Release|Any CPU + {25017513-0D97-49D3-98D7-BA76D9B251B0}.CD_ROM|x86.Build.0 = Release|Any CPU + {25017513-0D97-49D3-98D7-BA76D9B251B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {25017513-0D97-49D3-98D7-BA76D9B251B0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {25017513-0D97-49D3-98D7-BA76D9B251B0}.Debug|x86.ActiveCfg = Debug|Any CPU + {25017513-0D97-49D3-98D7-BA76D9B251B0}.Debug|x86.Build.0 = Debug|Any CPU + {25017513-0D97-49D3-98D7-BA76D9B251B0}.DVD-5|Any CPU.ActiveCfg = Release|Any CPU + {25017513-0D97-49D3-98D7-BA76D9B251B0}.DVD-5|Any CPU.Build.0 = Release|Any CPU + {25017513-0D97-49D3-98D7-BA76D9B251B0}.DVD-5|x86.ActiveCfg = Release|Any CPU + {25017513-0D97-49D3-98D7-BA76D9B251B0}.DVD-5|x86.Build.0 = Release|Any CPU + {25017513-0D97-49D3-98D7-BA76D9B251B0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {25017513-0D97-49D3-98D7-BA76D9B251B0}.Release|Any CPU.Build.0 = Release|Any CPU + {25017513-0D97-49D3-98D7-BA76D9B251B0}.Release|x86.ActiveCfg = Release|Any CPU + {25017513-0D97-49D3-98D7-BA76D9B251B0}.Release|x86.Build.0 = Release|Any CPU + {25017513-0D97-49D3-98D7-BA76D9B251B0}.SingleImage|Any CPU.ActiveCfg = Release|Any CPU + {25017513-0D97-49D3-98D7-BA76D9B251B0}.SingleImage|Any CPU.Build.0 = Release|Any CPU + {25017513-0D97-49D3-98D7-BA76D9B251B0}.SingleImage|x86.ActiveCfg = Release|Any CPU + {25017513-0D97-49D3-98D7-BA76D9B251B0}.SingleImage|x86.Build.0 = Release|Any CPU + {A8C3F298-76AB-4359-AB3C-986E313B4336}.CD_ROM|Any CPU.ActiveCfg = Release|Any CPU + {A8C3F298-76AB-4359-AB3C-986E313B4336}.CD_ROM|Any CPU.Build.0 = Release|Any CPU + {A8C3F298-76AB-4359-AB3C-986E313B4336}.CD_ROM|x86.ActiveCfg = Release|Any CPU + {A8C3F298-76AB-4359-AB3C-986E313B4336}.CD_ROM|x86.Build.0 = Release|Any CPU + {A8C3F298-76AB-4359-AB3C-986E313B4336}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A8C3F298-76AB-4359-AB3C-986E313B4336}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A8C3F298-76AB-4359-AB3C-986E313B4336}.Debug|x86.ActiveCfg = Debug|Any CPU + {A8C3F298-76AB-4359-AB3C-986E313B4336}.Debug|x86.Build.0 = Debug|Any CPU + {A8C3F298-76AB-4359-AB3C-986E313B4336}.DVD-5|Any CPU.ActiveCfg = Release|Any CPU + {A8C3F298-76AB-4359-AB3C-986E313B4336}.DVD-5|Any CPU.Build.0 = Release|Any CPU + {A8C3F298-76AB-4359-AB3C-986E313B4336}.DVD-5|x86.ActiveCfg = Release|Any CPU + {A8C3F298-76AB-4359-AB3C-986E313B4336}.DVD-5|x86.Build.0 = Release|Any CPU + {A8C3F298-76AB-4359-AB3C-986E313B4336}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A8C3F298-76AB-4359-AB3C-986E313B4336}.Release|Any CPU.Build.0 = Release|Any CPU + {A8C3F298-76AB-4359-AB3C-986E313B4336}.Release|x86.ActiveCfg = Release|Any CPU + {A8C3F298-76AB-4359-AB3C-986E313B4336}.Release|x86.Build.0 = Release|Any CPU + {A8C3F298-76AB-4359-AB3C-986E313B4336}.SingleImage|Any CPU.ActiveCfg = Release|Any CPU + {A8C3F298-76AB-4359-AB3C-986E313B4336}.SingleImage|Any CPU.Build.0 = Release|Any CPU + {A8C3F298-76AB-4359-AB3C-986E313B4336}.SingleImage|x86.ActiveCfg = Release|Any CPU + {A8C3F298-76AB-4359-AB3C-986E313B4336}.SingleImage|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/app/TaskFlow/frmAdmin_AppserviceTest.Designer.vb b/app/TaskFlow/frmAdmin_AppserviceTest.Designer.vb index 0296189..8c4fef6 100644 --- a/app/TaskFlow/frmAdmin_AppserviceTest.Designer.vb +++ b/app/TaskFlow/frmAdmin_AppserviceTest.Designer.vb @@ -48,7 +48,7 @@ Partial Class frmAdmin_AppserviceTest ' Me.SimpleButton1.Location = New System.Drawing.Point(12, 1) Me.SimpleButton1.Name = "SimpleButton1" - Me.SimpleButton1.Size = New System.Drawing.Size(196, 32) + Me.SimpleButton1.Size = New System.Drawing.Size(240, 32) Me.SimpleButton1.TabIndex = 1 Me.SimpleButton1.Text = "Prüfe Status EDMI Service" ' @@ -78,7 +78,7 @@ Partial Class frmAdmin_AppserviceTest ' 'btnLoadStart ' - Me.btnLoadStart.Location = New System.Drawing.Point(12, 150) + Me.btnLoadStart.Location = New System.Drawing.Point(12, 261) Me.btnLoadStart.Name = "btnLoadStart" Me.btnLoadStart.Size = New System.Drawing.Size(250, 38) Me.btnLoadStart.TabIndex = 5 @@ -86,7 +86,7 @@ Partial Class frmAdmin_AppserviceTest ' 'btnLoadStop ' - Me.btnLoadStop.Location = New System.Drawing.Point(12, 223) + Me.btnLoadStop.Location = New System.Drawing.Point(12, 334) Me.btnLoadStop.Name = "btnLoadStop" Me.btnLoadStop.Size = New System.Drawing.Size(250, 32) Me.btnLoadStop.TabIndex = 6 @@ -95,7 +95,7 @@ Partial Class frmAdmin_AppserviceTest ' 'LabelControl1 ' - Me.LabelControl1.Location = New System.Drawing.Point(12, 197) + Me.LabelControl1.Location = New System.Drawing.Point(12, 308) Me.LabelControl1.Name = "LabelControl1" Me.LabelControl1.Size = New System.Drawing.Size(143, 13) Me.LabelControl1.TabIndex = 10 @@ -104,14 +104,14 @@ Partial Class frmAdmin_AppserviceTest 'NumericUpDown1 ' Me.NumericUpDown1.Font = New System.Drawing.Font("Segoe UI", 8.25!, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, CType(0, Byte)) - Me.NumericUpDown1.Increment = New Decimal(New Integer() {50, 0, 0, 0}) - Me.NumericUpDown1.Location = New System.Drawing.Point(208, 192) + Me.NumericUpDown1.Increment = New Decimal(New Integer() {10, 0, 0, 0}) + Me.NumericUpDown1.Location = New System.Drawing.Point(208, 303) Me.NumericUpDown1.Maximum = New Decimal(New Integer() {1000, 0, 0, 0}) - Me.NumericUpDown1.Minimum = New Decimal(New Integer() {100, 0, 0, 0}) + Me.NumericUpDown1.Minimum = New Decimal(New Integer() {10, 0, 0, 0}) Me.NumericUpDown1.Name = "NumericUpDown1" Me.NumericUpDown1.Size = New System.Drawing.Size(54, 22) Me.NumericUpDown1.TabIndex = 11 - Me.NumericUpDown1.Value = New Decimal(New Integer() {100, 0, 0, 0}) + Me.NumericUpDown1.Value = New Decimal(New Integer() {10, 0, 0, 0}) ' 'SplitContainerControl1 ' diff --git a/app/TaskFlow/frmAdmin_AppserviceTest.vb b/app/TaskFlow/frmAdmin_AppserviceTest.vb index dfe36e7..a35f1ed 100644 --- a/app/TaskFlow/frmAdmin_AppserviceTest.vb +++ b/app/TaskFlow/frmAdmin_AppserviceTest.vb @@ -13,8 +13,9 @@ Public Class frmAdmin_AppserviceTest Private _loadLog As New ConcurrentQueue(Of String) Private _loadCts As CancellationTokenSource Private _timerTick As Integer = 0 - Private LOAD_CLIENT_COUNT As Integer = 100 + Private LOAD_CLIENT_COUNT As Integer = 10 Private Shared ReadOnly _rnd As New Random() ' ◄ ergänzen + Private Const RECENT_USER_MAX_AGE_MINUTES As Integer = 15 ' ◄ NEU – muss mit Service-Wert übereinstimmen Private Sub SimpleButton1_Click(sender As Object, e As EventArgs) Handles SimpleButton1.Click ' Status abrufen Dim oStatus = _EDMIClient.GetStatus() @@ -59,16 +60,34 @@ Public Class frmAdmin_AppserviceTest MemoEdit1.Text &= "─────────────────────────────" & Environment.NewLine MemoEdit1.Text &= "Aktive Benutzer:" & Environment.NewLine - For Each oUser In oStatus2.ActiveUsers - MemoEdit1.Text &= $" • {oUser}" & Environment.NewLine - Next + If oStatus2.ActiveUsers IsNot Nothing AndAlso oStatus2.ActiveUsers.Count > 0 Then + For Each oUser In oStatus2.ActiveUsers + MemoEdit1.Text &= $" • {oUser}" & Environment.NewLine + Next + Else + MemoEdit1.Text &= " (keine)" & Environment.NewLine + End If MemoEdit1.Text &= "─────────────────────────────" & Environment.NewLine MemoEdit1.Text &= "Sessions (Id → Benutzer):" & Environment.NewLine - For Each oPair In oStatus2.ActiveSessions - MemoEdit1.Text &= $" {oPair.Key.Substring(0, Math.Min(8, oPair.Key.Length))}… → {oPair.Value}" & Environment.NewLine - Next + If oStatus2.ActiveSessions IsNot Nothing AndAlso oStatus2.ActiveSessions.Count > 0 Then + For Each oPair In oStatus2.ActiveSessions + MemoEdit1.Text &= $" {oPair.Key.Substring(0, Math.Min(8, oPair.Key.Length))}… → {oPair.Value}" & Environment.NewLine + Next + Else + MemoEdit1.Text &= " (keine)" & Environment.NewLine + End If + + MemoEdit1.Text &= "─────────────────────────────" & Environment.NewLine + MemoEdit1.Text &= $"Kürzlich aktive Benutzer (letzte {RECENT_USER_MAX_AGE_MINUTES} Min.):" & Environment.NewLine + If oStatus2.RecentUsers IsNot Nothing AndAlso oStatus2.RecentUsers.Count > 0 Then + For Each oRecentUser In oStatus2.RecentUsers + MemoEdit1.Text &= $" • {oRecentUser}" & Environment.NewLine + Next + Else + MemoEdit1.Text &= " (keine)" & Environment.NewLine + End If End Sub Private Sub SimpleButton2_Click(sender As Object, e As EventArgs) Handles btnLoadStart.Click @@ -76,52 +95,85 @@ Public Class frmAdmin_AppserviceTest MessageBox.Show("Lasttest läuft bereits.", "Hinweis", MessageBoxButtons.OK, MessageBoxIcon.Information) Return End If - LOAD_CLIENT_COUNT = NumericUpDown1.Value + + LOAD_CLIENT_COUNT = CInt(NumericUpDown1.Value) _loadLog = New ConcurrentQueue(Of String) _loadCts = New CancellationTokenSource() _timerTick = 0 - ' 150 Client-Verbindungen parallel aufbauen btnLoadStart.Enabled = False btnLoadStop.Enabled = True btnLoadStop.Visible = True MemoEdit2.Text = "" MemoEdit2.Text &= $"[{Now:HH:mm:ss}] Baue {LOAD_CLIENT_COUNT} Verbindungen auf…" & Environment.NewLine - Task.Run(Sub() - _loadClients = New List(Of Client) - Dim errors As Integer = 0 - Parallel.For(0, LOAD_CLIENT_COUNT, - New ParallelOptions With {.CancellationToken = _loadCts.Token}, - Sub(i) - Try - ' ► Hier ggf. eigene Client-Factory verwenden - Dim oClientName = "PerformanceClient_" & (i + 1) - Dim oClient = New Client(LOGCONFIG, CONFIG.Config.EDMIAppServer, oClientName) - Dim oConnected = oClient.Connect() ' ◄ Connect() muss aufgerufen werden + ' ► IProgress für thread-sicheres UI-Feedback während des Aufbaus + Dim progress As New Progress(Of String)(Sub(msg) + MemoEdit2.Text &= msg & Environment.NewLine + MemoEdit2.SelectionStart = MemoEdit2.Text.Length + MemoEdit2.ScrollToCaret() + End Sub) - SyncLock _loadClients - If oConnected Then ' ◄ nur verbundene hinzufügen - _loadClients.Add(oClient) - _loadLog.Enqueue($"[{Now:HH:mm:ss}] Client #{i + 1} verbunden") - Else - oClient.Dispose() - Interlocked.Increment(errors) - _loadLog.Enqueue($"[{Now:HH:mm:ss}] Client #{i + 1} Connect() fehlgeschlagen") - End If - End SyncLock - Catch ex As Exception - Interlocked.Increment(errors) - _loadLog.Enqueue($"[{Now:HH:mm:ss}] Client #{i + 1} Fehler: {ex.Message}") - End Try - End Sub) + Dim token = _loadCts.Token - Me.Invoke(Sub() - FlushLoadLog() - MemoEdit2.Text &= $"[{Now:HH:mm:ss}] {_loadClients.Count} Clients aktiv, {errors} Fehler" & Environment.NewLine - StartLoadTimer() - End Sub) - End Sub) + Task.Run( + Async Function() + ' ► ConcurrentBag ersetzt List(Of Client) + SyncLock beim Hinzufügen + Dim bag As New System.Collections.Concurrent.ConcurrentBag(Of Client) + Dim errors As Integer = 0 + + Dim options As New ParallelOptions With { + .CancellationToken = token, + .MaxDegreeOfParallelism = Math.Min(LOAD_CLIENT_COUNT, 32) ' ◄ Threadpool schonen + } + + Try + Parallel.For(0, LOAD_CLIENT_COUNT, options, + Sub(i) + token.ThrowIfCancellationRequested() + Try + Dim oClientName = $"PerformanceClient_{i + 1}" + Dim oClient = New Client(LOGCONFIG, CONFIG.Config.EDMIAppServer, oClientName) + Dim oConnected = oClient.Connect() + + If oConnected Then + bag.Add(oClient) + _loadLog.Enqueue($"[{Now:HH:mm:ss}] Client #{i + 1} verbunden") + Else + oClient.Dispose() + Interlocked.Increment(errors) + _loadLog.Enqueue($"[{Now:HH:mm:ss}] Client #{i + 1} Connect() fehlgeschlagen") + End If + Catch ex As OperationCanceledException + ' Abbruch gewünscht – still beenden + Catch ex As Exception + Interlocked.Increment(errors) + _loadLog.Enqueue($"[{Now:HH:mm:ss}] Client #{i + 1} Fehler: {ex.Message}") + End Try + End Sub) + Catch ex As OperationCanceledException + ' ◄ Parallel.For selbst kann werfen – abfangen + _loadLog.Enqueue($"[{Now:HH:mm:ss}] Verbindungsaufbau abgebrochen.") + End Try + + ' ► Erst nach Abschluss: thread-sicher in _loadClients übertragen + Dim connectedClients = bag.ToList() + + Me.Invoke(Sub() + ' ◄ Zuweisung auf UI-Thread – keine Race Condition + _loadClients = connectedClients + FlushLoadLog() + MemoEdit2.Text &= $"[{Now:HH:mm:ss}] {_loadClients.Count} Clients aktiv, {errors} Fehler" & Environment.NewLine + If _loadClients.Count > 0 Then + StartLoadTimer() + Else + ' ◄ Kein einziger Client verbunden → Test nicht starten + MemoEdit2.Text &= $"[{Now:HH:mm:ss}] Kein Client verbunden – Lasttest wird nicht gestartet." & Environment.NewLine + btnLoadStart.Enabled = True + btnLoadStop.Enabled = False + End If + End Sub) + End Function) End Sub Private Sub StopLoadTest() _loadCts?.Cancel() @@ -166,8 +218,12 @@ Public Class frmAdmin_AppserviceTest oSnapshot = _loadClients.ToList() End SyncLock + ' ← MaxDegreeOfParallelism begrenzt gleichzeitige Service-Calls Parallel.ForEach(oSnapshot, - New ParallelOptions With {.CancellationToken = _loadCts.Token}, + New ParallelOptions With { + .CancellationToken = _loadCts.Token, + .MaxDegreeOfParallelism = 10 ' ← NEU: max. 10 gleichzeitige Calls + }, Sub(oClient As Client) Try Select Case _timerTick Mod 4 diff --git a/app/TaskFlow/frmMain.vb b/app/TaskFlow/frmMain.vb index 2fe3277..71169bc 100644 --- a/app/TaskFlow/frmMain.vb +++ b/app/TaskFlow/frmMain.vb @@ -2810,15 +2810,38 @@ Public Class frmMain End Try End Function Private Async Sub ValidatorClosed(sender As Object, e As FormClosedEventArgs) + Dim oGridOverlayHandle As Object = Nothing Try - ' LoadingPanel-Verwaltung erfolgt in Decide_Load - Await Decide_Load(False, True) + ' Aktuellen Grid-Zustand sichern BEVOR DataSource zurückgesetzt wird + If GridControlWorkflows.Visible = True AndAlso FormOpenClose = False Then + RefreshHelper.SaveViewInfo() + End If - If GridControlWorkflows.Visible = True And FormOpenClose = False Then + ' DevExpress-Overlay NUR über dem Grid anzeigen – verhindert leere Bereiche + ' und signalisiert dem Nutzer den laufenden Ladevorgang direkt im Grid-Bereich. + ' ShowOverlayForm(Control) ist die empfohlene DevExpress-Methode hierfür. + oGridOverlayHandle = SplashScreenManager.ShowOverlayForm(GridControlWorkflows) + + ' BeginUpdate verhindert zusätzliche Paint-Events während des Neuaufbaus. + GridControlWorkflows.BeginUpdate() + GridViewWorkflows.BeginUpdate() + Try + Await Decide_Load(False, True) + Finally + GridViewWorkflows.EndUpdate() + GridControlWorkflows.EndUpdate() + End Try + + If GridControlWorkflows.Visible = True AndAlso FormOpenClose = False Then RefreshHelper.LoadViewInfo() End If Catch ex As Exception LOGGER.Error(ex) + Finally + ' Overlay immer schließen – auch bei Fehlern + If oGridOverlayHandle IsNot Nothing Then + SplashScreenManager.CloseOverlayForm(oGridOverlayHandle) + End If End Try End Sub Private Function Init_IDB()