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