@
R2: Generic Host + Modul-Vertrag + Shell-UI (PolytraderSharp-Konzept) - Core/Modularity: neuer IModule (Name, DbPrefix, RegisterServices(services,config), RegisterUi(host,sp), StartAsync/StopAsync, GetActivationBlocker) + ModuleView + IModuleUiHost + WindowMenu. Alte IModule/ModuleRegistry/WindowManager/ModuleFormBase entfernt. - Program.cs: Host.CreateDefaultBuilder + IConfiguration (appsettings.json/.Local.json); Core-Services registriert, Module via RegisterServices, Views via RegisterUi. - UI: ShellUiHost (Einzelinstanz-Fenster + Fenster-Menue), LauncherForm als Shell (Buttons je View), Core-Views Logs/Settings/Workers als eigene Fenster. - CongressTrading auf neuen Vertrag; Worker als IWorker registriert. - --smoke-ui Headless-Test (konstruiert jede View + Launcher). - appsettings.Local.json gitignored. - Tests angepasst -> 30/30 gruen; Build + Smoke-UI + App-Start verifiziert. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> @
This commit is contained in:
+99
-168
@@ -1,224 +1,155 @@
|
||||
using IBKRTrader.Core.Database.Migrations;
|
||||
using IBKRTrader.Core.Logging;
|
||||
using IBKRTrader.Core.Modules;
|
||||
using IBKRTrader.Core.Modularity;
|
||||
using IBKRTrader.Core.Settings;
|
||||
using IBKRTrader.Core.Workers;
|
||||
using IBKRTrader.UI;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace IBKRTrader;
|
||||
|
||||
/// <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.
|
||||
/// Launcher – das Basis-Fenster (Shell). Zeigt je registrierter View einen Button, führt beim Start
|
||||
/// Migrationen aus, startet Module und die WorkerEngine, und trägt das gemeinsame Fenster-Menü.
|
||||
/// Die inhaltlichen Ansichten (Logs, Settings, Workers, Module) sind eigenständige Fenster.
|
||||
/// </summary>
|
||||
public partial class LauncherForm : Form
|
||||
public sealed class LauncherForm : Form
|
||||
{
|
||||
private readonly LoggingService _logger;
|
||||
private readonly WorkerEngine _workerEngine;
|
||||
private readonly SettingsService _settings;
|
||||
private readonly CoreMigrations _migrations;
|
||||
private readonly IBKRMigrations _ibkrMigrations;
|
||||
private readonly ModuleRegistry _modules;
|
||||
private readonly WindowManager _windows;
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly ShellUiHost _uiHost;
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly LoggingService _logger;
|
||||
private readonly WorkerEngine _workerEngine;
|
||||
private readonly IReadOnlyList<IModule> _modules;
|
||||
|
||||
private LogPanelController? _logPanel;
|
||||
|
||||
public LauncherForm(
|
||||
LoggingService logger,
|
||||
WorkerEngine workerEngine,
|
||||
SettingsService settings,
|
||||
CoreMigrations migrations,
|
||||
IBKRMigrations ibkrMigrations,
|
||||
ModuleRegistry modules,
|
||||
WindowManager windows,
|
||||
IServiceProvider services)
|
||||
private readonly Dictionary<string, Button> _viewButtons = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly FlowLayoutPanel _buttonPanel = new()
|
||||
{
|
||||
InitializeComponent();
|
||||
Dock = DockStyle.Fill, Padding = new Padding(16), AutoScroll = true
|
||||
};
|
||||
private readonly ToolStripStatusLabel _status = new("Start...");
|
||||
|
||||
_logger = logger;
|
||||
_workerEngine = workerEngine;
|
||||
_settings = settings;
|
||||
_migrations = migrations;
|
||||
_ibkrMigrations = ibkrMigrations;
|
||||
_modules = modules;
|
||||
_windows = windows;
|
||||
_services = services;
|
||||
public LauncherForm(ShellUiHost uiHost, IServiceProvider services)
|
||||
{
|
||||
_uiHost = uiHost;
|
||||
_services = services;
|
||||
_logger = services.GetRequiredService<LoggingService>();
|
||||
_workerEngine = services.GetRequiredService<WorkerEngine>();
|
||||
_modules = services.GetServices<IModule>().ToList();
|
||||
|
||||
Text = "IBKRTrader — Launcher";
|
||||
Width = 720;
|
||||
Height = 540;
|
||||
StartPosition = FormStartPosition.CenterScreen;
|
||||
|
||||
_uiHost.SetMainWindow(this);
|
||||
|
||||
BuildUi();
|
||||
_uiHost.OpenStateChanged += UpdateButtonStates;
|
||||
}
|
||||
|
||||
// ─── Form-Events ──────────────────────────────────────────────────────────
|
||||
private void BuildUi()
|
||||
{
|
||||
var menu = new MenuStrip { Dock = DockStyle.Top, ImageScalingSize = new Size(24, 24) };
|
||||
WindowMenu.Wire(menu, _uiHost, null);
|
||||
|
||||
foreach (var view in _uiHost.Views.OrderBy(v => v.Order).ThenBy(v => v.Title))
|
||||
{
|
||||
var id = view.Id;
|
||||
var btn = new Button
|
||||
{
|
||||
Text = view.Title, Width = 210, Height = 48, Margin = new Padding(8),
|
||||
TextAlign = ContentAlignment.MiddleLeft, Image = view.Icon,
|
||||
ImageAlign = ContentAlignment.MiddleLeft, TextImageRelation = TextImageRelation.ImageBeforeText
|
||||
};
|
||||
btn.Click += (_, _) => _uiHost.OpenView(id);
|
||||
_viewButtons[id] = btn;
|
||||
_buttonPanel.Controls.Add(btn);
|
||||
}
|
||||
|
||||
var statusStrip = new StatusStrip();
|
||||
statusStrip.Items.Add(_status);
|
||||
|
||||
// Reihenfolge: Fill-Panel zuerst, dann Bottom, dann Top (WinForms Dock-Stacking).
|
||||
Controls.Add(_buttonPanel);
|
||||
Controls.Add(statusStrip);
|
||||
Controls.Add(menu);
|
||||
MainMenuStrip = menu;
|
||||
}
|
||||
|
||||
protected override void OnLoad(EventArgs e)
|
||||
{
|
||||
base.OnLoad(e);
|
||||
InitializeLogPanel();
|
||||
InitializeWorkerList();
|
||||
InitializeSettingsGrid();
|
||||
InitializeModulePanel();
|
||||
_ = StartupAsync();
|
||||
}
|
||||
|
||||
protected override void OnFormClosing(FormClosingEventArgs e)
|
||||
{
|
||||
base.OnFormClosing(e);
|
||||
// Offene Modul-Fenster schließen, dann Worker sauber beenden.
|
||||
_windows.CloseAll();
|
||||
_workerEngine.StopAllAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
// ─── Initialisierung ──────────────────────────────────────────────────────
|
||||
|
||||
private void InitializeLogPanel()
|
||||
{
|
||||
// RichTextBox auf Dock.Fill setzen
|
||||
rtb_logs.Dock = DockStyle.Fill;
|
||||
_logPanel = new LogPanelController(rtb_logs, _logger);
|
||||
|
||||
// LogLevel aus Settings setzen
|
||||
var levelStr = _settings.Settings.Logging.Level;
|
||||
if (Enum.TryParse<AppLogLevel>(levelStr, true, out var level))
|
||||
_logger.SetMinLevel(level);
|
||||
}
|
||||
|
||||
private void InitializeWorkerList()
|
||||
{
|
||||
WorkerListBindingSource.Setup(dgv_workerlist, _workerEngine.WorkerInfos);
|
||||
}
|
||||
|
||||
private void InitializeSettingsGrid()
|
||||
{
|
||||
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
|
||||
// Auch das Schließen-X läuft über die Sicherheitsabfrage.
|
||||
if (!_uiHost.ShutdownConfirmed)
|
||||
{
|
||||
Width = 320,
|
||||
Height = 150,
|
||||
Margin = new Padding(8),
|
||||
BorderStyle = BorderStyle.FixedSingle
|
||||
};
|
||||
e.Cancel = true;
|
||||
BeginInvoke((Action)(() => _uiHost.RequestShutdown()));
|
||||
return;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
_uiHost.CloseAllViews();
|
||||
try
|
||||
{
|
||||
_windows.OpenOrFocus(module.Key, () => module.CreateWindow(_services));
|
||||
foreach (var module in _modules)
|
||||
module.StopAsync(default).GetAwaiter().GetResult();
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
catch { /* Shutdown darf nicht am Modul scheitern */ }
|
||||
_workerEngine.StopAllAsync().GetAwaiter().GetResult();
|
||||
|
||||
// ─── Async Startup ────────────────────────────────────────────────────────
|
||||
base.OnFormClosing(e);
|
||||
}
|
||||
|
||||
private async Task StartupAsync()
|
||||
{
|
||||
_logger.Info("Core", "=== IBKRTrader startet ===");
|
||||
_logger.Info("Core", $"Version: 1.0.0 | .NET {Environment.Version}");
|
||||
|
||||
// DB-Verbindung testen und Core-Migrationen laufen lassen
|
||||
// Log-Level aus Settings.
|
||||
var levelStr = _services.GetRequiredService<SettingsService>().Settings.Logging.Level;
|
||||
if (Enum.TryParse<AppLogLevel>(levelStr, true, out var level))
|
||||
_logger.SetMinLevel(level);
|
||||
|
||||
SetStatus("Migrationen...");
|
||||
try
|
||||
{
|
||||
_logger.Info("Core", "Verbinde mit Datenbank...");
|
||||
await _migrations.RunAsync();
|
||||
await _ibkrMigrations.RunAsync();
|
||||
await _services.GetRequiredService<CoreMigrations>().RunAsync();
|
||||
await _services.GetRequiredService<IBKRMigrations>().RunAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error("Core", "Core-Datenbankfehler beim Start.", ex);
|
||||
}
|
||||
|
||||
// Modul-Migrationen: über die Registry iterieren (Core kennt kein Modul).
|
||||
foreach (var module in _modules.Modules)
|
||||
foreach (var module in _modules)
|
||||
{
|
||||
try
|
||||
{
|
||||
await module.InitializeAsync(_services);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(module.Key, $"{module.DisplayName}: Initialisierung fehlgeschlagen.", ex);
|
||||
}
|
||||
try { await module.StartAsync(default); }
|
||||
catch (Exception ex) { _logger.Error(module.Name, $"{module.Name}: Start fehlgeschlagen.", ex); }
|
||||
}
|
||||
|
||||
// Worker starten
|
||||
await _workerEngine.StartAllAsync();
|
||||
|
||||
_logger.Info("Core", "IBKRTrader bereit.");
|
||||
UpdateStatusBar("Bereit");
|
||||
SetStatus("Bereit");
|
||||
UpdateButtonStates();
|
||||
}
|
||||
|
||||
// ─── StatusBar ────────────────────────────────────────────────────────────
|
||||
|
||||
private void UpdateStatusBar(string text)
|
||||
private void UpdateButtonStates()
|
||||
{
|
||||
if (statusStrip1.InvokeRequired)
|
||||
statusStrip1.BeginInvoke(() => UpdateStatusBar(text));
|
||||
else
|
||||
{
|
||||
// Vorhandenes Label nutzen oder neues anlegen
|
||||
if (statusStrip1.Items.Count == 0)
|
||||
statusStrip1.Items.Add(new ToolStripStatusLabel());
|
||||
statusStrip1.Items[0].Text = $"Status: {text} | {DateTime.Now:HH:mm:ss}";
|
||||
}
|
||||
if (IsDisposed) return;
|
||||
if (InvokeRequired) { BeginInvoke((Action)UpdateButtonStates); return; }
|
||||
foreach (var (id, btn) in _viewButtons)
|
||||
btn.Font = new Font(btn.Font, _uiHost.IsOpen(id) ? FontStyle.Bold : FontStyle.Regular);
|
||||
}
|
||||
|
||||
private void SetStatus(string text)
|
||||
{
|
||||
if (IsDisposed) return;
|
||||
if (InvokeRequired) { BeginInvoke((Action)(() => SetStatus(text))); return; }
|
||||
_status.Text = $"Status: {text} | {DateTime.Now:HH:mm:ss}";
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user