@
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:
@@ -14,6 +14,7 @@
|
||||
<PackageReference Include="MySqlConnector" Version="2.4.0" />
|
||||
<PackageReference Include="Dapper" Version="2.1.35" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.4" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Erlaubt dem Testprojekt, interne Helfer zu testen. -->
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace IBKRTrader.Core.Modularity;
|
||||
|
||||
/// <summary>
|
||||
/// Vertrag für ein IBKRTrader-Modul. Module referenzieren nur den Core, niemals einander.
|
||||
/// Ein Modul kapselt einen eigenständigen Funktionsbereich (z. B. eine Trading-Strategie)
|
||||
/// mit eigenen Services, Jobs, DB-Objekten sowie eigenen UI-Ansichten.
|
||||
///
|
||||
/// Vorbild: PolytraderSharp <c>IPolyTraderModule</c>.
|
||||
/// </summary>
|
||||
public interface IModule
|
||||
{
|
||||
/// <summary>Eindeutiger Anzeigename des Moduls, z. B. "CongressTrading".</summary>
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>DB-Präfix für die Objekte des Moduls (z. B. "ct_"), damit sich Module nicht überschneiden.</summary>
|
||||
string DbPrefix { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Registriert Services/Jobs des Moduls im DI-Container. Wird beim App-Start aufgerufen,
|
||||
/// bevor der Host gebaut wird.
|
||||
/// </summary>
|
||||
void RegisterServices(IServiceCollection services, IConfiguration configuration);
|
||||
|
||||
/// <summary>
|
||||
/// Registriert die UI-Ansichten des Moduls bei der Shell (Launcher). Die Shell öffnet jede Ansicht
|
||||
/// auf Wunsch in einem eigenen Fenster; Module liefern eigene Forms, die Shell bleibt modul-agnostisch.
|
||||
/// </summary>
|
||||
void RegisterUi(IModuleUiHost host, IServiceProvider services);
|
||||
|
||||
/// <summary>
|
||||
/// Optionale Vorab-Prüfung, ob das Modul aktiviert werden kann. Gibt <c>null</c> zurück, wenn
|
||||
/// aktivierbar, sonst einen kurzen Grund (z. B. „IBKR-Gateway nicht konfiguriert").
|
||||
/// </summary>
|
||||
string? GetActivationBlocker(IConfiguration configuration) => null;
|
||||
|
||||
/// <summary>Wird nach der Core-Initialisierung gestartet (kein Anlaufen gegen leeren State).</summary>
|
||||
Task StartAsync(CancellationToken cancellationToken);
|
||||
|
||||
Task StopAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace IBKRTrader.Core.Modularity;
|
||||
|
||||
/// <summary>
|
||||
/// Eine vom Core oder einem Modul beigesteuerte Fenster-Ansicht. Die eigentliche UI ist ein
|
||||
/// <see cref="Form"/>, das über <see cref="CreateForm"/> erzeugt wird (mit DI-Abhängigkeiten).
|
||||
/// Die Shell zeigt je View höchstens eine Instanz und holt ein offenes Fenster wieder nach vorne.
|
||||
/// </summary>
|
||||
public sealed class ModuleView
|
||||
{
|
||||
/// <summary>Stabile ID für Einzelinstanz-Handling (nur ein Fenster je View).</summary>
|
||||
public string Id { get; init; } = Guid.NewGuid().ToString();
|
||||
|
||||
/// <summary>Titel (Fallback-Fenstertitel, falls das Form keinen eigenen setzt).</summary>
|
||||
public string Title { get; init; } = "Fenster";
|
||||
|
||||
/// <summary>Optionale Gruppierung (z. B. "Core", "CongressTrading").</summary>
|
||||
public string? Group { get; init; }
|
||||
|
||||
/// <summary>Optionale Sortierreihenfolge in Menü/Buttons.</summary>
|
||||
public int Order { get; init; } = 0;
|
||||
|
||||
/// <summary>Optionales Icon für Menü/Buttons.</summary>
|
||||
public System.Drawing.Image? Icon { get; set; }
|
||||
|
||||
/// <summary>Erzeugt das anzuzeigende Fenster (frische Instanz je Öffnung).</summary>
|
||||
public Func<Form> CreateForm { get; init; } = () => new Form();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wird der Shell beim Start übergeben; Core und Module registrieren hier ihre Ansichten.
|
||||
/// Über die Navigations-Mitglieder kann JEDES Fenster (auch Modul-Fenster, die nur den Core kennen)
|
||||
/// das gemeinsame „Fenster"-Menü bauen (siehe <see cref="WindowMenu"/>).
|
||||
/// </summary>
|
||||
public interface IModuleUiHost
|
||||
{
|
||||
void RegisterView(ModuleView view);
|
||||
|
||||
/// <summary>Alle registrierten Ansichten (Core + Module).</summary>
|
||||
IReadOnlyList<ModuleView> Views { get; }
|
||||
|
||||
/// <summary>Ist das Fenster dieser View gerade offen?</summary>
|
||||
bool IsOpen(string viewId);
|
||||
|
||||
/// <summary>Öffnet die View bzw. holt ein bereits offenes Fenster in den Vordergrund.</summary>
|
||||
void OpenView(string viewId);
|
||||
|
||||
/// <summary>Holt das Hauptfenster (Launcher) in den Vordergrund.</summary>
|
||||
void ActivateMain();
|
||||
|
||||
/// <summary>Leitet das (bestätigte) Herunterfahren der App ein. Aus jedem Fenster aufrufbar.</summary>
|
||||
void RequestShutdown();
|
||||
|
||||
/// <summary>Feuert, wenn sich der Offen-Status irgendeiner View ändert.</summary>
|
||||
event Action? OpenStateChanged;
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
using System.Drawing;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace IBKRTrader.Core.Modularity;
|
||||
|
||||
/// <summary>
|
||||
/// Baut das gemeinsame Fenster-Menü, das auf JEDEM Fenster erscheint und das Wechseln zwischen allen
|
||||
/// Fenstern (Launcher + Core + Module) erlaubt. Da es nur den Core-Contract <see cref="IModuleUiHost"/>
|
||||
/// nutzt, funktioniert es auch aus Modul-Fenstern (die die App nicht kennen).
|
||||
/// </summary>
|
||||
public static class WindowMenu
|
||||
{
|
||||
/// <summary>
|
||||
/// Verdrahtet einen <see cref="MenuStrip"/> mit der Fensterliste: füllt ihn sofort und baut ihn bei
|
||||
/// jeder Offen-Status-Änderung neu auf. Die Registrierung wird beim Entsorgen sauber gelöst.
|
||||
/// </summary>
|
||||
public static void Wire(MenuStrip menu, IModuleUiHost host, string? currentViewId)
|
||||
{
|
||||
void Refresh()
|
||||
{
|
||||
if (menu.IsDisposed) return;
|
||||
if (menu.IsHandleCreated && menu.InvokeRequired)
|
||||
{
|
||||
try { menu.BeginInvoke((Action)(() => Populate(menu, host, currentViewId))); }
|
||||
catch { /* Fenster wird gerade geschlossen */ }
|
||||
return;
|
||||
}
|
||||
Populate(menu, host, currentViewId);
|
||||
}
|
||||
|
||||
Populate(menu, host, currentViewId);
|
||||
host.OpenStateChanged += Refresh;
|
||||
menu.Disposed += (_, _) => host.OpenStateChanged -= Refresh;
|
||||
}
|
||||
|
||||
/// <summary>Baut die Menüleiste komplett neu auf (Launcher, alle Views nebeneinander, Aktion rechts).</summary>
|
||||
public static void Populate(MenuStrip menu, IModuleUiHost host, string? currentViewId)
|
||||
{
|
||||
menu.Items.Clear();
|
||||
|
||||
var launcher = new ToolStripMenuItem("Launcher") { Checked = currentViewId == null };
|
||||
if (currentViewId == null) launcher.Font = new Font(launcher.Font, FontStyle.Bold);
|
||||
launcher.Click += (_, _) => host.ActivateMain();
|
||||
menu.Items.Add(launcher);
|
||||
|
||||
foreach (var view in host.Views.OrderBy(v => v.Order).ThenBy(v => v.Title))
|
||||
{
|
||||
bool isCurrent = view.Id == currentViewId;
|
||||
var item = new ToolStripMenuItem(view.Title)
|
||||
{
|
||||
Image = view.Icon,
|
||||
ImageScaling = ToolStripItemImageScaling.SizeToFit,
|
||||
DisplayStyle = view.Icon != null
|
||||
? ToolStripItemDisplayStyle.ImageAndText
|
||||
: ToolStripItemDisplayStyle.Text,
|
||||
Checked = isCurrent || host.IsOpen(view.Id)
|
||||
};
|
||||
if (isCurrent) item.Font = new Font(item.Font, FontStyle.Bold);
|
||||
string id = view.Id;
|
||||
item.Click += (_, _) => host.OpenView(id);
|
||||
menu.Items.Add(item);
|
||||
}
|
||||
|
||||
// Kontextabhängige rechte Aktion: nur der Launcher darf die App beenden; jedes andere Fenster
|
||||
// bietet nur „Fenster schließen" (kein App-Shutdown, Module laufen weiter).
|
||||
if (currentViewId == null)
|
||||
{
|
||||
var exit = new ToolStripMenuItem("Beenden") { Alignment = ToolStripItemAlignment.Right };
|
||||
exit.Click += (_, _) => host.RequestShutdown();
|
||||
menu.Items.Add(exit);
|
||||
}
|
||||
else
|
||||
{
|
||||
var close = new ToolStripMenuItem("Fenster schließen") { Alignment = ToolStripItemAlignment.Right };
|
||||
close.Click += (_, _) => menu.FindForm()?.Close();
|
||||
menu.Items.Add(close);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
using IBKRTrader.Core.Workers;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace IBKRTrader.Core.Modules;
|
||||
|
||||
/// <summary>
|
||||
/// Vertrag für ein eigenständiges Trading-Modul.
|
||||
///
|
||||
/// Architektur-Regel: Der Core kennt konkrete Module NICHT. Module hängen sich
|
||||
/// ausschließlich über diesen Vertrag und den DI-Container ein
|
||||
/// (Abhängigkeit nur Modul → Core, niemals umgekehrt).
|
||||
///
|
||||
/// Lebenszyklus:
|
||||
/// 1. <see cref="RegisterServices"/> – vor dem Bauen des DI-Containers
|
||||
/// 2. <see cref="InitializeAsync"/> – beim App-Start (Migrationen etc.)
|
||||
/// 3. <see cref="GetWorkers"/> – Worker werden der WorkerEngine übergeben
|
||||
/// 4. <see cref="CreateWindow"/> – Launcher öffnet das Modul-Fenster
|
||||
/// </summary>
|
||||
public interface IModule
|
||||
{
|
||||
/// <summary>Eindeutiges Modul-Kürzel, zugleich DB-Tabellen-Präfix (z. B. "CT").</summary>
|
||||
string Key { get; }
|
||||
|
||||
/// <summary>Anzeigename im Launcher (z. B. "Congress Trading").</summary>
|
||||
string DisplayName { get; }
|
||||
|
||||
/// <summary>Kurzbeschreibung der Strategie für den Launcher.</summary>
|
||||
string Description { get; }
|
||||
|
||||
/// <summary>Modul-Version.</summary>
|
||||
string Version { get; }
|
||||
|
||||
/// <summary>Registriert alle Modul-Services im DI-Container (vor dem Build).</summary>
|
||||
void RegisterServices(IServiceCollection services);
|
||||
|
||||
/// <summary>Führt Modul-Migrationen und einmalige Initialisierung aus (beim Start).</summary>
|
||||
Task InitializeAsync(IServiceProvider provider);
|
||||
|
||||
/// <summary>Liefert die Worker/Services des Moduls für die WorkerEngine.</summary>
|
||||
IEnumerable<IWorker> GetWorkers(IServiceProvider provider);
|
||||
|
||||
/// <summary>Erzeugt das eigenständige Fenster des Moduls (vom Launcher geöffnet).</summary>
|
||||
Form CreateWindow(IServiceProvider provider);
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
namespace IBKRTrader.Core.Modules;
|
||||
|
||||
/// <summary>
|
||||
/// Zentrale Registry aller geladenen Module.
|
||||
/// Wird in Program.cs mit den Modul-Instanzen befüllt und als Singleton
|
||||
/// bereitgestellt, damit Launcher und Startup-Code über die Module iterieren
|
||||
/// können, ohne konkrete Modultypen zu kennen.
|
||||
/// </summary>
|
||||
public class ModuleRegistry
|
||||
{
|
||||
private readonly List<IModule> _modules;
|
||||
|
||||
public ModuleRegistry(IEnumerable<IModule> modules)
|
||||
=> _modules = modules?.ToList() ?? throw new ArgumentNullException(nameof(modules));
|
||||
|
||||
/// <summary>Alle registrierten Module in Registrierungsreihenfolge.</summary>
|
||||
public IReadOnlyList<IModule> Modules => _modules;
|
||||
|
||||
/// <summary>Findet ein Modul anhand seines Kürzels (case-insensitive) oder null.</summary>
|
||||
public IModule? Find(string key)
|
||||
=> _modules.FirstOrDefault(m => string.Equals(m.Key, key, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,68 +1,68 @@
|
||||
using IBKRTrader.Core.Logging;
|
||||
using IBKRTrader.Core.Modules;
|
||||
using IBKRTrader.Core.Modularity;
|
||||
using IBKRTrader.Core.Workers;
|
||||
using IBKRTrader.Modules.CongressTrading.Database;
|
||||
using IBKRTrader.Modules.CongressTrading.Scraper;
|
||||
using IBKRTrader.Modules.CongressTrading.UI;
|
||||
using IBKRTrader.Modules.CongressTrading.Workers;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace IBKRTrader.Modules.CongressTrading;
|
||||
|
||||
/// <summary>
|
||||
/// CongressTrading-Modul: kopiert Aktien-Trades von US-Kongressmitgliedern
|
||||
/// (Quelle: capitoltrades.com).
|
||||
///
|
||||
/// Architektur-Regel: Dieses Modul hängt sich ausschließlich über den
|
||||
/// <see cref="IModule"/>-Vertrag ein. Der Core kennt dieses Modul NICHT –
|
||||
/// die Abhängigkeit geht nur von Modul → Core.
|
||||
/// CongressTrading-Modul: kopiert Aktien-Trades von US-Kongressmitgliedern (capitoltrades.com).
|
||||
/// Hängt sich ausschließlich über den <see cref="IModule"/>-Vertrag ein (Abhängigkeit nur Modul → Core).
|
||||
/// </summary>
|
||||
public sealed class CongressTradingModule : IModule
|
||||
{
|
||||
public const string ModuleKey = "CT";
|
||||
/// <summary>Kürzel für Logging/DB-Präfix.</summary>
|
||||
public const string LogTag = "CT";
|
||||
|
||||
public string Key => ModuleKey;
|
||||
public string DisplayName => "Congress Trading";
|
||||
public string Description => "Kopiert Aktien-Trades von US-Kongressmitgliedern (capitoltrades.com).";
|
||||
public string Version => "1.0.0";
|
||||
public string Name => "CongressTrading";
|
||||
public string DbPrefix => "ct_";
|
||||
|
||||
/// <summary>Registriert alle Modul-Services im DI-Container.</summary>
|
||||
public void RegisterServices(IServiceCollection services)
|
||||
// In RegisterUi gesetzt (hat den Provider); für Modul-Migrationen in StartAsync genutzt.
|
||||
private IServiceProvider? _services;
|
||||
|
||||
public void RegisterServices(IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
// Datenbank
|
||||
services.AddSingleton<CongressMigrations>();
|
||||
services.AddSingleton<CongressRepository>();
|
||||
|
||||
// Scraper
|
||||
services.AddSingleton<CapitolTradesScraper>();
|
||||
|
||||
// Worker
|
||||
services.AddSingleton<CongressHistoryImportWorker>();
|
||||
services.AddSingleton<CongressScrapeWorker>();
|
||||
|
||||
// Worker als IWorker registrieren → die WorkerEngine erhält sie über IEnumerable<IWorker>.
|
||||
services.AddSingleton<IWorker>(sp => sp.GetRequiredService<CongressHistoryImportWorker>());
|
||||
services.AddSingleton<IWorker>(sp => sp.GetRequiredService<CongressScrapeWorker>());
|
||||
}
|
||||
|
||||
/// <summary>Führt die Modul-Migrationen aus.</summary>
|
||||
public async Task InitializeAsync(IServiceProvider provider)
|
||||
public void RegisterUi(IModuleUiHost host, IServiceProvider services)
|
||||
{
|
||||
var migrations = provider.GetRequiredService<CongressMigrations>();
|
||||
await migrations.RunAsync();
|
||||
_services = services;
|
||||
|
||||
// Einmaliger Reset (deaktiviert): löscht alten Fortschritt für sauberen Neustart.
|
||||
// var repo = provider.GetRequiredService<CongressRepository>();
|
||||
// await repo.ResetHistoryImportAsync();
|
||||
host.RegisterView(new ModuleView
|
||||
{
|
||||
Id = "congresstrading.main",
|
||||
Title = "Congress Trading",
|
||||
Group = Name,
|
||||
Order = 100,
|
||||
CreateForm = () => new CongressTradingForm(
|
||||
services.GetRequiredService<CongressRepository>(),
|
||||
services.GetRequiredService<WorkerEngine>(),
|
||||
services.GetRequiredService<LoggingService>())
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>Gibt die Modul-Worker für die WorkerEngine zurück.</summary>
|
||||
public IEnumerable<IWorker> GetWorkers(IServiceProvider provider) =>
|
||||
[
|
||||
provider.GetRequiredService<CongressHistoryImportWorker>(),
|
||||
provider.GetRequiredService<CongressScrapeWorker>()
|
||||
];
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
// Modul-Migrationen laufen nach der Core-Initialisierung.
|
||||
if (_services is null) return;
|
||||
var migrations = _services.GetRequiredService<CongressMigrations>();
|
||||
await migrations.RunAsync();
|
||||
}
|
||||
|
||||
/// <summary>Erzeugt das eigenständige Modul-Fenster.</summary>
|
||||
public Form CreateWindow(IServiceProvider provider)
|
||||
=> new CongressTradingForm(
|
||||
provider.GetRequiredService<CongressRepository>(),
|
||||
provider.GetRequiredService<WorkerEngine>(),
|
||||
provider.GetRequiredService<LoggingService>());
|
||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
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.
|
||||
/// Grundgerüst mit DB-Kennzahlen und manuellem Scrape-Trigger.
|
||||
/// Die vollständige Trade-/Positions-Ansicht folgt in einer späteren Phase.
|
||||
/// </summary>
|
||||
public sealed class CongressTradingForm : ModuleFormBase
|
||||
public sealed class CongressTradingForm : Form
|
||||
{
|
||||
private const string ScrapeWorkerName = "CT-ScrapeWorker";
|
||||
|
||||
@@ -25,13 +24,16 @@ public sealed class CongressTradingForm : ModuleFormBase
|
||||
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]";
|
||||
Text = "Congress Trading [CT]";
|
||||
Width = 900;
|
||||
Height = 600;
|
||||
StartPosition = FormStartPosition.CenterScreen;
|
||||
MinimumSize = new Size(600, 400);
|
||||
BuildLayout();
|
||||
}
|
||||
|
||||
@@ -86,7 +88,7 @@ public sealed class CongressTradingForm : ModuleFormBase
|
||||
_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}");
|
||||
_logger.Warn(CongressTradingModule.LogTag, $"Kennzahlen konnten nicht geladen werden: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,12 +98,12 @@ public sealed class CongressTradingForm : ModuleFormBase
|
||||
{
|
||||
_lblStatus.Text = "Scrape angestoßen...";
|
||||
await _engine.TriggerWorkerAsync(ScrapeWorkerName);
|
||||
_logger.Info(CongressTradingModule.ModuleKey, "Scrape-Worker manuell ausgelöst (aus Modul-Fenster).");
|
||||
_logger.Info(CongressTradingModule.LogTag, "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);
|
||||
_logger.Error(CongressTradingModule.LogTag, "Manueller Scrape-Trigger fehlgeschlagen.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user