Settings nach Avalonia - PropertyGrid durch kategorisierten Editor ersetzt
Der PropertyGrid-Ersatz ist EIN wiederverwendbares Steuerelement statt Handarbeit je Feld: Controls/SettingsEditor zeigt ein beliebiges Einstellungsobjekt nach Richards Vorlage - Kategorie-Ueberschrift, Beschriftung links, Feld rechts, Erklaerung klein darunter. - SettingsModelBuilder liest die Attribute, die fuers PropertyGrid ohnehin gepflegt waren: [Category] gruppiert, [DisplayName] beschriftet, [Description] wird zum Hinweistext, [Browsable(false)] blendet aus (Watchdog-Token, Lizenzschluessel bleiben unsichtbar). Ergebnis fuer ServerSettings: 6 Abschnitte, 15 Felder - ohne eine Zeile Feld-Code. - Layout-Regel gewahrt: WELCHE Felder es gibt, kommt als Daten; WIE ein Feld aussieht, steht deklarativ als DataTemplate je Feldtyp (Text/Zahl/Ja-Nein/Auswahl/Nur-Lese). - Damit sind auch die drei restlichen PropertyGrid-Stellen (Account-Einstellungen, Master-Trader, ResolutionFarming) mit je einem Aufruf erledigt. SettingsWindow: Server-Settings + Polymarket-Accounts, Master-Key erzeugen, OpenRouter-Key/Watchdog-Token setzen, Test-Heartbeat. Rueckmeldungen laufen ueber die Statuszeile statt ueber Dialoge - nur echte Entscheidungen bekommen einen Dialog (neu: Views/DialogWindow fuer Hinweis/Rueckfrage/maskierte Eingabe, Avalonia hat keine MessageBox). Nebenbei einen offenen Punkt der Linux-Analyse erledigt: Schluesseldateien (master.key, openrouter.key) werden jetzt per File.SetUnixFileMode auf 600 gesetzt. Auf Linux legt File.WriteAllText sonst mit ueblicher umask 644 an - weltweit lesbar. Lizenzdialog bewusst NICHT portiert: haengt am alten LicenseLabrador-SDK, das mit der Deploymentcenter-Anbindung (P3c) ohnehin ersetzt wird - waere Wegwerfarbeit. Smoke-UI prueft jetzt zusaetzlich die Feldzahl des Editors: ein Fenster kann fehlerfrei konstruieren und trotzdem leer sein, wenn die Attribute verlorengehen. Verifiziert: Solution baut, 442 Tests gruen, --smoke-ui gruen (5 Fenster + Editor-Pruefung), Linux-Publish laeuft. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
{
|
||||
/// <summary>Ein Eingabefeld der Einstellungen. Schreibt Änderungen direkt in das Zielobjekt.</summary>
|
||||
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; }
|
||||
|
||||
/// <summary>Erklärungstext aus dem <see cref="DescriptionAttribute"/> – erscheint klein unter dem Feld.</summary>
|
||||
public string Description { get; }
|
||||
|
||||
public bool HasDescription => Description.Length > 0;
|
||||
|
||||
/// <summary>Falsch bei Eigenschaften ohne Setter (reine Statusanzeigen).</summary>
|
||||
public bool IsEditable => Property.CanWrite;
|
||||
|
||||
protected void Raise([CallerMemberName] string? name = null) =>
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
}
|
||||
|
||||
/// <summary>Freitext (string).</summary>
|
||||
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(); } }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Ja/Nein (bool) – als Kontrollkästchen.</summary>
|
||||
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(); } }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ganzzahl. Bewusst als Text mit invarianter Umwandlung: eine ungültige Eingabe lässt den
|
||||
/// bisherigen Wert stehen, statt still eine 0 zu schreiben.
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Auswahl aus festen Werten (Enum) – als Aufklappliste.</summary>
|
||||
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<string> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Nur-Lese-Anzeige (Eigenschaft ohne Setter, oder nicht editierbarer Typ).</summary>
|
||||
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<object?>().Select(o => o?.ToString()));
|
||||
return raw?.ToString() ?? string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Eine Kategorie mit ihren Feldern – entspricht einer Überschrift im Dialog.</summary>
|
||||
public sealed class SettingsSection
|
||||
{
|
||||
public string Title { get; init; } = string.Empty;
|
||||
public IReadOnlyList<SettingsField> Fields { get; init; } = Array.Empty<SettingsField>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Baut aus einem Einstellungsobjekt die Abschnitte für den <c>SettingsEditor</c>.
|
||||
///
|
||||
/// <para>Nutzt die Attribute, die für das frühere <c>PropertyGrid</c> ohnehin schon gepflegt
|
||||
/// wurden: <see cref="CategoryAttribute"/> gruppiert, <see cref="DisplayNameAttribute"/> liefert
|
||||
/// die Beschriftung, <see cref="DescriptionAttribute"/> den Hinweistext unter dem Feld, und
|
||||
/// <c>[Browsable(false)]</c> blendet aus (z.B. Watchdog-Token und Lizenzschlüssel – die gehören
|
||||
/// nicht offen in die Oberfläche).</para>
|
||||
///
|
||||
/// <para>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.</para>
|
||||
/// </summary>
|
||||
public static class SettingsModelBuilder
|
||||
{
|
||||
public static IReadOnlyList<SettingsSection> Build(object target)
|
||||
{
|
||||
var fields = target.GetType()
|
||||
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||||
.Where(p => p.GetIndexParameters().Length == 0)
|
||||
.Where(p => p.GetCustomAttribute<BrowsableAttribute>()?.Browsable != false)
|
||||
.Select(p => new
|
||||
{
|
||||
Property = p,
|
||||
Category = p.GetCustomAttribute<CategoryAttribute>()?.Category ?? "Allgemein",
|
||||
Label = p.GetCustomAttribute<DisplayNameAttribute>()?.DisplayName ?? p.Name,
|
||||
Description = p.GetCustomAttribute<DescriptionAttribute>()?.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(string)) return 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user