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> @
156 lines
5.3 KiB
C#
156 lines
5.3 KiB
C#
using IBKRTrader.Core.Database.Migrations;
|
||
using IBKRTrader.Core.Logging;
|
||
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 (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 sealed class LauncherForm : Form
|
||
{
|
||
private readonly ShellUiHost _uiHost;
|
||
private readonly IServiceProvider _services;
|
||
private readonly LoggingService _logger;
|
||
private readonly WorkerEngine _workerEngine;
|
||
private readonly IReadOnlyList<IModule> _modules;
|
||
|
||
private readonly Dictionary<string, Button> _viewButtons = new(StringComparer.OrdinalIgnoreCase);
|
||
private readonly FlowLayoutPanel _buttonPanel = new()
|
||
{
|
||
Dock = DockStyle.Fill, Padding = new Padding(16), AutoScroll = true
|
||
};
|
||
private readonly ToolStripStatusLabel _status = new("Start...");
|
||
|
||
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;
|
||
}
|
||
|
||
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);
|
||
_ = StartupAsync();
|
||
}
|
||
|
||
protected override void OnFormClosing(FormClosingEventArgs e)
|
||
{
|
||
// Auch das Schließen-X läuft über die Sicherheitsabfrage.
|
||
if (!_uiHost.ShutdownConfirmed)
|
||
{
|
||
e.Cancel = true;
|
||
BeginInvoke((Action)(() => _uiHost.RequestShutdown()));
|
||
return;
|
||
}
|
||
|
||
_uiHost.CloseAllViews();
|
||
try
|
||
{
|
||
foreach (var module in _modules)
|
||
module.StopAsync(default).GetAwaiter().GetResult();
|
||
}
|
||
catch { /* Shutdown darf nicht am Modul scheitern */ }
|
||
_workerEngine.StopAllAsync().GetAwaiter().GetResult();
|
||
|
||
base.OnFormClosing(e);
|
||
}
|
||
|
||
private async Task StartupAsync()
|
||
{
|
||
_logger.Info("Core", "=== IBKRTrader startet ===");
|
||
_logger.Info("Core", $"Version: 1.0.0 | .NET {Environment.Version}");
|
||
|
||
// 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
|
||
{
|
||
await _services.GetRequiredService<CoreMigrations>().RunAsync();
|
||
await _services.GetRequiredService<IBKRMigrations>().RunAsync();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.Error("Core", "Core-Datenbankfehler beim Start.", ex);
|
||
}
|
||
|
||
foreach (var module in _modules)
|
||
{
|
||
try { await module.StartAsync(default); }
|
||
catch (Exception ex) { _logger.Error(module.Name, $"{module.Name}: Start fehlgeschlagen.", ex); }
|
||
}
|
||
|
||
await _workerEngine.StartAllAsync();
|
||
|
||
_logger.Info("Core", "IBKRTrader bereit.");
|
||
SetStatus("Bereit");
|
||
UpdateButtonStates();
|
||
}
|
||
|
||
private void UpdateButtonStates()
|
||
{
|
||
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}";
|
||
}
|
||
}
|