L3: Avalonia-Shell und Core-Ansichten - die Oberflaeche laeuft auf Linux
Neues Projekt IBKRTrader.App.Avalonia (net10.0, plattformneutral) mit Launcher, Fenster-Menue, Beenden-Abfrage und den vier Core-Ansichten Dashboard, Workers, Logs und Settings. Nutzt denselben AppHostBuilder wie der Daemon; ShellServices ergaenzt nur, was ohne Oberflaeche nicht existiert. Versionen bewusst auf der 11er-Linie: Avalonia 11.3.19, DataGrid 11.3.13. LiveCharts2 2.0.5 ist gegen Avalonia 11 gebaut und bricht unter 12 (Gestures.PinchEvent gibt es dort nicht mehr). Diagramme kommen mit den neuen Modulen - bis LiveCharts2 Avalonia 12 unterstuetzt, darf hier nicht angehoben werden. Erfahrung aus PolytraderSharp, im csproj vermerkt. Ersatz fuer WinForms-Bausteine ohne Gegenstueck: - PropertyGrid -> SettingsModelBuilder erzeugt die Maske aus den bereits vorhandenen Category-/DisplayName-/Description-Attributen von AppSettings. Eine neue Einstellung erscheint damit automatisch, ohne dass jemand die Oberflaeche anfasst - genau der Vorteil des PropertyGrid. Kennwortfelder (DB-Passwort, Flex-Token) werden verdeckt. Gemessen: 11 Abschnitte, 41 Felder. - MessageBox.Show -> ShutdownConfirmWindow (Avalonia bringt keinen Meldungsdialog mit). Schliessen ueber das X zaehlt als Abbruch, damit ein versehentlicher Klick nie den Handelsbetrieb stoppt. - RichTextBox mit SelectionColor -> eingefaerbte Elemente je Logzeile, mit Filter, Auto-Scroll und Zeilenbegrenzung (im Dauerbetrieb waere die Liste sonst unbegrenzt gewachsen). - ToolStrip/StatusStrip -> zentrale Stilklassen in App.axaml (toolbar, statusbar, kpi, section). Die neuen Module setzen darauf auf. Gemeinsame Bausteine bewusst jetzt schon zentral, weil die neuen Module direkt in Avalonia entwickelt werden sollen. Kompilierte Bindings sind aktiv (x:DataType je Datenkontext) - Tippfehler in Bindings fallen damit beim Kompilieren auf statt erst zur Laufzeit. Dafuer brauchte es benannte Record-Zeilentypen statt der anonymen Typen, die DataGridView.DataSource frueher bekommen hat. --smoke-ui laeuft jetzt ueber SetupWithoutStarting, also OHNE Anzeigegeraet und ohne laufende Dienste. Das war mit WinForms nicht moeglich und macht die Konstruktionspruefung erstmals CI-tauglich. Der Host wird dabei bewusst nicht gestartet, sonst liefen Worker und Broker-Verbindungen gegen die echten Endpunkte an. AVLN3001 unterdrueckt: die Fenster haben absichtlich keinen parameterlosen Konstruktor. Einer wuerde sie ohne ihre Dienste konstruierbar machen und genau den Fehler verdecken, den die Konstruktionspruefung finden soll. Verifiziert: Build 0 Fehler/0 Warnungen, 193 Tests gruen, Avalonia-Smoke-UI konstruiert alle 6 Fenster, publish -r linux-x64 liefert 32 MB mit ELF-Launcher samt libSkiaSharp.so/libHarfBuzzSharp.so und ohne Windows-Abhaengigkeiten. Die WinForms-Shell ist unveraendert lauffaehig. Offen fuer L4: die drei Modul-Fenster (ModuleViews ist noch leer). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
using System.ComponentModel;
|
||||
using System.Reflection;
|
||||
|
||||
namespace IBKRTrader.App.Avalonia.ViewModels;
|
||||
|
||||
/// <summary>Ein bearbeitbares Einzelfeld der Einstellungen.</summary>
|
||||
public sealed class SettingsField
|
||||
{
|
||||
public required string DisplayName { get; init; }
|
||||
public required string Description { get; init; }
|
||||
public required Type ValueType { get; init; }
|
||||
public required bool IsPassword { get; init; }
|
||||
|
||||
public required Func<object?> Get { get; init; }
|
||||
public required Action<object?> Set { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>Ein Abschnitt (entspricht einem aufklappbaren Knoten des früheren PropertyGrid).</summary>
|
||||
public sealed record SettingsSection(string Title, IReadOnlyList<SettingsField> Fields);
|
||||
|
||||
/// <summary>
|
||||
/// Baut die Eingabemaske der Einstellungen aus den Attributen von <c>AppSettings</c>.
|
||||
///
|
||||
/// <para><b>Warum aus Attributen:</b> Die WinForms-Fassung zeigte <c>AppSettings</c> in einem
|
||||
/// <c>PropertyGrid</c>. Avalonia hat dafür kein Gegenstück. Die Klassen tragen bereits
|
||||
/// <see cref="CategoryAttribute"/>, <see cref="DisplayNameAttribute"/> und
|
||||
/// <see cref="DescriptionAttribute"/> – daraus lässt sich die Maske erzeugen, statt sie von Hand
|
||||
/// zu pflegen. Eine neue Einstellung erscheint damit automatisch, ohne dass jemand die Oberfläche
|
||||
/// anfasst; genau das war der Vorteil des PropertyGrid, und er bleibt erhalten.</para>
|
||||
/// </summary>
|
||||
public static class SettingsModelBuilder
|
||||
{
|
||||
/// <summary>Typen, die als Eingabefeld dargestellt werden. Alles andere gilt als Unterabschnitt.</summary>
|
||||
private static bool IsLeaf(Type t) =>
|
||||
t == typeof(string) || t.IsEnum ||
|
||||
t == typeof(int) || t == typeof(long) || t == typeof(double) ||
|
||||
t == typeof(decimal) || t == typeof(bool);
|
||||
|
||||
/// <summary>Zerlegt das Einstellungsobjekt in Abschnitte mit Feldern.</summary>
|
||||
public static IReadOnlyList<SettingsSection> Build(object root)
|
||||
{
|
||||
var sections = new List<SettingsSection>();
|
||||
Walk(root, prefix: null, sections);
|
||||
return sections;
|
||||
}
|
||||
|
||||
private static void Walk(object owner, string? prefix, List<SettingsSection> sections)
|
||||
{
|
||||
var fields = new List<SettingsField>();
|
||||
|
||||
foreach (var prop in owner.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance))
|
||||
{
|
||||
if (!prop.CanRead || prop.GetIndexParameters().Length > 0) continue;
|
||||
|
||||
var title = prop.GetCustomAttribute<DisplayNameAttribute>()?.DisplayName ?? prop.Name;
|
||||
|
||||
if (IsLeaf(prop.PropertyType))
|
||||
{
|
||||
if (!prop.CanWrite) continue; // z. B. berechnete Eigenschaften
|
||||
|
||||
var target = owner; // für den Abschluss festhalten
|
||||
fields.Add(new SettingsField
|
||||
{
|
||||
DisplayName = title,
|
||||
Description = prop.GetCustomAttribute<DescriptionAttribute>()?.Description ?? "",
|
||||
ValueType = prop.PropertyType,
|
||||
IsPassword = prop.GetCustomAttribute<PasswordPropertyTextAttribute>()?.Password == true,
|
||||
Get = () => prop.GetValue(target),
|
||||
Set = v => prop.SetValue(target, v)
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Verschachteltes Einstellungsobjekt → eigener Abschnitt. Nur eigene Typen verfolgen,
|
||||
// damit die Rekursion nicht in Framework-Typen abbiegt.
|
||||
if (prop.PropertyType.IsClass && prop.PropertyType.Namespace?.StartsWith("IBKRTrader") == true)
|
||||
{
|
||||
var child = prop.GetValue(owner);
|
||||
if (child is not null)
|
||||
Walk(child, prefix is null ? title : $"{prefix} · {title}", sections);
|
||||
}
|
||||
}
|
||||
|
||||
if (fields.Count > 0)
|
||||
sections.Add(new SettingsSection(prefix ?? "Allgemein", fields));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user