using System; using System.Collections.Generic; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Globalization; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; namespace PolyTrader.App.Avalonia.ViewModels { /// Ein Eingabefeld der Einstellungen. Schreibt Änderungen direkt in das Zielobjekt. public abstract class SettingsField : INotifyPropertyChanged { protected readonly object Target; protected readonly PropertyInfo Property; protected SettingsField(object target, PropertyInfo property, string label, string? description) { Target = target; Property = property; Label = label; Description = description ?? string.Empty; } public string Label { get; } /// Erklärungstext aus dem – erscheint klein unter dem Feld. public string Description { get; } public bool HasDescription => Description.Length > 0; /// Falsch bei Eigenschaften ohne Setter (reine Statusanzeigen). public bool IsEditable => Property.CanWrite; /// Typ der dahinterliegenden Eigenschaft – die Konstruktionsprüfung schaut darauf. public Type ValueType => Property.PropertyType; protected void Raise([CallerMemberName] string? name = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); public event PropertyChangedEventHandler? PropertyChanged; } /// Freitext (string). public sealed class TextSettingsField : SettingsField { public TextSettingsField(object t, PropertyInfo p, string l, string? d) : base(t, p, l, d) { } public string Value { get => Property.GetValue(Target) as string ?? string.Empty; set { if (IsEditable) { Property.SetValue(Target, value ?? string.Empty); Raise(); } } } } /// /// Freitext über mehrere Zeilen. Gleiche Bindung wie , nur mit /// eigener Darstellung: Bis zum 26.08.2026 standen auch „Beschreibung" und „Begründung" /// (je bis 1000 Zeichen) in einzeiligen Feldern, in denen nur ein Bruchteil sichtbar war /// (Befund B-14 des Linux-Sichttests). Ausgezeichnet wird das am Modell mit /// [DataType(DataType.MultilineText)]. /// public sealed class MultilineTextSettingsField : SettingsField { public MultilineTextSettingsField(object t, PropertyInfo p, string l, string? d) : base(t, p, l, d) { } public string Value { get => Property.GetValue(Target) as string ?? string.Empty; set { if (IsEditable) { Property.SetValue(Target, value ?? string.Empty); Raise(); } } } } /// /// Dezimalzahl (decimal/double/float) – wie bewusst als Text: /// eine ungültige Eingabe lässt den bisherigen Wert stehen, statt still eine 0 zu schreiben. /// /// Vor dem 27.08.2026 fehlte dieser Feldtyp: alle Dezimal-Einstellungen fielen auf /// zurück und waren damit in der Oberfläche gar nicht mehr /// änderbar – betroffen war die komplette Risikokonfiguration (Positionsgrößen, Preisgrenzen, /// Kill-Switch-Schwellen), die unter WinForms im PropertyGrid noch editierbar war. /// /// Beim Lesen wird die Anzeigekultur verwendet (de-DE: „5,0"), beim Schreiben werden /// Komma und Punkt akzeptiert – sonst scheitert ein aus der Dokumentation kopierter Wert. /// public sealed class DecimalSettingsField : SettingsField { public DecimalSettingsField(object t, PropertyInfo p, string l, string? d) : base(t, p, l, d) { } public string Value { get { object? raw = Property.GetValue(Target); return raw switch { decimal m => m.ToString("0.############", CultureInfo.CurrentCulture), double db => db.ToString("0.############", CultureInfo.CurrentCulture), float f => f.ToString("0.############", CultureInfo.CurrentCulture), _ => string.Empty }; } set { if (!IsEditable) return; if (TryParse(value, out decimal parsed)) { Type t = Nullable.GetUnderlyingType(Property.PropertyType) ?? Property.PropertyType; if (t == typeof(decimal)) Property.SetValue(Target, parsed); else if (t == typeof(double)) Property.SetValue(Target, (double)parsed); else if (t == typeof(float)) Property.SetValue(Target, (float)parsed); } Raise(); } } /// Nimmt „5,0" und „5.0" an – unabhängig von der eingestellten Kultur. internal static bool TryParse(string? text, out decimal value) { value = 0m; if (string.IsNullOrWhiteSpace(text)) return false; const NumberStyles styles = NumberStyles.Float; return decimal.TryParse(text, styles, CultureInfo.CurrentCulture, out value) || decimal.TryParse(text, styles, CultureInfo.InvariantCulture, out value) || decimal.TryParse(text.Replace(',', '.'), styles, CultureInfo.InvariantCulture, out value); } } /// Ja/Nein (bool) – als Kontrollkästchen. public sealed class BoolSettingsField : SettingsField { public BoolSettingsField(object t, PropertyInfo p, string l, string? d) : base(t, p, l, d) { } public bool Value { get => Property.GetValue(Target) is true; set { if (IsEditable) { Property.SetValue(Target, value); Raise(); } } } } /// /// Ganzzahl. Bewusst als Text mit invarianter Umwandlung: eine ungültige Eingabe lässt den /// bisherigen Wert stehen, statt still eine 0 zu schreiben. /// public sealed class IntSettingsField : SettingsField { public IntSettingsField(object t, PropertyInfo p, string l, string? d) : base(t, p, l, d) { } public string Value { get => Convert.ToInt32(Property.GetValue(Target) ?? 0).ToString(CultureInfo.InvariantCulture); set { if (!IsEditable) return; if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out int parsed)) Property.SetValue(Target, parsed); Raise(); } } } /// Auswahl aus festen Werten (Enum) – als Aufklappliste. public sealed class ChoiceSettingsField : SettingsField { public ChoiceSettingsField(object t, PropertyInfo p, string l, string? d) : base(t, p, l, d) { Options = Enum.GetNames(Nullable.GetUnderlyingType(p.PropertyType) ?? p.PropertyType); } public IReadOnlyList Options { get; } public string? Value { get => Property.GetValue(Target)?.ToString(); set { if (!IsEditable || value == null) return; Property.SetValue(Target, Enum.Parse( Nullable.GetUnderlyingType(Property.PropertyType) ?? Property.PropertyType, value)); Raise(); } } } /// Nur-Lese-Anzeige (Eigenschaft ohne Setter, oder nicht editierbarer Typ). public sealed class ReadOnlySettingsField : SettingsField { /// Steht für „hier steht nichts" – dieselbe Schreibweise wie in der Account-Übersicht. public const string EmptyPlaceholder = "—"; public ReadOnlySettingsField(object t, PropertyInfo p, string l, string? d) : base(t, p, l, d) { } public string Value { get { object? raw = Property.GetValue(Target); if (raw is System.Collections.IEnumerable seq and not string) return string.Join(", ", seq.Cast().Select(o => o?.ToString())); return raw?.ToString() ?? string.Empty; } } /// /// Der Wert für die Oberfläche: leer wird zum Platzhalter. /// /// /// „Deaktivierte Module" ist im Auslieferungszustand eine leere Liste. Ein leeres Nur-Lese-Feld /// ist von einem fehlenden Bedienelement nicht zu unterscheiden – in der Zeile stand links die /// Beschriftung, rechts der Hilfetext und dazwischen nichts, während jede andere Zeile des /// Dialogs dort ein Feld hat (Befund #386). Der Platzhalter beantwortet die Frage „ist hier /// etwas kaputt?" ohne Blick in den Quelltext. /// public string DisplayValue { get { string v = Value; return string.IsNullOrWhiteSpace(v) ? EmptyPlaceholder : v; } } } /// Eine Kategorie mit ihren Feldern – entspricht einer Überschrift im Dialog. public sealed class SettingsSection { public string Title { get; init; } = string.Empty; public IReadOnlyList Fields { get; init; } = Array.Empty(); } /// /// Baut aus einem Einstellungsobjekt die Abschnitte für den SettingsEditor. /// /// Nutzt die Attribute, die für das frühere PropertyGrid ohnehin schon gepflegt /// wurden: gruppiert, liefert /// die Beschriftung, den Hinweistext unter dem Feld, und /// [Browsable(false)] blendet aus (z.B. Watchdog-Token und Lizenzschlüssel – die gehören /// nicht offen in die Oberfläche). /// /// Damit ist der Ersatz des PropertyGrid keine Fleißarbeit je Feld: die Beschreibung der /// Felder steht weiterhin am Modell, nur die Darstellung ist jetzt eine eigene, gestaltbare /// Oberfläche statt einer generischen Reflection-Liste. /// public static class SettingsModelBuilder { public static IReadOnlyList Build(object target) { var fields = target.GetType() .GetProperties(BindingFlags.Public | BindingFlags.Instance) .Where(p => p.GetIndexParameters().Length == 0) .Where(p => p.GetCustomAttribute()?.Browsable != false) .Select(p => new { Property = p, Category = p.GetCustomAttribute()?.Category ?? "Allgemein", Label = p.GetCustomAttribute()?.DisplayName ?? p.Name, Description = p.GetCustomAttribute()?.Description }) .ToList(); return fields .GroupBy(f => f.Category) .Select(g => new SettingsSection { Title = g.Key, Fields = g.Select(f => Create(target, f.Property, f.Label, f.Description)).ToList() }) .ToList(); } private static SettingsField Create(object target, PropertyInfo p, string label, string? description) { Type t = Nullable.GetUnderlyingType(p.PropertyType) ?? p.PropertyType; if (!p.CanWrite) return new ReadOnlySettingsField(target, p, label, description); if (t.IsEnum) return new ChoiceSettingsField(target, p, label, description); if (t == typeof(bool)) return new BoolSettingsField(target, p, label, description); if (t == typeof(int) || t == typeof(long)) return new IntSettingsField(target, p, label, description); if (t == typeof(decimal) || t == typeof(double) || t == typeof(float)) return new DecimalSettingsField(target, p, label, description); if (t == typeof(string)) { bool multiline = p.GetCustomAttribute()?.DataType == DataType.MultilineText; return multiline ? new MultilineTextSettingsField(target, p, label, description) : new TextSettingsField(target, p, label, description); } // Listen u.a. werden angezeigt, aber nicht hier bearbeitet (z.B. DisabledModules – // die pflegt der Modul-Tab des Dashboards). return new ReadOnlySettingsField(target, p, label, description); } } }