using System.ComponentModel; using System.Reflection; namespace IBKRTrader.App.Avalonia.ViewModels; /// Ein bearbeitbares Einzelfeld der Einstellungen. 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 Get { get; init; } public required Action Set { get; init; } } /// Ein Abschnitt (entspricht einem aufklappbaren Knoten des früheren PropertyGrid). public sealed record SettingsSection(string Title, IReadOnlyList Fields); /// /// Baut die Eingabemaske der Einstellungen aus den Attributen von AppSettings. /// /// Warum aus Attributen: Die WinForms-Fassung zeigte AppSettings in einem /// PropertyGrid. Avalonia hat dafür kein Gegenstück. Die Klassen tragen bereits /// , und /// – 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. /// public static class SettingsModelBuilder { /// Typen, die als Eingabefeld dargestellt werden. Alles andere gilt als Unterabschnitt. 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); /// Zerlegt das Einstellungsobjekt in Abschnitte mit Feldern. public static IReadOnlyList Build(object root) { var sections = new List(); Walk(root, prefix: null, sections); return sections; } private static void Walk(object owner, string? prefix, List sections) { var fields = new List(); foreach (var prop in owner.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)) { if (!prop.CanRead || prop.GetIndexParameters().Length > 0) continue; var title = prop.GetCustomAttribute()?.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()?.Description ?? "", ValueType = prop.PropertyType, IsPassword = prop.GetCustomAttribute()?.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)); } }