WI01086104 - Fix Group 1 NRE: empty-parent AddNew on RelatedCurrencyManager#24
WI01086104 - Fix Group 1 NRE: empty-parent AddNew on RelatedCurrencyManager#24FahmiFuzi wants to merge 8 commits into
Conversation
…on CargoWise collections)
Replicate .NET Framework ZBindingContext.ParentCurrentChangedHandler empty-parent behavior
in the rewired CurrentChanged handler path. When parent manager has no current row (Count==0),
the child now binds an empty list (AllowNew=false) instead of calling AddNew(), which
materializes orphaned business objects whose SetDefaultsForNewChild/property getters
dereference null parent navigations.
Changes:
- RewireParentChangeHandler now wires parent CurrentChanged to new ParentManager_CurrentChanged
- ParentManager_CurrentChanged: empty parent → SetDataSource(empty BindingList) + listposition=-1
+ raise position/current events; non-empty → delegate to stock ParentManager_CurrentItemChanged
- Stock ParentManager_CurrentItemChanged: one-line guard change (LV1 WI01086017):
else if (currencyManager.List is IBindingList { AllowNew: true }) — skips AddNew on read-only lists
The recursive EnsureListManager ordering (parent registered+rewired before child ctor-primes)
ensures the guard+placeholder also protect the constructor-time priming AddNew.
Validated: DE ExitSummaryPlugInTest.TestBashUserControlOfPlugIn = PASS (was NRE);
MM GlbCompanyCampaignFormTest.TestBashingForm = NRE eliminated (0 signatures in TRX).
Earlier broad run cleared NRE in 23/23 across 11 assemblies.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
… the empty-parent AddNew and .NET 10 cannot Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Root cause analysis — why .NET Framework and .NET 8 work, and .NET 10 doesn'tThe hazard is ancient and identical in all versions
What changed between runtimes is not this code — it's whether CargoWise could intercept manager registration and suppress it. .NET Framework — interception works
.NET 8 — same trick, different WinFormsOn .NET 8 the CargoWise client does not run on Microsoft's WinForms — it runs on Winzor's own Notably, Microsoft's WinForms switched to .NET 10 — interception impossible, the suppressed path runs for the first timeThe fork inherits the upstream Execution on .NET 10 before this PR (binding
How this PR restores the old behavior
Constructor-time priming is covered by the same ordering argument as .NET Framework: because (The code comments in |
…placeholder lists
ParentManager_CurrentItemChanged: when the parent collection is empty AND its list disallows AddNew (read-only / query-backed CargoWise collection), the Everett AddNew dance is skipped and the child manager's List was left null, so bound ZGrids could not resolve their column metadata (a null list leaves the grid on its empty default table style and the designer column styles never get a PropertyDescriptor). Add an else branch that binds the same empty placeholder the rewired CurrentChanged path uses (BindEmptyParentPlaceholder), so List is a valid empty list. Also covers related managers created through a plain BindingContext (e.g. a grid bound in a control constructor before it is parented to the form's ZBindingContext). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…o reference original implementation in CargoWise repo
…e BindingContext for empty parent scenarios
Summary
Fixes the .NET 10 Group 1 NullReferenceException failures (39 failures in crikey run 27ca6b55) by replicating the .NET Framework
ZBindingContext.ParentCurrentChangedHandlerempty-parent behavior in the fork's rewired CurrentChanged handler path.When a parent currency manager has no current row (
Count == 0), the child manager now binds an empty list (AllowNew=false) instead of callingAddNew(), which materializes orphaned business objects whoseSetDefaultsForNewChild/property getters dereference null parent navigations.Root Cause — why .NET Framework and .NET 8 worked but .NET 10 fails
The dangerous code path is identical in every version:
RelatedCurrencyManager.ParentManager_CurrentItemChangedhas an "Everett appcompat" branch that, when the parent manager is empty (Count == 0), callsAddNew()on the parent's list to materialize a dummy row, thenCancelCurrentEdit(). On CargoWise business-object collections thatAddNew()runsSetCollectionRelationships/SetDefaultsForNewChildagainst an orphaned collection (Parent == null) → NRE.What differs is whether CargoWise can intercept manager registration and suppress that path:
BindingContextstoreprivate Hashtable listManagersZBindingContextreflection-swaps the field forBindingContextHashtable;Hashtable.Addis virtual, so every registration is intercepted → empty parent rebound to inertTempList, neverAddNewSystem.Windows.Formskeptprivate readonly Hashtable _listManagersfor exactly this reason (#if NETFRAMEWORK || WINZOR)Dictionary<HashKey, WeakReference>Hashtablesubclass andDictionary<,>.Addis not virtualAddNewruns against real CargoWise collections for the first time → NRENote: Microsoft's WinForms switched to the Dictionary in Jan 2023 (dotnet#8235), so even stock .NET 8 WinForms would have failed — .NET 8 worked only because the client ran on Winzor's implementation.
The earlier WI01076374 fix added
BindingContext.OnListManagerAdded+RewireParentChangeHandleras the .NET 10 replacement for the Hashtable interception, but it only replicated theCurrentItemChanged → CurrentChangedevent swap (the Group 7 stack-overflow fix). The empty-parent "bind an inert list instead of AddNew" behavior ofParentCurrentChangedHandlerwas not ported — this PR completes that port. See the PR conversation for the step-by-step execution trace.Changes
RelatedCurrencyManager.cs:
RewireParentChangeHandler: Wires parent
CurrentChangedto newParentManager_CurrentChanged(instead of stock handler) and primes through it.New ParentManager_CurrentChanged (port of FW
ParentCurrentChangedHandler):Count == 0) →SetDataSource(new BindingList<object> { AllowNew=false, AllowEdit=false, AllowRemove=false })+listposition = -1+ raiseOnPositionChanged/OnCurrentChanged/OnCurrentItemChangedParentManager_CurrentItemChangedParentManager_CurrentItemChanged: One-line guard change (combines with LV1 WI01086017):
else→else if (currencyManager.List is IBindingList { AllowNew: true })Why This Works
Ctor-priming coverage:
EnsureListManageris recursive — each parent manager is registered + rewired before the next child is constructed. By the time a child's constructor primes, its empty parent already holds theAllowNew=falseplaceholder and the guard skips the EverettAddNew. This is the same inter-construction interception window that .NET Framework'sBindingContextHashtable.Addused.Guard+placeholder are interdependent: Without the guard,
AddNewon the placeholderBindingList<object>would insert a bareobject()and_fieldInfo.GetValuewould throw.Test Plan
ExitSummaryPlugInTest.TestBashUserControlOfPlugIn(was NRE inSetDefaultsForNewChild) — PASSGlbCompanyCampaignFormTest.TestBashingForm(was NRE inget_TrackingStatusDescription) — NRE eliminated (0 signatures in TRX)Residual Issues
Tests that still fail after the fix fail on previously-masked issues (not Group 1):
DataGridGridColumnStylesCollectionindex out-of-rangeColumnNamevalidation (Group 7)These are separate from the Group 1 NRE and can be addressed in follow-up work.
Notes
}andelse if— moved inside the blocksrc/System.Windows.Forms.Legacy/how-to-release-nuget-package.md) and bump CargoWiseDirectory.Packages.propsfrom0.2.2-pr.17.1Co-authored-by: Fahmi Fuzi fahmi.fuzi@wisetechglobal.com