Unity adoption of Noesis
Posted: 27 Jun 2026, 09:35
Hey,
I am developing with Noesis for almost 4 months now. Through this time i've accumulated war history of adopting it, which I thought might be useful for you to glance over. First, I've reported a few issues that look like genuine problems. However, reports 1, 2, 3 fix probably all collapse to one feature request: a single, package-wide "is the native library present/initialized?" check that every entry point consults, disabling cleanly instead of throwing.
Second, I wanted to share adoption gotchas I've been through - you might find it interesting. Obviously, some or even all of it might come from my lack of experience, solely, and is not a problem at all, but I thought you might want to know :)
Binding & data
I am developing with Noesis for almost 4 months now. Through this time i've accumulated war history of adopting it, which I thought might be useful for you to glance over. First, I've reported a few issues that look like genuine problems. However, reports 1, 2, 3 fix probably all collapse to one feature request: a single, package-wide "is the native library present/initialized?" check that every entry point consults, disabling cleanly instead of throwing.
Second, I wanted to share adoption gotchas I've been through - you might find it interesting. Obviously, some or even all of it might come from my lack of experience, solely, and is not a problem at all, but I thought you might want to know :)
Binding & data
- {Binding Foo} silently fails when Foo is a public field, not a property. Error: Can't solve PropertyPath: Type 'T' does not contain a property named 'Foo'. The path resolver reflects over CLR properties only and skips public fields. WPF tolerates fields in some path-resolution code; the C++ bridge does not — a host-specific divergence. Most likely to bite when exposing a reactive wrapper ({Binding X.Value} where X is a field). Fix: expose anything XAML-bound as a get-only property. The error message points at the wrong layer (says the property doesn't exist when the real issue is field-vs-property) — a one-line doc note would save the hunt.
- Implicit DataTemplate type-match (DataType="{x:Type ...}") is unreliable inside a Popup/ToolTip tree. Tooltip/menu chrome renders, body is blank, no error, VM non-null. Resolving the template by explicit x:Key via StaticResource (ContentTemplate="{StaticResource …}") works; only the implicit type-match step fails across the popup boundary. Useful corollary I verified: {noesis:Loc} does resolve inside the same popup body (it reads an inheritable attached property, which propagates into popups like DataContext/FontSize do) — so the popup boundary breaks ResourceDictionary key lookup, not inheritable-DP propagation. The two get conflated easily.
- RelativeSource AncestorType=ContentPresenter fails silently (3.2); AncestorType=Border works. Surfaced when sizing custom-templated TabControl content to its host. A silent no-op (rather than a resolution error) makes it hard to spot.
- StaticResource cross-file resolution differs between Editor and built client. A StaticResource whose key lives in a sibling ResourceDictionary that isn't explicitly merged resolves in the Editor but throws StaticResource '...' not found in the built client. The Editor flat-merges sibling dictionaries early; the build parses each dictionary in isolation. This Editor-vs-build leniency gap is the biggest "works on my machine" trap in the stack — every cross-file resource reference must be validated in an actual build. A doc note (or an Editor strict-mode mimicking build resolution) would prevent a lot of late surprises.
- A local element-scoped <X.Style> without BasedOn renders magenta. Wrapping a Theme-styled control in an inline <X.Style> (commonly just to add Style.Triggers) replaces the implicit Theme style wholesale — including its Template — and the missing template falls back to a magenta rectangle. Fix: always chain BasedOn="{StaticResource {x:Type X}}". WPF-consistent in principle, but the magenta-on-missing-template failure mode is easy to hit; the tell is "magenta appears exactly when a triggered element becomes visible, siblings of the same type render fine."
- An ItemContainer ControlTemplate without a <ContentPresenter/> renders empty rows. Chrome shows, content doesn't. Easy to forget when fully restyling a ListBox item container.
- Nested ListBox loses the outer selection highlight when the inner one is clicked. Focus moves inward, the outer item goes Selected -> SelectedUnfocused, and the default SelectedUnfocused visual state has an empty storyboard. Fix: give SelectedUnfocused the same storyboard as Selected at each nesting level. The empty default is the surprise.
- ScrollViewer default control template clipped content under the scrollbar via Grid.ColumnSpan/RowSpan="2" on the ScrollContentPresenter (rightmost ~17px painted under the bar). Removing the spans restores stock WPF layout (the Auto column is reserved for the bar). Flagging in case the shipped default template still carries the spans.
- An armed tooltip show-timer caches IsEnabled, InitialShowDelay, and the ToolTip value at MouseEnter (arm time); later writes are ignored. So ToolTipService.IsEnabled="{Binding …}" (or any ToolTipService.Set*) flipped after hover-in but before the delay elapses does nothing — the tooltip still pops, e.g. mid-drag or over a just-opened ContextMenu. The only lever that works on an already-armed timer is collapsing the cached ToolTip element's own Visibility. Compounding it: ToolTipService exposes no events, and ToolTipOpening/ToolTipClosing don't bubble out of the popup tree, so the open can't be cleanly intercepted. (Related: calling SetToolTip(null) on an inline-declared ToolTip throws 'ToolTip' cannot have logical or visual parent — but that's WPF-consistent parse-time parenting.)
- Native popups (ToolTip, ContextMenu) render at fixed pixel size under a root LayoutTransform scale — because a popup is a separate top-level visual tree the root transform can't reach (this matches WPF, so the non-scaling itself is expected). The gotcha worth sharing is the fix constraint: the popup's own compensating scale must use LayoutTransform, not RenderTransform — RenderTransform scales after rasterization and visibly blurs tooltip text at high DPI. Anyone scaling a whole UI for DPI hits the popup-doesn't-scale wall, then the blur trap, in sequence.
- Custom cursors live in Noesis.settings.asset, not in XAML/code, and the slot list is the fixed WPF CursorType enum. Declaring Cursor="SizeAll" (or any type) whose settings slot has no texture assigned silently falls back to the OS arrow — SetCursor(null), no error. There is no Custom/filename branch, so a custom .cur name in XAML matches nothing; you must repurpose a built-in slot. Under-documented; the silent arrow fallback reads as "my cursor XAML is broken" when the slot is just empty.
- UnityEngine.Cursor.SetCursor(null) from your own C# (for cursors driven by non-Noesis hover, e.g. 3D-world hover) shows the OS arrow, not your themed arrow. Noesis only re-applies its configured cursor when the hovered Noesis element changes; moving off a world object onto empty space isn't a Noesis-element change, so it never re-fires and the OS arrow sticks. You must cache and re-apply the themed arrow texture yourself. A note that "Noesis owns the cursor only while over Noesis elements; you own it elsewhere, and the two don't hand off automatically" would help anyone mixing Noesis UI with 3D/world hover cursors.
- Loaded fires before the first measure/arrange, so ActualWidth/ActualHeight are still 0 inside a Loaded handler — IsLoaded == true and ActualWidth == 0 simultaneously. This diverges from WPF silently (WPF has layout done by Loaded). The layout pass runs later (in the view's LateUpdate/Update). Fix: read size in SizeChanged (self-unsubscribe on first non-zero), not Loaded.
- A window shown by flipping Visibility never acquires keyboard/logical focus, so input that depends on a focused element tree (notably the first MouseDown starting a DragDrop.DoDragDrop modal loop) is routed to focus-acquisition instead, and "feels dead until you alt-tab away and back" (which re-focuses the view). Fix: explicitly claim focus on show — set the root Focusable = true (Grid/Border roots are non-focusable by default, so Focus() no-ops otherwise), then Focus(), deferring one retry to Loaded if not yet loaded. Worth sharing because the symptom ("drag works only after alt-tab") is baffling until you connect it to focus.
- Noesis Studio auto-generates its xmlns prefix from the project name, which collides with NoesisGUIExtensions if the project name starts with Noesis. Studio then renames the extensions namespace to noesis1, and DataType resolution + noesis:Loc markup break in some files. Fix: don't name the Studio project with a Noesis/noesis prefix. A reserved-prefix warning in Studio would prevent this.
- Studio treats an inline <UserControl> (used purely as a resource-scoping wrapper) as an opaque component boundary and can't drill into the wrapped element's child templates in the Navigator. Using a .Resources block for key isolation instead keeps templates editable. Also: <Component> appears in Noesis samples but is Studio-only and doesn't exist in the Unity 3.2 runtime — copying it from a sample breaks the build.