using Avalonia.Controls; using IBKRTrader.App.ViewModels; using IBKRTrader.App.Views; using IBKRTrader.Core.Modularity; using IBKRTrader.Core.Settings; using IBKRTrader.Hosting; using Microsoft.Extensions.DependencyInjection; namespace IBKRTrader.App.Shell; /// /// Konstruktionsprüfung aller Fenster – ohne Message-Loop und ohne laufende Dienste. /// /// Nachfolger des --smoke-ui-Laufs der WinForms-Shell, der Konstruktionsfehler /// zuverlässig gefangen hat. Der Host wird bewusst nicht gestartet: sonst liefen Worker, /// Broker-Verbindungen und Marktdaten-Abrufe gegen die echten Endpunkte an – für eine reine /// Konstruktionsprüfung unerwünscht, auf einem Build-Server schlicht falsch. /// /// Im Gegensatz zu WinForms braucht Avalonia dafür kein Anzeigegerät: /// SetupWithoutStarting initialisiert das Framework, ohne ein Fenster zu zeigen. Damit ist /// diese Prüfung erstmals CI-tauglich. /// public static class SmokeUi { public static int Run() { Program.BuildAvaloniaApp().SetupWithoutStarting(); var modules = AppHostBuilder.CreateModules(); using var host = AppHostBuilder.Build(modules, ShellServices.Register); App.Services = host.Services; var uiHost = host.Services.GetRequiredService(); CoreViews.Register(uiHost, host.Services); foreach (var module in modules) module.RegisterUi(uiHost, host.Services); ModuleViews.Register(uiHost, host.Services); ViewIcons.AssignDefaults(uiHost); var failures = 0; Console.WriteLine("=== Smoke-UI: Fenster-Konstruktion (Avalonia) ==="); foreach (var view in uiHost.Views) failures += Check(view.Id, view.Title, () => view.CreateView()); failures += Check("shell.launcher", "Launcher", () => new LauncherWindow(uiHost, host.Services)); failures += Check("shell.shutdown", "Beenden-Abfrage", () => new ShutdownConfirmWindow()); // Die Einstellungsmaske entsteht aus den Attributen von AppSettings. Ein Fenster kann // fehlerfrei konstruieren und trotzdem leer sein, wenn die Attribute verlorengehen – // deshalb hier gegen die tatsächliche Feldzahl prüfen. failures += CheckSettingsForm(host.Services.GetRequiredService()); failures += CheckAppIcon(); Console.WriteLine(failures == 0 ? "=== Smoke-UI OK ===" : $"=== Smoke-UI: {failures} Fehler ==="); return failures == 0 ? 0 : 1; } private static int Check(string id, string title, Func construct) { try { _ = construct(); Console.WriteLine($"[OK] {id} ({title})"); return 0; } catch (Exception ex) { Console.WriteLine($"[FEHLER] {id}: {ex.GetType().Name}: {ex.Message}"); return 1; } } /// /// Prüft das Anwendungssymbol. schluckt einen Fehlschlag absichtlich – /// ein fehlendes Symbol darf den Betrieb nicht aufhalten. Dann muss er aber hier /// auffallen, sonst startet die Shell monatelang ohne Symbol und niemand merkt es. Genau /// dieser Fall ist der Anlass: auf dem Server waren die Anwendungen nicht zu unterscheiden. /// /// Geprüft wird gegen ein echtes Fenster statt nur gegen die Ressource: Dass das Bild /// dekodierbar ist, sagt noch nicht, dass es am Fenster ankommt. /// private static int CheckAppIcon() { try { if (AppIcon.Value is null) { Console.WriteLine("[FEHLER] Anwendungssymbol: Assets/icon.png nicht ladbar " + "(AvaloniaResource in der .csproj verloren, oder die PNG ist beschädigt)."); return 1; } var probe = new Window(); AppIcon.Apply(probe); if (probe.Icon is null) { Console.WriteLine("[FEHLER] Anwendungssymbol: geladen, kommt aber nicht am Fenster an."); return 1; } Console.WriteLine("[OK] Anwendungssymbol (Assets/icon.png)"); return 0; } catch (Exception ex) { Console.WriteLine($"[FEHLER] Anwendungssymbol: {ex.GetType().Name}: {ex.Message}"); return 1; } } private static int CheckSettingsForm(SettingsService settings) { try { var sections = SettingsModelBuilder.Build(settings.Settings); var fieldCount = sections.Sum(s => s.Fields.Count); if (sections.Count == 0 || fieldCount == 0) { Console.WriteLine("[FEHLER] Einstellungsmaske: keine Felder aus AppSettings ermittelt " + "(Category-/DisplayName-Attribute verloren?)."); return 1; } Console.WriteLine($"[OK] Einstellungsmaske: {sections.Count} Abschnitte, {fieldCount} Felder"); return 0; } catch (Exception ex) { Console.WriteLine($"[FEHLER] Einstellungsmaske: {ex.GetType().Name}: {ex.Message}"); return 1; } } }