using System; using System.Collections.Generic; using System.ComponentModel; using System.Reflection; using Godot; public sealed class FormBinder where TViewModel : INotifyPropertyChanged { private readonly TViewModel _vm; private readonly Control _root; private readonly Dictionary _lineToProp = new(); private readonly Dictionary _propNameToLine = new(StringComparer.OrdinalIgnoreCase); private bool _suppress; public FormBinder(TViewModel vm, Control root) { _vm = vm; _root = root; } public FormBinder Bind() { Discover(); _vm.PropertyChanged += OnPropertyChanged; foreach (var (line, prop) in _lineToProp) { line.TextChanged += (string text) => { if (_suppress) return; if (prop.PropertyType == typeof(string) || prop.PropertyType == typeof(string)) prop.SetValue(_vm, text); }; } // Initial sync UI <- VM PushAllFromVm(); return this; } private void Discover() { var props = typeof(TViewModel).GetProperties(BindingFlags.Instance | BindingFlags.Public); var propMap = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var p in props) if (p.CanRead || p.CanWrite) propMap[p.Name] = p; void Walk(Node n) { if (n is LineEdit le) { // Prefer explicit metadata "vm_prop" to map to a property name; fallback to node name. string propName = le.HasMeta("vm_prop") ? (string)le.GetMeta("vm_prop") : StripSuffix(le.Name, "LineEdit"); if (propMap.TryGetValue(propName, out var pi)) { _lineToProp[le] = pi; _propNameToLine[propName] = le; } } foreach (var c in n.GetChildren()) if (c is Node child) Walk(child); } Walk(_root); } private static string StripSuffix(string s, string suffix) => s.EndsWith(suffix, StringComparison.Ordinal) ? s[..^suffix.Length] : s; private void OnPropertyChanged(object? sender, PropertyChangedEventArgs e) { if (e.PropertyName == null) return; if (_propNameToLine.TryGetValue(e.PropertyName, out var le)) { _suppress = true; try { le.Text = _lineToProp[le].GetValue(_vm)?.ToString() ?? string.Empty; } finally { _suppress = false; } } } public void PushAllFromVm() { _suppress = true; try { foreach (var (line, prop) in _lineToProp) line.Text = prop.GetValue(_vm)?.ToString() ?? string.Empty; } finally { _suppress = false; } } }