diff --git a/src/PolyTrader.App.Avalonia/Controls/SettingsEditor.axaml b/src/PolyTrader.App.Avalonia/Controls/SettingsEditor.axaml
new file mode 100644
index 0000000..6acc58a
--- /dev/null
+++ b/src/PolyTrader.App.Avalonia/Controls/SettingsEditor.axaml
@@ -0,0 +1,124 @@
+
+
+
+
+
+ 210
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/PolyTrader.App.Avalonia/Controls/SettingsEditor.axaml.cs b/src/PolyTrader.App.Avalonia/Controls/SettingsEditor.axaml.cs
new file mode 100644
index 0000000..5dd24d9
--- /dev/null
+++ b/src/PolyTrader.App.Avalonia/Controls/SettingsEditor.axaml.cs
@@ -0,0 +1,26 @@
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using PolyTrader.App.Avalonia.ViewModels;
+
+namespace PolyTrader.App.Avalonia.Controls
+{
+ ///
+ /// Zeigt ein beliebiges Einstellungsobjekt als kategorisiertes Formular. Ersetzt das
+ /// WinForms-PropertyGrid an allen vier Stellen (Server-Settings, Account-Einstellungen,
+ /// Master-Trader, ResolutionFarming) – ein Aufruf je Fenster, kein Feld-Code.
+ ///
+ /// Änderungen wirken direkt auf dem übergebenen Objekt; das Speichern (Datei/DB) bleibt
+ /// Sache des jeweiligen Fensters – genau wie zuvor beim PropertyGrid.
+ ///
+ public partial class SettingsEditor : UserControl
+ {
+ public SettingsEditor() => AvaloniaXamlLoader.Load(this);
+
+ /// Setzt das anzuzeigende Objekt (oder null, um zu leeren).
+ public void Show(object? target)
+ {
+ var list = this.FindControl("sections")!;
+ list.ItemsSource = target == null ? null : SettingsModelBuilder.Build(target);
+ }
+ }
+}
diff --git a/src/PolyTrader.App.Avalonia/Program.cs b/src/PolyTrader.App.Avalonia/Program.cs
index cbf56f9..9855250 100644
--- a/src/PolyTrader.App.Avalonia/Program.cs
+++ b/src/PolyTrader.App.Avalonia/Program.cs
@@ -210,6 +210,31 @@ namespace PolyTrader.App.Avalonia
Console.WriteLine($"[FEHLER] ShutdownConfirmWindow: {ex.GetType().Name}: {ex.Message}");
}
+ // Der Einstellungs-Editor wird aus den Attributen des Modells aufgebaut. Ein Fenster kann
+ // fehlerfrei konstruieren und trotzdem leer sein, wenn die Attribute verlorengehen -
+ // deshalb hier gegen die tatsaechliche Feldzahl pruefen.
+ try
+ {
+ var sections = ViewModels.SettingsModelBuilder.Build(new ServerSettings());
+ int fieldCount = sections.Sum(x => x.Fields.Count);
+ if (sections.Count == 0 || fieldCount == 0)
+ {
+ failures++;
+ Console.WriteLine("[FEHLER] Einstellungs-Editor: keine Felder aus ServerSettings ermittelt " +
+ "(Category-/DisplayName-Attribute verloren?).");
+ }
+ else
+ {
+ Console.WriteLine($"[OK] Einstellungs-Editor: {sections.Count} Abschnitte, {fieldCount} Felder " +
+ $"({string.Join(", ", sections.Select(x => x.Title))})");
+ }
+ }
+ catch (Exception ex)
+ {
+ failures++;
+ Console.WriteLine($"[FEHLER] Einstellungs-Editor: {ex.GetType().Name}: {ex.Message}");
+ }
+
Console.WriteLine(failures == 0 ? "=== Smoke-UI OK ===" : $"=== Smoke-UI: {failures} Fehler ===");
return failures == 0 ? 0 : 1;
}
diff --git a/src/PolyTrader.App.Avalonia/Shell/CoreViews.cs b/src/PolyTrader.App.Avalonia/Shell/CoreViews.cs
index ae28174..ffd49bc 100644
--- a/src/PolyTrader.App.Avalonia/Shell/CoreViews.cs
+++ b/src/PolyTrader.App.Avalonia/Shell/CoreViews.cs
@@ -35,6 +35,21 @@ namespace PolyTrader.App.Avalonia.Shell
ServerSettingsPath)
});
+ host.RegisterView(new ModuleView
+ {
+ Id = "core.settings",
+ Title = "Server Settings",
+ Group = "Core",
+ Order = 20,
+ CreateView = () => new Views.SettingsWindow(
+ host,
+ services.GetRequiredService(),
+ services.GetRequiredService(),
+ services.GetRequiredService(),
+ services.GetService(),
+ services.GetService())
+ });
+
host.RegisterView(new ModuleView
{
Id = "core.jobs",
@@ -44,7 +59,7 @@ namespace PolyTrader.App.Avalonia.Shell
CreateView = () => new Views.JobsWindow(host, services.GetRequiredService())
});
- // TODO Portierung: core.settings (Order 20) und core.terminal (Order 40) folgen.
+ // TODO Portierung: core.terminal (Order 40) folgt.
// Aufbau siehe docs/UI-SPEZIFIKATION-WinForms.md, Originalcode im Git-Tag winforms-final.
}
diff --git a/src/PolyTrader.App.Avalonia/ViewModels/SettingsFields.cs b/src/PolyTrader.App.Avalonia/ViewModels/SettingsFields.cs
new file mode 100644
index 0000000..f5729c9
--- /dev/null
+++ b/src/PolyTrader.App.Avalonia/ViewModels/SettingsFields.cs
@@ -0,0 +1,188 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+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;
+
+ 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(); } }
+ }
+ }
+
+ /// 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
+ {
+ 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