Create Dapplo.Ini.Ui — UI configuration framework with WPF demo app#98
Create Dapplo.Ini.Ui — UI configuration framework with WPF demo app#98
Conversation
|
|
… 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>
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 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? |
…tation Agent-Logs-Url: https://github.com/dapplo/Dapplo.Ini/sessions/d36be111-d551-4928-8e50-3b25788fa347 Co-authored-by: Lakritzator <708125+Lakritzator@users.noreply.github.com>
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 ( What the source generator produces For every // 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 Translated labels
React to changes Implement 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. |
|
@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? |
…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>
Done — commit What's included
How to run on Windows dotnet run --project demos/Dapplo.Ini.Ui.DemoApp/Settings persist to The project cross-compiles on Linux CI ( |
|
@copilot While trying the demo, I got the following error in the Dapplo.Ini existing code. It happened after I clicked on the close window button: |
…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>
Fixed in commit Root cause: The fix removes |
|
@copilot Can you add the following to the demo:
|
…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>
Done in commit Dark / light theme — Chromeless titlebar — both Modern design — the main window now uses a surface card ( |
|
@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>
Done in commit What changed
Source generator path (compile time — zero reflection at runtime) 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" },
|
Description
Introduces
Dapplo.Ini.Ui,Dapplo.Ini.Ui.Generator, andDapplo.Ini.Ui.DemoApp— a trio of new projects that let developers declaratively describe a settings UI via attributes on their existingIIniSectioninterfaces, 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/(targetsnet10.0)Attributes
[UiControl(UiControlType.Slider, Minimum=0, Maximum=100)][UiGroup("Proxy", Order=5)][UiPage(Title="Network", Category="Advanced", Order=10)][UiLayout(UiTabLayout.VerticalTabs)][UiConditionalVisibility(nameof(UseProxy))]boolproperty (supportsInvert)[UiConditionalEnable(nameof(UseProxy))]boolproperty (supportsInvert)[UiLabelKey("proxy_host_label", DescriptionKey="proxy_host_tip")][UiChangeMode(UiChangeMode.OnConfirm)][UiOrder(20)]Interfaces & runtime
IUiSettingsManager—BeginSession/Apply/Rollback/EndSessionlifecycle;UiSettingsManagerdelegates toITransactionalfor 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 intoUiPageMetadata/UiPropertyMetadata; auto-infers control type from CLR type (bool→CheckBox,enum→DropDown, numerics→UpDown,string→TextBox).Typed accessor delegates on
UiPropertyMetadataUiPropertyMetadatanow exposes four additional fields that eliminatePropertyInforeflection from all call-site code (renderers, condition evaluators):PropertyTypeType?typeof(bool),typeof(AccentColorOption)GetterFunc<IIniSection, object?>?PropertyInfo.GetValueat the call siteSetterAction<IIniSection, object?>?Convert.ChangeTypefor numerics)EnumNamesstring[]?DropDowncontrols — noEnum.GetNamesat the call siteUiMetadataReader.ReadPagealways 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
IIniSectioninterface that carries at least one UI attribute, the generator emits a static{TypeName}UiDescriptorpartial class at compile time: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 callingUiMetadataReader.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
IIniSectioninterfacesIGeneralSettingsComboBox); Immediate change modeINetworkSettingsITransactional+OnConfirmso Cancel rolls back all editsIAppearanceSettingsSliderwith unit label;FontSizedisabled whenDarkModeis on (inverted enable condition)WpfSettingsRenderer— the core renderer class:*UiDescriptor.Page(generated at compile time byDapplo.Ini.Ui.Generator) and the liveIIniSectioninstance.GroupNameinside aGroupBox.INotifyPropertyChangedon the section and re-evaluates all conditional visibility/enable rules reactively on every property change.meta.Getter/meta.Setter/meta.EnumNamesexclusively —System.Reflectionimport removed; agettersByNamedictionary built once inBuildPageprovides O(1) condition lookup.SettingsWindow— aTabControl(left-side tabs, one per[UiPage]) with OK/Cancel/Apply buttons wired toUiSettingsManager.BeginSession()/Apply()/EndSession().MainWindow— shows the current setting values and opens the settings window via:Settings are loaded from and saved to
%APPDATA%\DappIniDemo\settings.iniviaIniConfigRegistry.To run the demo on Windows:
The project cross-compiles on Linux CI via
<EnableWindowsTargeting>true</EnableWindowsTargeting>.Full workflow example
Properties that come from a constants file (
IsConstant(key) == true) are always rendered disabled regardless of[UiConditionalEnable].Bug fix:
UnauthorizedAccessExceptionon first runApp.xaml.cspreviously calledSetWritablePath(settingsDir)with a directory path, butIniConfigBuilder.SetWritablePathexpects an absolute path to the INI file. On first run (whensettings.inidoes not yet exist),IniConfig.Load()stored the directory path asLoadedFromPath, causingSaveAsync()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 computesPath.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:
Themes/LightTheme.xamlandThemes/DarkTheme.xamlcontain Windows 11 Fluent-inspired color tokens and implicit control styles (Button withCornerRadius="4", TextBox with focus highlight, GroupBox as a surface card, TabControl/TabItem as a left-nav strip, Separator, Slider).Theme/ThemeManager.csswaps the lastMergedDictionariesentry at runtime so allDynamicResourcebindings 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.MainWindowandSettingsWindowuseWindowStyle="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.MainWindowhas Minimize / Maximize-Restore / Close chrome buttons;SettingsWindowhas a single Close (×) button that routes throughOnClosingfor proper session rollback.CornerRadius="8") for the settings overview, typed subtext labels for visual hierarchy, and Segoe UI applied globally via the implicitWindowstyle in the theme dictionary.SettingsWindownow uses anEndSessionOnce()guard to prevent doubleEndSession()calls when OK/Cancel setDialogResultandOnClosingfires. After OK,Apply()is followed byEndSessionOnce()(safe —HasPendingChangesis false after a commit, so no rollback occurs).Type of change
Documentation
README.md— feature bullet list, table of contents, relevant section(s), and Registry API reference table updateddocs/wiki/Home.md— new wiki page added to the table (if a new page was created)docs/wiki/*.mdpage(s) updated (e.g.Loading-Configuration.md,Registry-API.md,Singleton-and-DI.md)docs/wiki/<Feature>.mdpage created for significant new featuresdocs/wiki/Ui-Configuration-Framework.mdhas been added, covering the full end-to-end workflow: annotating interfaces → source-generator output → translated labels viaLanguageConfig→UiSettingsManagersession lifecycle →IUiControlEventspartial-class hooks → control-type inference table → assembly-level layout → runtime metadata reader as fallback → WPF renderer pattern.Tests
dotnet test tests/Dapplo.Ini.Tests/)New test project
tests/Dapplo.Ini.Ui.Tests/adds 51 tests:UiMetadataReader,UiSettingsManager, andIUiControlEvents.*UiDescriptor.Page) for correct title, category, order, change mode, control types, group names, conditional properties, i18n keys, and the newPropertyType/Getter/Setter/EnumNamesfields — including a cross-check that each generated descriptor is equivalent to the runtime metadata reader.Checklist