Deviations from the XML Loader
Note
This article is written for UIExtenderEx maintainers and core contributors. It catalogs all intentional behavioral differences where compiled C# prefabs diverge from Gauntlet's runtime XML loader. For the triage rules governing how differences are approved, see Handling Discrepancies from the XML Loader. For differences between UIExtenderEx and TaleWorlds' offline code generator, see Code Generator.
Overview
A compiled prefab replicates the behavior of Gauntlet UI's runtime XML loader—including its asserts, non-fatal quirks, and failure modes—everywhere except the documented cases below.
Each deviation represents a scenario where the XML loader either produces corrupted UI state or crashes on benign developer mistakes, and the compiled prefab intentionally executes the correct behavior. Every deviation is pinned by automated tests covering both the XML behavior and the compiled behavior.
Catalog of Intentional Deviations
1. Replaced Child ViewModel Updates Every Bound Widget
- XML Behavior: When a parent ViewModel replaces a child ViewModel instance,
GauntletViewonly refreshes views directly nested beneath the view bound to that parent. Any widget that accesses the child ViewModel through a sibling scope (e.g.,DataSource="{..\Visual}"under an adjacent"{Hint}"scope) fails to update and continues displaying stale data from the previous ViewModel instance. - Compiled Behavior: All widgets bound to paths traversing the replaced child ViewModel are rebound to the new instance. Widgets whose bound values remain identical are reassigned without side effects (only internal brush
Versioncounters reflect the assignment). - Rationale: Retaining stale data on screen is a silent failure that corrupts UI state.
- Roslyn Analyzer: UIX0026 flags such sibling-traversing data source expressions as compile-time errors.
- Test Pinning: Verified by
BindingLoaderOracle.IntendedDeviations["stale child ViewModel"].
2. Child in Logical Children Location Resolves Parameters in Declaring Scope
XML Behavior: When a widget passes child elements to a prefab that defines a
<LogicalChildrenLocation />,WidgetTemplate.CreateWidgetsmoves those children under the target prefab's internal container. WhenSetAttributessubsequently evaluates parameters, it resolves*Parameterexpressions against the receiving prefab's parameters rather than the declaring prefab's parameters. Consequently, any parameter passed into the declaring prefab is ignored, falling back to default values:<!-- MyPanel.xml --> <Parameters><Parameter Name="Title" DefaultValue="Untitled" /></Parameters> <Window> <WindowFrame> <!-- Contains <LogicalChildrenLocation /> --> <Children><TextWidget Text="*Title" /></Children> </WindowFrame> </Window> <!-- Usage in another prefab: XML renders "Untitled", Compiled renders "Recruits" --> <MyPanel Parameter.Title="Recruits" />Compiled Behavior: Parameter references (
*Parameter) are consistently resolved against the parameters of the prefab in which the widget was declared.Rationale: In the XML loader, whether a prefab's parameters reach its own widgets depends entirely on the internal implementation details of child prefabs. Introducing a content slot into a prefab silently breaks all consuming widgets.
Roslyn Analyzer: UIX0027 flags parameter lookups inside logical children locations as compile-time errors and suggests explicitly forwarding them.
Test Pinning: Verified by
FuzzFindingTests.APassedChildsParameter_InALogicalChildrenLocation_IsResolvedWhereItIsDeclared.
3. Incompatible Bound Property Value Assigns Nothing on Initial Load
- XML Behavior: If a bound ViewModel property cannot be converted by
WidgetExtensions.ConvertObjector widened via reflection (e.g., binding anintproperty to astringwidget property, or astringto anXmlElement),MethodInfo.Invokethrows anArgumentExceptionduring initial movie binding, aborting screen loading. - Compiled Behavior: The initial binding pass skips the assignment, allowing the widget property to retain its XML default value. Subsequent notifications carrying incompatible payloads route through
SetWidgetAttributeand throw as XML does. Types that might be assignable at runtime (object, unsealed classes, or interfaces) are always evaluated dynamically. - Rationale: Crashing an entire screen and locking the player out of gameplay over a single non-critical binding mistake is unacceptable UI behavior.
- Roslyn Analyzer: UIX0025 reports incompatible type bindings as compile-time errors.
- Test Pinning: Verified by
XmlLoaderParityTests.APairNeitherConvertsNorWidens_IsNotAssignedAtAll.
4. Incompatible Widget Value Is Not Written Back to ViewModel
- XML Behavior: In two-way bindings,
GauntletView.OnViewPropertyChangedpasses the widget's announced value unconverted toViewModel.SetPropertyValue. When the ViewModel property cannot accept the type (e.g., aBrushWidgetannouncing aBrushbound to astringproperty), reflection throws anArgumentExceptionevery time the widget property changes. The ViewModel value does not update, and the exception disrupts execution. - Compiled Behavior: The write-back assignment is skipped when types are demonstrably incompatible. Null values are written back normally, numeric conversions are handled via casting, and flexible types (
object, interfaces) are assigned dynamically. - Rationale: An unhandled exception firing continuously on UI changes without changing state is a framework defect.
- Roslyn Analyzer: UIX0025 flags incompatible two-way write-back bindings at compile time.
- Test Pinning: Verified by
NeverFittingWriteBackTestsandBindingLoaderOracle.IntendedDeviations["value the property can never take not written back"].
5. Widget.Tag Is Initialized to Null
- XML Behavior: The
WidgetTemplateconstructor assignsTag = Guid.NewGuid()on every parse for the benefit of TaleWorlds' internal prefab editor. - Compiled Behavior:
Widget.Tagremainsnull. - Rationale:
Tagis purely internal editor metadata that generates non-deterministic values per parse. TaleWorlds' own pre-compiled assemblies leaveTagasnull. - Roslyn Analyzer: None required (editor-only metadata).
- Test Pinning: Verified by
LoaderOracle.IgnoredProperties["Tag"].
6. Alignment Properties Bound to Enum Properties Function Bi-directionally
- XML Behavior:
Widget.HorizontalAlignmentandVerticalAlignmentannounce change events using string representations of enum values (e.g.,"Top").GauntletView.OnViewPropertyChangedpasses this string directly toViewModel.SetPropertyValue, triggering anArgumentExceptionwhen attempting to assign astringto an enum property. - Compiled Behavior: When an alignment property is bound to an enum ViewModel property, the announced string is parsed back into the appropriate enum value and assigned successfully.
- Rationale: Prevents crashes on two-way alignment bindings while ensuring the ViewModel reflects the active alignment state.
- Roslyn Analyzer: UIX0025 flags the type mismatch at compile time.
- Test Pinning: Verified by
AlignmentBindingPatternTestsandBindingLoaderOracle.IntendedDeviations["alignment name written back"].
7. Floating-Point Numbers Written Back to Integers Are Cast
- XML Behavior: Certain widget properties (such as
Widget.SuggestedWidth) announce values asfloat. If two-way bound to anintViewModel property, reflection rejects the narrowing conversion, throwing anArgumentExceptionon each change. - Compiled Behavior: The announced floating-point value is truncated to an integer using an
uncheckedcast, allowing the binding to complete and synchronize bi-directionally. - Rationale: Eliminates continuous runtime exceptions while maintaining intuitive UI synchronization.
- Roslyn Analyzer: UIX0025 reports narrowing assignments at compile time.
- Test Pinning: Verified by
WriteBackTypeTestsandBindingLoaderOracle.IntendedDeviations["number cast written back"].
8. List Reached Out of Replaced Child Scope Is Not Rebuilt
XML Behavior: Replacing a child ViewModel causes
GauntletViewto refresh all views in its hierarchy. Refreshing a list view destroys and re-instantiates all item widgets. This occurs even when the list is reached via a parent traversal ({..}) back to an unchanged collection on the root ViewModel:<Widget DataSource="{SigilItem}"> <Children> <Widget DataSource="{..}"> <Children> <!-- Rebuilt from scratch in XML when SigilItem changes --> <ListPanel DataSource="{Cultures}" /> </Children> </Widget> </Children> </Widget>Destroying items unnecessarily resets animations, interrupts active user interactions, and leaks gamepad navigation event subscriptions.
Compiled Behavior: Because the collection at
Root\Cultureshas not changed, the list retains its existing widget instances.Rationale: Rebuilding unchanged items is redundant and leaks memory.
Exemption Status: Approved Rule 1 exception without an analyzer. The pattern is used extensively in Native prefabs (
Lobby.Profile,PartyTroopTuple), and prohibiting it would penalize mod authors copying native layouts.Test Pinning: Verified by
ListRebuiltThroughParentScopeTestsandBindingLoaderOracle.IntendedDeviations["list rebuilt through {..}"].
9. List With Path Prefix Sharing Replaced Property Name Is Not Rebuilt
- XML Behavior:
GauntletView.OnPropertyChangedusesBindingPath.IsRelatedWithPathAsString, which performs a plainStartsWithstring check. If a ViewModel replaces propertyPlayer, a sibling view atRoot\PlayerActionsis erroneously treated as related and rebuilt from scratch. - Compiled Behavior: Only views whose data source paths match the modified scope are refreshed.
PlayerActionsremains untouched. - Rationale: Rebuilding sibling collections due to substring collisions is a clear bug in the XML loader's path comparison logic.
- Exemption Status: Approved Rule 1 exception without an analyzer. Native prefabs exhibit this naming pattern (e.g.,
Lobby.Friends). - Test Pinning: Verified by
ListRebuiltThroughANamePrefixTestsandBindingLoaderOracle.IntendedDeviations["list rebuilt through a name prefix"].
10. Disconnected Widgets Do Not Write Back Reset Values
- XML Behavior: When widgets are removed from the hierarchy,
EventManager.OnWidgetDisconnectedFromRootresets navigation properties (GamepadNavigationIndex = -1,UsedNavigationMovements = None,IsUsingNavigation = false). Because Gauntlet's XML loader does not detach property-changed listeners prior to widget removal, these reset values are written back into the ViewModel, corrupting persistent state when a screen closes or an item leaves a list. - Compiled Behavior: Generated classes unsubscribe two-way binding handlers in
DestroyDataSourcebefore disconnecting widgets from the visual tree, preventing reset values from polluting the ViewModel. - Rationale: Corrupting ViewModel state upon screen closing or item deletion is an engine bug.
- Exemption Status: Approved Rule 1 exception without an analyzer. Detecting which widget properties undergo internal engine resets would require hardcoding game-version-specific implementation details into public SDK analyzers.
- Test Pinning: Verified by
ResetWrittenBackOnRemovalTestsandBindingLoaderOracle.IntendedDeviations["reset written back on removal"].
11. Paths Traversing Lists by Index Follow Live Mutations
- XML Behavior: A
DataSourcepath traversing a list by index (e.g.,{Perks\0\CandidatePerks}) is only evaluated when the view is initially refreshed. If the underlying collection is modified in place (items added, removed, or sorted), the view fails to re-evaluate the index and remains bound to the previous item or an item that is no longer in the list. - Compiled Behavior: Whenever the collection raises a collection-changed event, the index path is re-evaluated, and the widget rebinds to whichever item currently resides at that index.
- Rationale: Remaining bound to a displaced item produces stale or invalid screen data.
- Roslyn Analyzer: UIX0028 reports indexed list path bindings as compile-time errors.
- Test Pinning: Verified by
IndexedDataSourceTestsandBindingLoaderOracle.IntendedDeviations["indexed item followed"].
Non-Deviations
To avoid confusion, the following scenarios are not considered deviations:
- Compilation Declines: When the code generator declines a movie (e.g., due to recursive constants, circular prefab roots, or unresolvable properties), the movie runs via the XML loader. Declines represent capability gaps rather than behavioral deviations and are tracked as oracle failures (
OracleOutcome.NotGenerated). - Missing Resource Constant Arithmetic: If a constant performs mathematical operations on a missing sprite or brush layer dimension (e.g.,
Additive="8"on a missing sprite), Gauntlet's XML loader throws aFormatExceptionand aborts movie loading. Because the presence of sprite resources depends on external user mod configurations that cannot be validated statically, compiled prefabs replicate the XML loader's exception behavior.
Related Documentation
- Handling Discrepancies from the XML Loader: Governance rules, triage procedures, and exemption policies.
- Code Generator: Generator architecture, data sources, and member resolution.
- General Analyzers Documentation: Index of all
UIX...diagnostic rules.
This page was last modified at 10/05/2026 13:34:58 +03:00 (UTC).
Commit Message
Author: Vitalii Mikhailov
Commit: f3b725a97c3f299c20e56b5f206e27142ec15edd
* Added C# Auto-Generated XML prefab compilation for patched and dynamic movies, matching vanilla UI performance
* Added name-based property binding fallback for ViewModel subtypes not declared on the parent
* Improved unparseable prefab error logging with name and file details
* Fixed game crash with 3-segment attribute paths
* Added Bannerlord.UIExtenderE...