@
Phase 2: Launcher-UI + eigenstaendige Modul-Fenster - Form1 -> LauncherForm (Dateien via git mv, Designer/resx angepasst) - Modul-Tab: Karten aus ModuleRegistry mit "Fenster oeffnen"-Button je Modul - UI/WindowManager: Fenster-Tracking (Key->Form), Re-Open fokussiert, CloseAll - UI/ModuleFormBase: Basisklasse fuer eigenstaendige Modul-Fenster - CongressTradingForm: DB-Kennzahlen + manueller Scrape-Trigger (Phase-4-Ausbau folgt) - WindowManager in DI; Launcher schliesst Modul-Fenster beim Beenden - Tests: WindowManager (6) -> 21/21 gruen; Launcher-Start verifiziert Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> @
This commit is contained in:
@@ -1,8 +1,13 @@
|
|||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
|
using IBKRTrader.Core.Database;
|
||||||
|
using IBKRTrader.Core.Logging;
|
||||||
|
using IBKRTrader.Core.Settings;
|
||||||
|
using IBKRTrader.Core.Workers;
|
||||||
using IBKRTrader.Modules.CongressTrading;
|
using IBKRTrader.Modules.CongressTrading;
|
||||||
using IBKRTrader.Modules.CongressTrading.Database;
|
using IBKRTrader.Modules.CongressTrading.Database;
|
||||||
using IBKRTrader.Modules.CongressTrading.Scraper;
|
using IBKRTrader.Modules.CongressTrading.Scraper;
|
||||||
using IBKRTrader.Modules.CongressTrading.Workers;
|
using IBKRTrader.Modules.CongressTrading.Workers;
|
||||||
|
using IBKRTrader.UI;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace IBKRTrader.Tests.Modules;
|
namespace IBKRTrader.Tests.Modules;
|
||||||
@@ -39,11 +44,22 @@ public class CongressTradingModuleTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void CreateWindow_ReturnsForm()
|
public void CreateWindow_ReturnsModuleForm()
|
||||||
{
|
{
|
||||||
using var form = new CongressTradingModule().CreateWindow(new ServiceCollection().BuildServiceProvider());
|
// Minimaler DI-Container mit allen von CreateWindow benötigten Abhängigkeiten.
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
services.AddSingleton<SettingsService>();
|
||||||
|
services.AddSingleton<LoggingService>();
|
||||||
|
services.AddSingleton<DatabaseService>();
|
||||||
|
services.AddSingleton<IEnumerable<IWorker>>(_ => Array.Empty<IWorker>());
|
||||||
|
services.AddSingleton<WorkerEngine>();
|
||||||
|
var module = new CongressTradingModule();
|
||||||
|
module.RegisterServices(services);
|
||||||
|
|
||||||
form.Should().NotBeNull();
|
using var provider = services.BuildServiceProvider();
|
||||||
form.Text.Should().Contain("CT");
|
using var form = module.CreateWindow(provider);
|
||||||
|
|
||||||
|
form.Should().BeAssignableTo<ModuleFormBase>();
|
||||||
|
((ModuleFormBase)form).ModuleKey.Should().Be("CT");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
using FluentAssertions;
|
||||||
|
using IBKRTrader.UI;
|
||||||
|
|
||||||
|
namespace IBKRTrader.Tests.UI;
|
||||||
|
|
||||||
|
[Trait("cat", "unit")]
|
||||||
|
public class WindowManagerTests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Testbare Variante: unterdrückt echte Show-/Focus-Aufrufe (kein Message-Loop),
|
||||||
|
/// zählt aber die Focus-Aufrufe.
|
||||||
|
/// </summary>
|
||||||
|
private sealed class TestWindowManager : WindowManager
|
||||||
|
{
|
||||||
|
public int FocusCount;
|
||||||
|
protected override void Present(Form form) { /* kein Show im Test */ }
|
||||||
|
protected override void Focus(Form form) => FocusCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OpenOrFocus_NewKey_InvokesFactory_AndTracks()
|
||||||
|
{
|
||||||
|
var wm = new TestWindowManager();
|
||||||
|
var created = 0;
|
||||||
|
|
||||||
|
using var form = wm.OpenOrFocus("CT", () => { created++; return new Form(); });
|
||||||
|
|
||||||
|
created.Should().Be(1);
|
||||||
|
wm.IsOpen("CT").Should().BeTrue();
|
||||||
|
wm.OpenCount.Should().Be(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OpenOrFocus_SameKey_FocusesExisting_DoesNotRecreate()
|
||||||
|
{
|
||||||
|
var wm = new TestWindowManager();
|
||||||
|
var created = 0;
|
||||||
|
|
||||||
|
using var first = wm.OpenOrFocus("CT", () => { created++; return new Form(); });
|
||||||
|
var second = wm.OpenOrFocus("CT", () => { created++; return new Form(); });
|
||||||
|
|
||||||
|
created.Should().Be(1);
|
||||||
|
wm.FocusCount.Should().Be(1);
|
||||||
|
second.Should().BeSameAs(first);
|
||||||
|
wm.OpenCount.Should().Be(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OpenOrFocus_DifferentKeys_TracksSeparately()
|
||||||
|
{
|
||||||
|
var wm = new TestWindowManager();
|
||||||
|
|
||||||
|
using var a = wm.OpenOrFocus("A", () => new Form());
|
||||||
|
using var b = wm.OpenOrFocus("B", () => new Form());
|
||||||
|
|
||||||
|
wm.OpenCount.Should().Be(2);
|
||||||
|
wm.IsOpen("A").Should().BeTrue();
|
||||||
|
wm.IsOpen("B").Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OpenOrFocus_AfterDisposed_RecreatesWindow()
|
||||||
|
{
|
||||||
|
var wm = new TestWindowManager();
|
||||||
|
var created = 0;
|
||||||
|
|
||||||
|
var first = wm.OpenOrFocus("CT", () => { created++; return new Form(); });
|
||||||
|
first.Dispose();
|
||||||
|
|
||||||
|
wm.IsOpen("CT").Should().BeFalse();
|
||||||
|
|
||||||
|
using var second = wm.OpenOrFocus("CT", () => { created++; return new Form(); });
|
||||||
|
|
||||||
|
created.Should().Be(2);
|
||||||
|
second.Should().NotBeSameAs(first);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CloseAll_RemovesAllWindows()
|
||||||
|
{
|
||||||
|
var wm = new TestWindowManager();
|
||||||
|
wm.OpenOrFocus("A", () => new Form());
|
||||||
|
wm.OpenOrFocus("B", () => new Form());
|
||||||
|
|
||||||
|
wm.CloseAll();
|
||||||
|
|
||||||
|
wm.OpenCount.Should().Be(0);
|
||||||
|
wm.IsOpen("A").Should().BeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OpenOrFocus_BlankKey_Throws()
|
||||||
|
{
|
||||||
|
var wm = new TestWindowManager();
|
||||||
|
|
||||||
|
var act = () => wm.OpenOrFocus(" ", () => new Form());
|
||||||
|
|
||||||
|
act.Should().Throw<ArgumentException>();
|
||||||
|
}
|
||||||
|
}
|
||||||
+30
-16
@@ -1,6 +1,6 @@
|
|||||||
namespace IBKRTrader
|
namespace IBKRTrader
|
||||||
{
|
{
|
||||||
partial class Form1
|
partial class LauncherForm
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Required designer variable.
|
/// Required designer variable.
|
||||||
@@ -28,14 +28,15 @@
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private void InitializeComponent()
|
private void InitializeComponent()
|
||||||
{
|
{
|
||||||
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(Form1));
|
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(LauncherForm));
|
||||||
menuStrip1 = new MenuStrip();
|
menuStrip1 = new MenuStrip();
|
||||||
toolStrip1 = new ToolStrip();
|
toolStrip1 = new ToolStrip();
|
||||||
statusStrip1 = new StatusStrip();
|
statusStrip1 = new StatusStrip();
|
||||||
tabControl1 = new TabControl();
|
tabControl1 = new TabControl();
|
||||||
tabPage_dash = new TabPage();
|
tabPage_dash = new TabPage();
|
||||||
tabPage_sett = new TabPage();
|
tabPage_sett = new TabPage();
|
||||||
tabPage_mod_congresstrade = new TabPage();
|
tabPage_modules = new TabPage();
|
||||||
|
flp_modules = new FlowLayoutPanel();
|
||||||
btn_activateTrading = new ToolStripButton();
|
btn_activateTrading = new ToolStripButton();
|
||||||
tabPage_history = new TabPage();
|
tabPage_history = new TabPage();
|
||||||
tabPage_workers = new TabPage();
|
tabPage_workers = new TabPage();
|
||||||
@@ -89,7 +90,7 @@
|
|||||||
tabControl1.Controls.Add(tabPage_sett);
|
tabControl1.Controls.Add(tabPage_sett);
|
||||||
tabControl1.Controls.Add(tabPage_logs);
|
tabControl1.Controls.Add(tabPage_logs);
|
||||||
tabControl1.Controls.Add(tabPage_workers);
|
tabControl1.Controls.Add(tabPage_workers);
|
||||||
tabControl1.Controls.Add(tabPage_mod_congresstrade);
|
tabControl1.Controls.Add(tabPage_modules);
|
||||||
tabControl1.Location = new Point(12, 52);
|
tabControl1.Location = new Point(12, 52);
|
||||||
tabControl1.Name = "tabControl1";
|
tabControl1.Name = "tabControl1";
|
||||||
tabControl1.SelectedIndex = 0;
|
tabControl1.SelectedIndex = 0;
|
||||||
@@ -116,15 +117,27 @@
|
|||||||
tabPage_sett.TabIndex = 1;
|
tabPage_sett.TabIndex = 1;
|
||||||
tabPage_sett.Text = "Settings";
|
tabPage_sett.Text = "Settings";
|
||||||
tabPage_sett.UseVisualStyleBackColor = true;
|
tabPage_sett.UseVisualStyleBackColor = true;
|
||||||
//
|
//
|
||||||
// tabPage_mod_congresstrade
|
// tabPage_modules
|
||||||
//
|
//
|
||||||
tabPage_mod_congresstrade.Location = new Point(4, 34);
|
tabPage_modules.Controls.Add(flp_modules);
|
||||||
tabPage_mod_congresstrade.Name = "tabPage_mod_congresstrade";
|
tabPage_modules.Location = new Point(4, 34);
|
||||||
tabPage_mod_congresstrade.Size = new Size(2421, 1104);
|
tabPage_modules.Name = "tabPage_modules";
|
||||||
tabPage_mod_congresstrade.TabIndex = 2;
|
tabPage_modules.Padding = new Padding(3);
|
||||||
tabPage_mod_congresstrade.Text = "Module: CongressTrading";
|
tabPage_modules.Size = new Size(2421, 1104);
|
||||||
tabPage_mod_congresstrade.UseVisualStyleBackColor = true;
|
tabPage_modules.TabIndex = 2;
|
||||||
|
tabPage_modules.Text = "Module";
|
||||||
|
tabPage_modules.UseVisualStyleBackColor = true;
|
||||||
|
//
|
||||||
|
// flp_modules
|
||||||
|
//
|
||||||
|
flp_modules.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right;
|
||||||
|
flp_modules.AutoScroll = true;
|
||||||
|
flp_modules.Location = new Point(6, 6);
|
||||||
|
flp_modules.Name = "flp_modules";
|
||||||
|
flp_modules.Padding = new Padding(6);
|
||||||
|
flp_modules.Size = new Size(2409, 1092);
|
||||||
|
flp_modules.TabIndex = 0;
|
||||||
//
|
//
|
||||||
// btn_activateTrading
|
// btn_activateTrading
|
||||||
//
|
//
|
||||||
@@ -223,8 +236,8 @@
|
|||||||
Controls.Add(toolStrip1);
|
Controls.Add(toolStrip1);
|
||||||
Controls.Add(menuStrip1);
|
Controls.Add(menuStrip1);
|
||||||
MainMenuStrip = menuStrip1;
|
MainMenuStrip = menuStrip1;
|
||||||
Name = "Form1";
|
Name = "LauncherForm";
|
||||||
Text = "IBKRTrader";
|
Text = "IBKRTrader — Launcher";
|
||||||
toolStrip1.ResumeLayout(false);
|
toolStrip1.ResumeLayout(false);
|
||||||
toolStrip1.PerformLayout();
|
toolStrip1.PerformLayout();
|
||||||
tabControl1.ResumeLayout(false);
|
tabControl1.ResumeLayout(false);
|
||||||
@@ -247,7 +260,8 @@
|
|||||||
private TabControl tabControl1;
|
private TabControl tabControl1;
|
||||||
private TabPage tabPage_dash;
|
private TabPage tabPage_dash;
|
||||||
private TabPage tabPage_sett;
|
private TabPage tabPage_sett;
|
||||||
private TabPage tabPage_mod_congresstrade;
|
private TabPage tabPage_modules;
|
||||||
|
private FlowLayoutPanel flp_modules;
|
||||||
private TabPage tabPage_history;
|
private TabPage tabPage_history;
|
||||||
private TabPage tabPage_workers;
|
private TabPage tabPage_workers;
|
||||||
private DataGridView dgv_workerlist;
|
private DataGridView dgv_workerlist;
|
||||||
+99
-11
@@ -4,27 +4,35 @@ using IBKRTrader.Core.Modules;
|
|||||||
using IBKRTrader.Core.Settings;
|
using IBKRTrader.Core.Settings;
|
||||||
using IBKRTrader.Core.Workers;
|
using IBKRTrader.Core.Workers;
|
||||||
using IBKRTrader.UI;
|
using IBKRTrader.UI;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
|
|
||||||
namespace IBKRTrader;
|
namespace IBKRTrader;
|
||||||
|
|
||||||
public partial class Form1 : Form
|
/// <summary>
|
||||||
|
/// Launcher – das Basis-Fenster der Anwendung.
|
||||||
|
/// Enthält die Core-Panels (Workers, Logs, Settings) und eine Modul-Liste,
|
||||||
|
/// aus der jedes Modul als eigenständiges Fenster geöffnet wird.
|
||||||
|
/// </summary>
|
||||||
|
public partial class LauncherForm : Form
|
||||||
{
|
{
|
||||||
private readonly LoggingService _logger;
|
private readonly LoggingService _logger;
|
||||||
private readonly WorkerEngine _workerEngine;
|
private readonly WorkerEngine _workerEngine;
|
||||||
private readonly SettingsService _settings;
|
private readonly SettingsService _settings;
|
||||||
private readonly CoreMigrations _migrations;
|
private readonly CoreMigrations _migrations;
|
||||||
private readonly IBKRMigrations _ibkrMigrations;
|
private readonly IBKRMigrations _ibkrMigrations;
|
||||||
|
private readonly ModuleRegistry _modules;
|
||||||
|
private readonly WindowManager _windows;
|
||||||
private readonly IServiceProvider _services;
|
private readonly IServiceProvider _services;
|
||||||
|
|
||||||
private LogPanelController? _logPanel;
|
private LogPanelController? _logPanel;
|
||||||
|
|
||||||
public Form1(
|
public LauncherForm(
|
||||||
LoggingService logger,
|
LoggingService logger,
|
||||||
WorkerEngine workerEngine,
|
WorkerEngine workerEngine,
|
||||||
SettingsService settings,
|
SettingsService settings,
|
||||||
CoreMigrations migrations,
|
CoreMigrations migrations,
|
||||||
IBKRMigrations ibkrMigrations,
|
IBKRMigrations ibkrMigrations,
|
||||||
|
ModuleRegistry modules,
|
||||||
|
WindowManager windows,
|
||||||
IServiceProvider services)
|
IServiceProvider services)
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
@@ -34,6 +42,8 @@ public partial class Form1 : Form
|
|||||||
_settings = settings;
|
_settings = settings;
|
||||||
_migrations = migrations;
|
_migrations = migrations;
|
||||||
_ibkrMigrations = ibkrMigrations;
|
_ibkrMigrations = ibkrMigrations;
|
||||||
|
_modules = modules;
|
||||||
|
_windows = windows;
|
||||||
_services = services;
|
_services = services;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,13 +55,15 @@ public partial class Form1 : Form
|
|||||||
InitializeLogPanel();
|
InitializeLogPanel();
|
||||||
InitializeWorkerList();
|
InitializeWorkerList();
|
||||||
InitializeSettingsGrid();
|
InitializeSettingsGrid();
|
||||||
|
InitializeModulePanel();
|
||||||
_ = StartupAsync();
|
_ = StartupAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnFormClosing(FormClosingEventArgs e)
|
protected override void OnFormClosing(FormClosingEventArgs e)
|
||||||
{
|
{
|
||||||
base.OnFormClosing(e);
|
base.OnFormClosing(e);
|
||||||
// Worker sauber beenden (feuert & vergisst – Form muss kurz warten)
|
// Offene Modul-Fenster schließen, dann Worker sauber beenden.
|
||||||
|
_windows.CloseAll();
|
||||||
_workerEngine.StopAllAsync().GetAwaiter().GetResult();
|
_workerEngine.StopAllAsync().GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,6 +91,83 @@ public partial class Form1 : Form
|
|||||||
pg_settings.SelectedObject = _settings.Settings;
|
pg_settings.SelectedObject = _settings.Settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Baut je Modul eine Karte mit „Fenster öffnen"-Button.</summary>
|
||||||
|
private void InitializeModulePanel()
|
||||||
|
{
|
||||||
|
flp_modules.FlowDirection = FlowDirection.LeftToRight;
|
||||||
|
flp_modules.WrapContents = true;
|
||||||
|
flp_modules.Controls.Clear();
|
||||||
|
|
||||||
|
foreach (var module in _modules.Modules)
|
||||||
|
flp_modules.Controls.Add(BuildModuleCard(module));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Control BuildModuleCard(IModule module)
|
||||||
|
{
|
||||||
|
var card = new Panel
|
||||||
|
{
|
||||||
|
Width = 320,
|
||||||
|
Height = 150,
|
||||||
|
Margin = new Padding(8),
|
||||||
|
BorderStyle = BorderStyle.FixedSingle
|
||||||
|
};
|
||||||
|
|
||||||
|
var title = new Label
|
||||||
|
{
|
||||||
|
Text = $"{module.DisplayName} [{module.Key}]",
|
||||||
|
Font = new Font(Font.FontFamily, 11f, FontStyle.Bold),
|
||||||
|
Location = new Point(10, 10),
|
||||||
|
AutoSize = true
|
||||||
|
};
|
||||||
|
|
||||||
|
var desc = new Label
|
||||||
|
{
|
||||||
|
Text = module.Description,
|
||||||
|
Location = new Point(10, 42),
|
||||||
|
Size = new Size(300, 60),
|
||||||
|
AutoEllipsis = true
|
||||||
|
};
|
||||||
|
|
||||||
|
var open = new Button
|
||||||
|
{
|
||||||
|
Text = "Fenster öffnen",
|
||||||
|
Location = new Point(10, 108),
|
||||||
|
Width = 140,
|
||||||
|
Tag = module
|
||||||
|
};
|
||||||
|
open.Click += (_, _) => OpenModuleWindow(module);
|
||||||
|
|
||||||
|
var version = new Label
|
||||||
|
{
|
||||||
|
Text = $"v{module.Version}",
|
||||||
|
Location = new Point(240, 113),
|
||||||
|
AutoSize = true,
|
||||||
|
ForeColor = SystemColors.GrayText
|
||||||
|
};
|
||||||
|
|
||||||
|
card.Controls.Add(title);
|
||||||
|
card.Controls.Add(desc);
|
||||||
|
card.Controls.Add(open);
|
||||||
|
card.Controls.Add(version);
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Öffnet (oder fokussiert) das eigenständige Fenster eines Moduls.</summary>
|
||||||
|
private void OpenModuleWindow(IModule module)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_windows.OpenOrFocus(module.Key, () => module.CreateWindow(_services));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Error(module.Key, $"{module.DisplayName}: Fenster konnte nicht geöffnet werden.", ex);
|
||||||
|
MessageBox.Show(this,
|
||||||
|
$"Fenster für '{module.DisplayName}' konnte nicht geöffnet werden:\n{ex.Message}",
|
||||||
|
"Modul-Fenster", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Async Startup ────────────────────────────────────────────────────────
|
// ─── Async Startup ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private async Task StartupAsync()
|
private async Task StartupAsync()
|
||||||
@@ -99,8 +188,7 @@ public partial class Form1 : Form
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Modul-Migrationen: über die Registry iterieren (Core kennt kein Modul).
|
// Modul-Migrationen: über die Registry iterieren (Core kennt kein Modul).
|
||||||
var registry = _services.GetRequiredService<ModuleRegistry>();
|
foreach (var module in _modules.Modules)
|
||||||
foreach (var module in registry.Modules)
|
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
using System.Drawing;
|
using IBKRTrader.Core.Logging;
|
||||||
using IBKRTrader.Core.Modules;
|
using IBKRTrader.Core.Modules;
|
||||||
using IBKRTrader.Core.Workers;
|
using IBKRTrader.Core.Workers;
|
||||||
using IBKRTrader.Modules.CongressTrading.Database;
|
using IBKRTrader.Modules.CongressTrading.Database;
|
||||||
using IBKRTrader.Modules.CongressTrading.Scraper;
|
using IBKRTrader.Modules.CongressTrading.Scraper;
|
||||||
|
using IBKRTrader.Modules.CongressTrading.UI;
|
||||||
using IBKRTrader.Modules.CongressTrading.Workers;
|
using IBKRTrader.Modules.CongressTrading.Workers;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
@@ -58,24 +59,10 @@ public sealed class CongressTradingModule : IModule
|
|||||||
provider.GetRequiredService<CongressScrapeWorker>()
|
provider.GetRequiredService<CongressScrapeWorker>()
|
||||||
];
|
];
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>Erzeugt das eigenständige Modul-Fenster.</summary>
|
||||||
/// Erzeugt das Modul-Fenster. Platzhalter – die vollständige UI folgt in Phase 2.
|
|
||||||
/// </summary>
|
|
||||||
public Form CreateWindow(IServiceProvider provider)
|
public Form CreateWindow(IServiceProvider provider)
|
||||||
{
|
=> new CongressTradingForm(
|
||||||
var form = new Form
|
provider.GetRequiredService<CongressRepository>(),
|
||||||
{
|
provider.GetRequiredService<WorkerEngine>(),
|
||||||
Text = $"{DisplayName} ({Key})",
|
provider.GetRequiredService<LoggingService>());
|
||||||
Width = 800,
|
|
||||||
Height = 500,
|
|
||||||
StartPosition = FormStartPosition.CenterParent
|
|
||||||
};
|
|
||||||
form.Controls.Add(new Label
|
|
||||||
{
|
|
||||||
Dock = DockStyle.Fill,
|
|
||||||
TextAlign = ContentAlignment.MiddleCenter,
|
|
||||||
Text = $"{DisplayName}\n\nModul-UI folgt in Phase 2."
|
|
||||||
});
|
|
||||||
return form;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
using IBKRTrader.Core.Logging;
|
||||||
|
using IBKRTrader.Core.Workers;
|
||||||
|
using IBKRTrader.Modules.CongressTrading.Database;
|
||||||
|
using IBKRTrader.UI;
|
||||||
|
|
||||||
|
namespace IBKRTrader.Modules.CongressTrading.UI;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Eigenständiges Fenster des CongressTrading-Moduls.
|
||||||
|
/// Phase 2: Grundgerüst mit DB-Kennzahlen und manuellem Scrape-Trigger.
|
||||||
|
/// Die vollständige Trade-/Positions-Ansicht folgt in Phase 4.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class CongressTradingForm : ModuleFormBase
|
||||||
|
{
|
||||||
|
private const string ScrapeWorkerName = "CT-ScrapeWorker";
|
||||||
|
|
||||||
|
private readonly CongressRepository _repo;
|
||||||
|
private readonly WorkerEngine _engine;
|
||||||
|
private readonly LoggingService _logger;
|
||||||
|
|
||||||
|
private readonly Label _lblTrades = new() { AutoSize = true, Location = new Point(20, 70) };
|
||||||
|
private readonly Label _lblMembers = new() { AutoSize = true, Location = new Point(20, 100) };
|
||||||
|
private readonly Button _btnRefresh = new() { Text = "Aktualisieren", Location = new Point(20, 140), Width = 140 };
|
||||||
|
private readonly Button _btnScrape = new() { Text = "Scrape jetzt", Location = new Point(170, 140), Width = 140 };
|
||||||
|
private readonly Label _lblStatus = new() { AutoSize = true, Location = new Point(20, 185), ForeColor = SystemColors.GrayText };
|
||||||
|
|
||||||
|
public CongressTradingForm(CongressRepository repo, WorkerEngine engine, LoggingService logger)
|
||||||
|
: base(CongressTradingModule.ModuleKey)
|
||||||
|
{
|
||||||
|
_repo = repo;
|
||||||
|
_engine = engine;
|
||||||
|
_logger = logger;
|
||||||
|
|
||||||
|
Text = "Congress Trading [CT]";
|
||||||
|
BuildLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildLayout()
|
||||||
|
{
|
||||||
|
var title = new Label
|
||||||
|
{
|
||||||
|
Text = "Congress Trading",
|
||||||
|
Font = new Font(Font.FontFamily, 14f, FontStyle.Bold),
|
||||||
|
Location = new Point(18, 20),
|
||||||
|
AutoSize = true
|
||||||
|
};
|
||||||
|
|
||||||
|
var hint = new Label
|
||||||
|
{
|
||||||
|
Text = "Vollständige Trade- und Positions-Ansicht folgt in Phase 4.",
|
||||||
|
Location = new Point(20, 230),
|
||||||
|
AutoSize = true,
|
||||||
|
ForeColor = SystemColors.GrayText
|
||||||
|
};
|
||||||
|
|
||||||
|
_btnRefresh.Click += async (_, _) => await RefreshStatsAsync();
|
||||||
|
_btnScrape.Click += async (_, _) => await TriggerScrapeAsync();
|
||||||
|
|
||||||
|
Controls.Add(title);
|
||||||
|
Controls.Add(_lblTrades);
|
||||||
|
Controls.Add(_lblMembers);
|
||||||
|
Controls.Add(_btnRefresh);
|
||||||
|
Controls.Add(_btnScrape);
|
||||||
|
Controls.Add(_lblStatus);
|
||||||
|
Controls.Add(hint);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async void OnShown(EventArgs e)
|
||||||
|
{
|
||||||
|
base.OnShown(e);
|
||||||
|
await RefreshStatsAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RefreshStatsAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var trades = await _repo.GetTradeCountAsync();
|
||||||
|
var members = await _repo.GetMemberCountAsync();
|
||||||
|
_lblTrades.Text = $"Trades in DB: {trades:N0}";
|
||||||
|
_lblMembers.Text = $"Mitglieder in DB: {members:N0}";
|
||||||
|
_lblStatus.Text = $"Aktualisiert: {DateTime.Now:HH:mm:ss}";
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_lblTrades.Text = "Trades in DB: n/v";
|
||||||
|
_lblMembers.Text = "Mitglieder in DB: n/v";
|
||||||
|
_lblStatus.Text = $"DB nicht erreichbar: {ex.Message}";
|
||||||
|
_logger.Warn(CongressTradingModule.ModuleKey, $"Kennzahlen konnten nicht geladen werden: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task TriggerScrapeAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_lblStatus.Text = "Scrape angestoßen...";
|
||||||
|
await _engine.TriggerWorkerAsync(ScrapeWorkerName);
|
||||||
|
_logger.Info(CongressTradingModule.ModuleKey, "Scrape-Worker manuell ausgelöst (aus Modul-Fenster).");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_lblStatus.Text = $"Scrape fehlgeschlagen: {ex.Message}";
|
||||||
|
_logger.Error(CongressTradingModule.ModuleKey, "Manueller Scrape-Trigger fehlgeschlagen.", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
-2
@@ -10,6 +10,7 @@ using IBKRTrader.Core.Trading;
|
|||||||
using IBKRTrader.Core.Workers;
|
using IBKRTrader.Core.Workers;
|
||||||
using IBKRTrader.Core.Workers.BuiltIn;
|
using IBKRTrader.Core.Workers.BuiltIn;
|
||||||
using IBKRTrader.Modules.CongressTrading;
|
using IBKRTrader.Modules.CongressTrading;
|
||||||
|
using IBKRTrader.UI;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace IBKRTrader;
|
namespace IBKRTrader;
|
||||||
@@ -84,7 +85,10 @@ internal static class Program
|
|||||||
.ToArray());
|
.ToArray());
|
||||||
|
|
||||||
services.AddSingleton<WorkerEngine>();
|
services.AddSingleton<WorkerEngine>();
|
||||||
services.AddSingleton<Form1>();
|
|
||||||
|
// UI: Launcher + Fenster-Verwaltung
|
||||||
|
services.AddSingleton<WindowManager>();
|
||||||
|
services.AddSingleton<LauncherForm>();
|
||||||
|
|
||||||
var provider = services.BuildServiceProvider();
|
var provider = services.BuildServiceProvider();
|
||||||
|
|
||||||
@@ -93,6 +97,6 @@ internal static class Program
|
|||||||
var engine = provider.GetRequiredService<WorkerEngine>();
|
var engine = provider.GetRequiredService<WorkerEngine>();
|
||||||
webApi.SetEngine(engine);
|
webApi.SetEngine(engine);
|
||||||
|
|
||||||
Application.Run(provider.GetRequiredService<Form1>());
|
Application.Run(provider.GetRequiredService<LauncherForm>());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
namespace IBKRTrader.UI;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Basisklasse für eigenständige Modul-Fenster.
|
||||||
|
/// Stellt gemeinsame Grundeinstellungen (Größe, Startposition) bereit und
|
||||||
|
/// hält den Modul-Key, damit der Launcher/WindowManager das Fenster zuordnen kann.
|
||||||
|
/// </summary>
|
||||||
|
public class ModuleFormBase : Form
|
||||||
|
{
|
||||||
|
/// <summary>Kürzel des zugehörigen Moduls (z. B. "CT").</summary>
|
||||||
|
public string ModuleKey { get; }
|
||||||
|
|
||||||
|
// Parameterloser Ctor nur für den WinForms-Designer.
|
||||||
|
protected ModuleFormBase() : this("") { }
|
||||||
|
|
||||||
|
protected ModuleFormBase(string moduleKey)
|
||||||
|
{
|
||||||
|
ModuleKey = moduleKey;
|
||||||
|
Width = 900;
|
||||||
|
Height = 600;
|
||||||
|
StartPosition = FormStartPosition.CenterScreen;
|
||||||
|
ShowInTaskbar = true;
|
||||||
|
MinimumSize = new Size(600, 400);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
namespace IBKRTrader.UI;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verwaltet die eigenständigen Modul-Fenster des Launchers.
|
||||||
|
/// Pro Modul-Key wird höchstens ein Fenster gehalten: erneutes Öffnen
|
||||||
|
/// fokussiert das bestehende Fenster statt ein zweites zu erzeugen.
|
||||||
|
///
|
||||||
|
/// Die tatsächlichen UI-Aktionen (Show/Focus) sind in überschreibbare
|
||||||
|
/// Methoden gekapselt, damit die Tracking-Logik ohne Message-Loop testbar ist.
|
||||||
|
/// </summary>
|
||||||
|
public class WindowManager
|
||||||
|
{
|
||||||
|
private readonly Dictionary<string, Form> _open = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
/// <summary>Öffnet das Fenster zum Key oder fokussiert ein bereits offenes.</summary>
|
||||||
|
public Form OpenOrFocus(string key, Func<Form> factory)
|
||||||
|
{
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(key);
|
||||||
|
ArgumentNullException.ThrowIfNull(factory);
|
||||||
|
|
||||||
|
if (_open.TryGetValue(key, out var existing))
|
||||||
|
{
|
||||||
|
if (!existing.IsDisposed)
|
||||||
|
{
|
||||||
|
Focus(existing);
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
_open.Remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
var form = factory();
|
||||||
|
_open[key] = form;
|
||||||
|
form.FormClosed += OnFormClosed;
|
||||||
|
Present(form);
|
||||||
|
return form;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>True, wenn zum Key aktuell ein nicht-disposetes Fenster offen ist.</summary>
|
||||||
|
public bool IsOpen(string key)
|
||||||
|
=> _open.TryGetValue(key, out var f) && !f.IsDisposed;
|
||||||
|
|
||||||
|
/// <summary>Anzahl aktuell offener Fenster.</summary>
|
||||||
|
public int OpenCount => _open.Values.Count(f => !f.IsDisposed);
|
||||||
|
|
||||||
|
/// <summary>Schließt alle offenen Fenster (z. B. beim Beenden des Launchers).</summary>
|
||||||
|
public void CloseAll()
|
||||||
|
{
|
||||||
|
foreach (var f in _open.Values.ToList())
|
||||||
|
{
|
||||||
|
if (!f.IsDisposed)
|
||||||
|
{
|
||||||
|
f.FormClosed -= OnFormClosed;
|
||||||
|
f.Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_open.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Überschreibbare UI-Aktionen (für Tests) ──────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>Zeigt ein neu erzeugtes Fenster an.</summary>
|
||||||
|
protected virtual void Present(Form form) => form.Show();
|
||||||
|
|
||||||
|
/// <summary>Bringt ein bestehendes Fenster in den Vordergrund.</summary>
|
||||||
|
protected virtual void Focus(Form form)
|
||||||
|
{
|
||||||
|
if (form.WindowState == FormWindowState.Minimized)
|
||||||
|
form.WindowState = FormWindowState.Normal;
|
||||||
|
form.Activate();
|
||||||
|
form.BringToFront();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnFormClosed(object? sender, FormClosedEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is not Form f) return;
|
||||||
|
f.FormClosed -= OnFormClosed;
|
||||||
|
|
||||||
|
var entry = _open.FirstOrDefault(p => ReferenceEquals(p.Value, f));
|
||||||
|
if (entry.Key is not null)
|
||||||
|
_open.Remove(entry.Key);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -109,12 +109,13 @@ WinForms selbst wird **nicht** unit-getestet – Logik in Services/Manager halte
|
|||||||
- [x] Testbarkeits-Seams: `WorkerBase.BeginRunLogAsync/EndRunLogAsync`, `CapitolTradesScraper.ParseTradesFromHtml`
|
- [x] Testbarkeits-Seams: `WorkerBase.BeginRunLogAsync/EndRunLogAsync`, `CapitolTradesScraper.ParseTradesFromHtml`
|
||||||
- [x] Tests: `ModuleRegistry`, `CongressTradingModule`, `WorkerBase`, `CapitolTradesScraper` (gegen `ct_raw.html`) → **15/15 grün**
|
- [x] Tests: `ModuleRegistry`, `CongressTradingModule`, `WorkerBase`, `CapitolTradesScraper` (gegen `ct_raw.html`) → **15/15 grün**
|
||||||
|
|
||||||
### Phase 2 – Launcher-UI + Modul-Fenster
|
### Phase 2 – Launcher-UI + Modul-Fenster ✅
|
||||||
- [ ] `Form1` → `LauncherForm`; Kern-Panels behalten (Workers, Logs, Settings, Core-Status)
|
- [x] `Form1` → `LauncherForm` (Dateien via `git mv`, Designer/resx angepasst); Kern-Panels behalten
|
||||||
- [ ] Modul-Panel: Liste aus `ModuleRegistry`, je Modul „Fenster öffnen"
|
- [x] Modul-Tab: Karten aus `ModuleRegistry`, je Modul „Fenster öffnen"
|
||||||
- [ ] `UI/ModuleFormBase.cs` + `UI/WindowManager.cs` (Fenster-Tracking)
|
- [x] `UI/ModuleFormBase.cs` + `UI/WindowManager.cs` (Fenster-Tracking, Re-Open fokussiert, `CloseAll`)
|
||||||
- [ ] `Modules/CongressTrading/UI/CongressTradingForm.cs` (erstes Modul-Fenster)
|
- [x] `Modules/CongressTrading/UI/CongressTradingForm.cs` (DB-Kennzahlen + „Scrape jetzt")
|
||||||
- [ ] Tests: `WindowManager`
|
- [x] `WindowManager` in DI; Launcher schließt Modul-Fenster beim Beenden
|
||||||
|
- [x] Tests: `WindowManager` (6) → **21/21 grün**; Launcher-Start verifiziert
|
||||||
|
|
||||||
### Phase 3 – Trading-Kern (Core)
|
### Phase 3 – Trading-Kern (Core)
|
||||||
- [ ] `Core/Trading/IIbkrClient.cs` (+ Adapter auf `IBKRGatewayService`)
|
- [ ] `Core/Trading/IIbkrClient.cs` (+ Adapter auf `IBKRGatewayService`)
|
||||||
|
|||||||
Reference in New Issue
Block a user