Skip to content

Create Dapplo.Ini.Ui — UI configuration framework with WPF demo app#98

Draft
Copilot wants to merge 10 commits intomainfrom
copilot/create-ui-configuration-framework
Draft

Create Dapplo.Ini.Ui — UI configuration framework with WPF demo app#98
Copilot wants to merge 10 commits intomainfrom
copilot/create-ui-configuration-framework

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 14, 2026

Description

Introduces Dapplo.Ini.Ui, Dapplo.Ini.Ui.Generator, and Dapplo.Ini.Ui.DemoApp — a trio of new projects that let developers declaratively describe a settings UI via attributes on their existing IIniSection interfaces, generate compile-time UI descriptors, and render a fully functional WPF settings window with no per-section hand-written XAML.

New project: src/Dapplo.Ini.Ui/ (targets net10.0)

Attributes

Attribute Purpose
[UiControl(UiControlType.Slider, Minimum=0, Maximum=100)] Explicit control type + range/step/unit/placeholder
[UiGroup("Proxy", Order=5)] Groups properties under a labelled panel; supports group-level visibility/enable conditions
[UiPage(Title="Network", Category="Advanced", Order=10)] Marks a section as a named page/tab
[UiLayout(UiTabLayout.VerticalTabs)] Assembly- or interface-level window layout
[UiConditionalVisibility(nameof(UseProxy))] Show/hide control based on another bool property (supports Invert)
[UiConditionalEnable(nameof(UseProxy))] Enable/disable control based on another bool property (supports Invert)
[UiLabelKey("proxy_host_label", DescriptionKey="proxy_host_tip")] i18n key for label and tooltip
[UiChangeMode(UiChangeMode.OnConfirm)] Section- or property-level immediate vs. buffered change
[UiOrder(20)] Explicit property display order

Interfaces & runtime

  • IUiSettingsManagerBeginSession / Apply / Rollback / EndSession lifecycle; UiSettingsManager delegates to ITransactional for buffered changes with auto-rollback on cancel.
  • IUiControlEvents — optional partial-class hook interface with default no-op implementations: OnPropertyChanged, OnControlFocused, OnControlBlurred, OnControlClicked.
  • UiMetadataReader — reflects all UI attributes at runtime into UiPageMetadata / UiPropertyMetadata; auto-infers control type from CLR type (bool→CheckBox, enum→DropDown, numerics→UpDown, string→TextBox).

Typed accessor delegates on UiPropertyMetadata

UiPropertyMetadata now exposes four additional fields that eliminate PropertyInfo reflection from all call-site code (renderers, condition evaluators):

Field Type Purpose
PropertyType Type? The CLR type — e.g. typeof(bool), typeof(AccentColorOption)
Getter Func<IIniSection, object?>? Reads the property value without PropertyInfo.GetValue at the call site
Setter Action<IIniSection, object?>? Writes a value back with implicit coercion (string→enum, Convert.ChangeType for numerics)
EnumNames string[]? Enum member names for DropDown controls — no Enum.GetNames at the call site

UiMetadataReader.ReadPage always populates these fields (reflection is hidden once inside lambda closures). The source generator emits concrete typed lambda expressions at compile time so descriptors used with source generation are 100% reflection-free at runtime.

New project: src/Dapplo.Ini.Ui.Generator/ (Roslyn source generator)

For every IIniSection interface that carries at least one UI attribute, the generator emits a static {TypeName}UiDescriptor partial class at compile time:

// Auto-generated — no runtime reflection needed
public static partial class NetworkSettingsUiDescriptor
{
    public static UiPageMetadata Page { get; } = new UiPageMetadata
    {
        SectionType = typeof(INetworkSettings),
        Title = "Network",
        Category = "Advanced",
        Order = 10,
        DefaultChangeMode = UiChangeMode.OnConfirm,
        Properties = new ReadOnlyCollection<UiPropertyMetadata>(new[]
        {
            new UiPropertyMetadata
            {
                PropertyName = "UseProxy",
                ControlType = UiControlType.CheckBox,
                PropertyType = typeof(bool),
                Getter = static section => ((INetworkSettings)section).UseProxy,
                Setter = static (section, v) => ((INetworkSettings)section).UseProxy = v is bool __b ? __b : bool.Parse(),
            },
            new UiPropertyMetadata
            {
                PropertyName = "ProxyHost",
                ControlType = UiControlType.TextBox,
                GroupName = "Proxy",
                VisibilityConditionProperty = "UseProxy",
                PropertyType = typeof(string),
                Getter = static section => ((INetworkSettings)section).ProxyHost,
                Setter = static (section, v) => ((INetworkSettings)section).ProxyHost = Convert.ToString(v) ?? string.Empty,
            },
            new UiPropertyMetadata
            {
                PropertyName = "AccentColor",
                ControlType = UiControlType.DropDown,
                PropertyType = typeof(AccentColorOption),
                Getter = static section => ((IAppearanceSettings)section).AccentColor,
                Setter = static (section, v) => ((IAppearanceSettings)section).AccentColor = v is AccentColorOption __e ? __e : (AccentColorOption)Enum.Parse(),
                EnumNames = new[] { "Blue", "Green", "Orange", "Purple" },
            },
        })
    };
}

Control type is inferred from the CLR property type when no explicit [UiControl] is present (bool→CheckBox, enum→DropDown, numerics→UpDown, string→TextBox). The descriptor is AOT/linker-friendly and equivalent to calling UiMetadataReader.ReadPage<T>() without the reflection cost.

New project: demos/Dapplo.Ini.Ui.DemoApp/ (WPF application, net10.0-windows)

A runnable WPF demo application that shows the complete end-to-end workflow:

Three annotated IIniSection interfaces

Interface Demonstrates
IGeneralSettings TextBox, CheckBox, DropDown (enum → auto-populated ComboBox); Immediate change mode
INetworkSettings Conditional visibility/enable — proxy fields hidden/disabled until "Use proxy" is ticked; ITransactional + OnConfirm so Cancel rolls back all edits
IAppearanceSettings Slider with unit label; FontSize disabled when DarkMode is on (inverted enable condition)

WpfSettingsRenderer — the core renderer class:

  • Takes a *UiDescriptor.Page (generated at compile time by Dapplo.Ini.Ui.Generator) and the live IIniSection instance.
  • Dynamically builds all WPF controls (CheckBox, TextBox, ComboBox, Slider, UpDown spinner, FolderPicker) — no hand-written XAML per section.
  • Groups properties with a shared GroupName inside a GroupBox.
  • Subscribes to INotifyPropertyChanged on the section and re-evaluates all conditional visibility/enable rules reactively on every property change.
  • Uses meta.Getter / meta.Setter / meta.EnumNames exclusively — System.Reflection import removed; a gettersByName dictionary built once in BuildPage provides O(1) condition lookup.

SettingsWindow — a TabControl (left-side tabs, one per [UiPage]) with OK/Cancel/Apply buttons wired to UiSettingsManager.BeginSession() / Apply() / EndSession().

MainWindow — shows the current setting values and opens the settings window via:

new SettingsWindow(new[]
{
    (GeneralSettingsUiDescriptor.Page,    (IIniSection)_general),
    (AppearanceSettingsUiDescriptor.Page, (IIniSection)_appearance),
    (NetworkSettingsUiDescriptor.Page,    (IIniSection)_network),
});

Settings are loaded from and saved to %APPDATA%\DappIniDemo\settings.ini via IniConfigRegistry.

To run the demo on Windows:

dotnet run --project demos/Dapplo.Ini.Ui.DemoApp/

The project cross-compiles on Linux CI via <EnableWindowsTargeting>true</EnableWindowsTargeting>.

Full workflow example

[IniSection("Network")]
[UiPage(Title = "Network", Category = "Advanced", Order = 10)]
[UiChangeMode(UiChangeMode.OnConfirm)]
public interface INetworkSettings : IIniSection, ITransactional, INotifyPropertyChanged
{
    bool UseProxy { get; set; }

    [UiGroup("Proxy Settings")]
    [UiConditionalVisibility(nameof(UseProxy))]
    [UiConditionalEnable(nameof(UseProxy))]
    string ProxyHost { get; set; }

    [UiGroup("Proxy Settings")]
    [UiControl(UiControlType.UpDown, Minimum = 1, Maximum = 65535)]
    [UiConditionalVisibility(nameof(UseProxy))]
    [UiConditionalEnable(nameof(UseProxy))]
    int ProxyPort { get; set; }
}

// UI renderer consumes the compile-time descriptor:
UiPageMetadata page = NetworkSettingsUiDescriptor.Page;

// WPF: build controls dynamically — no XAML required
UIElement panel = WpfSettingsRenderer.BuildPage(page, networkSettings);

// Settings session (apply / rollback):
var mgr = new UiSettingsManager();
mgr.RegisterSection(networkSettings);
mgr.BeginSession();
// … user edits …
mgr.Apply();   // or mgr.EndSession() on cancel (auto-rollback)

Properties that come from a constants file (IsConstant(key) == true) are always rendered disabled regardless of [UiConditionalEnable].

Bug fix: UnauthorizedAccessException on first run

App.xaml.cs previously called SetWritablePath(settingsDir) with a directory path, but IniConfigBuilder.SetWritablePath expects an absolute path to the INI file. On first run (when settings.ini does not yet exist), IniConfig.Load() stored the directory path as LoadedFromPath, causing SaveAsync() to attempt to open the directory as a file on application exit → System.UnauthorizedAccessException. The call has been removed; AddSearchPath(settingsDir) alone is sufficient and the load fallback correctly computes Path.Combine(settingsDir, "settings.ini") as the write target.

Demo app: modern design, dark/light theme, and chromeless titlebar

The demo application has been updated with:

  • Dark / light theme supportThemes/LightTheme.xaml and Themes/DarkTheme.xaml contain Windows 11 Fluent-inspired color tokens and implicit control styles (Button with CornerRadius="4", TextBox with focus highlight, GroupBox as a surface card, TabControl/TabItem as a left-nav strip, Separator, Slider). Theme/ThemeManager.cs swaps the last MergedDictionaries entry at runtime so all DynamicResource bindings across every open window update atomically. The theme switches live while the settings dialog is open — checking the Dark Mode checkbox applies the theme immediately without closing and re-opening the dialog.
  • Chromeless titlebar — Both MainWindow and SettingsWindow use WindowStyle="None" + System.Windows.Shell.WindowChrome (CaptionHeight=40, ResizeBorderThickness=5). OS window management — drag-to-move, edge/corner resize, Aero Snap, snap layouts, double-click to maximize — is fully preserved. MainWindow has Minimize / Maximize-Restore / Close chrome buttons; SettingsWindow has a single Close (×) button that routes through OnClosing for proper session rollback.
  • Modern design — the main window uses a rounded surface card (CornerRadius="8") for the settings overview, typed subtext labels for visual hierarchy, and Segoe UI applied globally via the implicit Window style in the theme dictionary.
  • Session lifecycle fixSettingsWindow now uses an EndSessionOnce() guard to prevent double EndSession() calls when OK/Cancel set DialogResult and OnClosing fires. After OK, Apply() is followed by EndSessionOnce() (safe — HasPendingChanges is false after a commit, so no rollback occurs).

Type of change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update only

Documentation

  • README.md — feature bullet list, table of contents, relevant section(s), and Registry API reference table updated
  • docs/wiki/Home.md — new wiki page added to the table (if a new page was created)
  • Relevant docs/wiki/*.md page(s) updated (e.g. Loading-Configuration.md, Registry-API.md, Singleton-and-DI.md)
  • New docs/wiki/<Feature>.md page created for significant new features
  • No documentation changes needed (explain why below)

docs/wiki/Ui-Configuration-Framework.md has been added, covering the full end-to-end workflow: annotating interfaces → source-generator output → translated labels via LanguageConfigUiSettingsManager session lifecycle → IUiControlEvents partial-class hooks → control-type inference table → assembly-level layout → runtime metadata reader as fallback → WPF renderer pattern.

Tests

  • New tests added that cover the change
  • All existing tests pass (dotnet test tests/Dapplo.Ini.Tests/)

New test project tests/Dapplo.Ini.Ui.Tests/ adds 51 tests:

  • 32 tests covering all attributes, UiMetadataReader, UiSettingsManager, and IUiControlEvents.
  • 19 tests verifying the source-generator output (*UiDescriptor.Page) for correct title, category, order, change mode, control types, group names, conditional properties, i18n keys, and the new PropertyType / Getter / Setter / EnumNames fields — including a cross-check that each generated descriptor is equivalent to the runtime metadata reader.

Checklist

  • Code follows the existing style conventions (see existing files for reference)
  • XML doc-comments added/updated on all new public API members
  • Breaking changes listed in the description above

Copilot AI linked an issue Apr 14, 2026 that may be closed by this pull request
@CLAassistant
Copy link
Copy Markdown

CLAassistant commented Apr 14, 2026

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
1 out of 2 committers have signed the CLA.

✅ Lakritzator
❌ Copilot
You have signed the CLA already but the status is still pending? Let us recheck it.

Copilot AI and others added 2 commits April 14, 2026 12:08
… interfaces, and tests

Agent-Logs-Url: https://github.com/dapplo/Dapplo.Ini/sessions/e5c1b5d9-0589-4099-91da-5b4aa1e8eeb5

Co-authored-by: Lakritzator <708125+Lakritzator@users.noreply.github.com>
Copilot AI changed the title [WIP] Create an UI configuration framework Create Dapplo.Ini.Ui — UI configuration framework for annotated INI section interfaces Apr 14, 2026
Copilot AI requested a review from Lakritzator April 14, 2026 12:13
@Lakritzator
Copy link
Copy Markdown
Member

@copilot Do I understand this correctly, this is just a meta-model?

How do I put together a set of tabs with one or more sections, have translated texts and radio / checkboxes etc on them, react to changes? Can this be generated via a source generator?

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 14, 2026

@copilot Do I understand this correctly, this is just a meta-model?

How do I put together a set of tabs with one or more sections, have translated texts and radio / checkboxes etc on them, react to...

Yes, the previous commit was just the foundational meta-model (attributes + runtime metadata reader + settings manager). The meta-model on its own is not sufficient for building a UI.

This follow-up commit (da4b343) adds the source generator (Dapplo.Ini.Ui.Generator) that is the key missing piece. Here is now the full picture:

What the source generator produces

For every IIniSection interface that carries at least one UI attribute, the generator emits a static descriptor class at compile time — no runtime reflection needed:

// Auto-generated for INetworkSettings
public static partial class NetworkSettingsUiDescriptor
{
    public static UiPageMetadata Page { get; } = new UiPageMetadata
    {
        SectionType = typeof(INetworkSettings),
        Title = "Network",
        Category = "Advanced",  // tree-nav parent node
        Order = 10,
        DefaultChangeMode = UiChangeMode.OnConfirm,
        Properties = new ReadOnlyCollection<UiPropertyMetadata>(new[]
        {
            new UiPropertyMetadata { PropertyName = "UseProxy",   ControlType = CheckBox,},
            new UiPropertyMetadata { PropertyName = "ProxyHost",  ControlType = TextBox,
                GroupName = "Proxy", VisibilityConditionProperty = "UseProxy",},
            new UiPropertyMetadata { PropertyName = "ProxyPort",  ControlType = UpDown,},
        })
    };
}

How to assemble the settings window

A UI renderer (Avalonia, WPF, etc.) iterates *UiDescriptor.Page.Properties, renders the correct control for each ControlType, and wires VisibilityConditionProperty / EnableConditionProperty bindings. The full workflow (tabs → groups → controls → translations → apply/rollback) is documented in the new docs/wiki/Ui-Configuration-Framework.md.

Translated labels

[UiLabelKey("proxy_host_label")] stores the i18n key in the descriptor; the UI renderer resolves it via LanguageConfig at render time.

React to changes

Implement IUiControlEvents in a partial class alongside the generated *Impl:

public partial class NetworkSettingsImpl : IUiControlEvents
{
    public void OnPropertyChanged(string prop, object? oldVal, object? newVal)
    {
        if (prop == nameof(INetworkSettings.UseProxy) && newVal is true)
            ProxyHost ??= Environment.GetEnvironmentVariable("HTTP_PROXY") ?? "";
    }
}

The concrete UI renderer (WPF/Avalonia) is out of scope for this library, but the descriptor + lifecycle interfaces are sufficient for any renderer to consume. A demo application would be the natural next step once a target UI framework is chosen.

@Lakritzator
Copy link
Copy Markdown
Member

@copilot in the previous comment you said "The concrete UI renderer (WPF/Avalonia) is out of scope for this library", why would it be out of scope?

Can you provide a demo app which uses WPF?
Project could be called Dapplo.Ini.Ui.DemoApp

…n framework

Agent-Logs-Url: https://github.com/dapplo/Dapplo.Ini/sessions/71f31233-8ea0-4586-a540-2f511fc99b8d

Co-authored-by: Lakritzator <708125+Lakritzator@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 14, 2026

@copilot in the previous comment you said "The concrete UI renderer (WPF/Avalonia) is out of scope for this library", why would it be out of scope?

Can you provide a demo app which uses WPF?
...

Done — commit 1b11f96 adds demos/Dapplo.Ini.Ui.DemoApp/, a self-contained WPF application that shows the complete end-to-end workflow.

What's included

File Purpose
Configuration/IGeneralSettings.cs TextBox, CheckBox, DropDown (enum → auto-populated ComboBox)
Configuration/INetworkSettings.cs Conditional visibility/enable — Proxy fields hidden/disabled until "Use proxy" is checked; ITransactional + OnConfirm so Cancel rolls back
Configuration/IAppearanceSettings.cs Slider with unit label; FontSize disabled when DarkMode is on (inverted condition)
Renderer/WpfSettingsRenderer.cs The core renderer — takes a *UiDescriptor.Page (generated at compile time) + the live section instance and dynamically builds all WPF controls; no hand-written XAML per section
Windows/SettingsWindow.xaml/.cs TabControl with left-side tabs, OK/Cancel/Apply wired to UiSettingsManager
MainWindow.xaml/.cs Shows current values; "Open Settings…" passes the compile-time *UiDescriptor.Page objects to the settings window
App.xaml.cs Builds IniConfig via IniConfigRegistry, loads on start, saves on exit

How to run on Windows

dotnet run --project demos/Dapplo.Ini.Ui.DemoApp/

Settings persist to %APPDATA%\DappIniDemo\settings.ini.

The project cross-compiles on Linux CI (EnableWindowsTargeting=true) and all 465 existing tests continue to pass.

Copilot AI changed the title Create Dapplo.Ini.Ui — UI configuration framework for annotated INI section interfaces Create Dapplo.Ini.Ui — UI configuration framework with WPF demo app Apr 14, 2026
@Lakritzator
Copy link
Copy Markdown
Member

@copilot While trying the demo, I got the following error in the Dapplo.Ini existing code.
It could have to do with using .NET 10 instead of .NET Framework?
It is reproduceble....

It happened after I clicked on the close window button:
System.UnauthorizedAccessException
at Microsoft.Win32.SafeHandles.SafeFileHandle.CreateFile(String fullPath, FileMode mode, FileAccess access, FileShare share, FileOptions options)
at Microsoft.Win32.SafeHandles.SafeFileHandle.Open(String fullPath, FileMode mode, FileAccess access, FileShare share, FileOptions options, Int64 preallocationSize, Nullable`1 unixCreateMode)
at System.IO.File.OpenHandle(String path, FileMode mode, FileAccess access, FileShare share, FileOptions options, Int64 preallocationSize)
at System.IO.File.d__122.MoveNext()
at Dapplo.Ini.Parsing.IniFileWriter.d__1.MoveNext() in D:\code\Dapplo.Ini\src\Dapplo.Ini\Parsing\IniFileWriter.cs:line 28
at Dapplo.Ini.IniConfig.d__56.MoveNext() in D:\code\Dapplo.Ini\src\Dapplo.Ini\Configuration\IniConfig.cs:line 367
at Dapplo.Ini.Ui.DemoApp.App.OnExit(ExitEventArgs e) in D:\code\Dapplo.Ini\demos\Dapplo.Ini.Ui.DemoApp\App.xaml.cs:line 81
at System.Windows.Application.DoShutdown()
at System.Windows.Application.ShutdownImpl()
at System.Windows.Application.ShutdownCallback(Object arg)
at System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs)
at System.Windows.Threading.ExceptionWrapper.TryCatchWhen(Object source, Delegate callback, Object args, Int32 numArgs, Delegate catchHandler)
at System.Windows.Threading.DispatcherOperation.InvokeImpl()
at MS.Internal.CulturePreservingExecutionContext.CallbackWrapper(Object obj)
at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
--- End of stack trace from previous location ---
at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
at System.Windows.Threading.DispatcherOperation.Invoke()
at System.Windows.Threading.Dispatcher.ProcessQueue()
at MS.Win32.HwndWrapper.WndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
at System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs)
at System.Windows.Threading.ExceptionWrapper.TryCatchWhen(Object source, Delegate callback, Object args, Int32 numArgs, Delegate catchHandler)
at MS.Win32.HwndSubclass.SubclassWndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam)
at MS.Win32.UnsafeNativeMethods.DispatchMessage(MSG& msg)
at System.Windows.Threading.Dispatcher.PushFrameImpl(DispatcherFrame frame)
at System.Windows.Application.RunDispatcher(Object ignore)
at System.Windows.Application.RunInternal(Window window)
at Dapplo.Ini.Ui.DemoApp.App.Main()

…tablePath(dir) call

Agent-Logs-Url: https://github.com/dapplo/Dapplo.Ini/sessions/588c24b5-0260-4078-bbc0-629f51e1f09c

Co-authored-by: Lakritzator <708125+Lakritzator@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 14, 2026

@copilot While trying the demo, I got the following error in the Dapplo.Ini existing code.
It could have to do with using .NET 10 instead of .NET Framework?
It is reproduceble....
...

Fixed in commit e8ae2ad.

Root cause: SetWritablePath(settingsDir) was receiving a directory path, but the API expects an absolute file path. On first run (no existing settings.ini), IniConfig.Load() used that directory path as LoadedFromPath, so SaveAsync() tried to write to the directory itself → UnauthorizedAccessException.

The fix removes SetWritablePath(settingsDir)AddSearchPath(settingsDir) alone is sufficient; when the file doesn't exist yet, IniConfig correctly falls back to Path.Combine(firstWritable, "settings.ini") as the write target.

@Lakritzator
Copy link
Copy Markdown
Member

@copilot Can you add the following to the demo:

  • Make it support dark/light theme
  • Use a more modern design
  • Have the titlebar use the same color as the rest of the window, I believe it's called chromeless?

…hrome titlebar

Agent-Logs-Url: https://github.com/dapplo/Dapplo.Ini/sessions/88ac4fa1-006c-4d6a-b6a0-d0dc217376f5

Co-authored-by: Lakritzator <708125+Lakritzator@users.noreply.github.com>
…h in tooltips/labels

Agent-Logs-Url: https://github.com/dapplo/Dapplo.Ini/sessions/88ac4fa1-006c-4d6a-b6a0-d0dc217376f5

Co-authored-by: Lakritzator <708125+Lakritzator@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 15, 2026

@copilot Can you add the following to the demo:

  • Make it support dark/light theme
    ...

Done in commit c6f1a34. Here's what was added:

Dark / light themeThemes/LightTheme.xaml and Themes/DarkTheme.xaml contain Windows 11 Fluent-inspired colour tokens and implicit control styles (Button with CornerRadius="4", TextBox with focus highlight, GroupBox as a card, TabControl/TabItem left-nav strip). ThemeManager.Apply(darkMode) swaps the last MergedDictionaries entry so all DynamicResource bindings in every open window update atomically. The theme switches live while the settings dialog is open — checking the Dark Mode checkbox applies the theme immediately without closing and re-opening anything.

Chromeless titlebar — both MainWindow and SettingsWindow use WindowStyle="None" + System.Windows.Shell.WindowChrome (40 px caption height, 5 px resize border). This keeps OS window management — drag-to-move, edge/corner resize, Aero Snap, double-click to maximise — while painting the titlebar in the same TitleBarBg brush as the window background. The Minimize / Maximize / Close buttons in MainWindow and the Close button in SettingsWindow have IsHitTestVisibleInChrome="True" and use keyed TitleBarButtonStyle / TitleBarCloseButtonStyle with the standard red hover on Close.

Modern design — the main window now uses a surface card (CornerRadius="8") for the settings overview, typed subtext labels, and the Segoe UI font set via the implicit Window style.

@Lakritzator
Copy link
Copy Markdown
Member

@copilot I see a lot of reflection being used, is it possible to get rid of it, by enhancing the current API? For example to provide extra functions for the missing meta-data model and/or access to values / types etc?

…typed delegates in UiPropertyMetadata

- UiPropertyMetadata: add PropertyType, Getter, Setter, EnumNames
- UiMetadataReader: populate all four fields; reflection hidden in lambda closures
- UiSectionGenerator: emit PropertyType=typeof(T), typed Getter/Setter lambdas and
  EnumNames[] at compile time; add BuildSetterExpression() for bool/string/enum/numeric
- WpfSettingsRenderer: drop System.Reflection import; use meta.Getter / meta.Setter /
  meta.EnumNames; build gettersByName dict for O(1) condition evaluation; wrap
  condition getter calls in try-catch to prevent UI crashes on misconfigured sections

0 warnings, 0 errors, 420/420 tests pass

Agent-Logs-Url: https://github.com/dapplo/Dapplo.Ini/sessions/7d7441b7-14a3-4606-8645-45731a43251c

Co-authored-by: Lakritzator <708125+Lakritzator@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 15, 2026

@copilot I see a lot of reflection being used, is it possible to get rid of it, by enhancing the current API? For example to provide extra functions for the missing meta-data model and/or access to va...

Done in commit 6d6e72e. The reflection has been pushed down into the metadata layer so call-site code needs none of it.

What changed

UiPropertyMetadata now carries four new fields:

Field Type Purpose
PropertyType Type? The CLR type — e.g. typeof(bool), typeof(AccentColorOption)
Getter Func<IIniSection, object?>? Reads the property value without PropertyInfo.GetValue at the call site
Setter Action<IIniSection, object?>? Writes a value back with implicit coercion (string→enum, Convert.ChangeType for numerics)
EnumNames string[]? Enum member names for DropDown controls — no Enum.GetNames at the call site

Source generator path (compile time — zero reflection at runtime)
The generator now emits concrete lambda expressions into each *UiDescriptor:

PropertyType = typeof(bool),
Getter = static section => ((INetworkSettings)section).UseProxy,
Setter = static (section, v) => ((INetworkSettings)section).UseProxy = v is bool __b ? __b : bool.Parse(),

PropertyType = typeof(AccentColorOption),
Getter = static section => ((IAppearanceSettings)section).AccentColor,
Setter = static (section, v) => ((IAppearanceSettings)section).AccentColor = v is AccentColorOption __e ? __e : (AccentColorOption)Enum.Parse(),
EnumNames = new[] { "Blue", "Green", "Orange", "Purple" },

UiMetadataReader runtime fallback
When source-generated descriptors aren't used, the reflection is hidden inside lambda closures created once at startup — renderers still get a clean Getter/Setter to call.

WpfSettingsRenderer
using System.Reflection and all GetProp/GetValue/SetValue helpers have been removed. Condition evaluation now uses a gettersByName dictionary built once in BuildPage for O(1) lookup. Getter calls are wrapped in try-catch to prevent any misbehaving getter from crashing the UI.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Create an UI configuration framework

3 participants