diff --git a/src/System.Windows.Forms/Properties/AssemblyInfo.cs b/src/System.Windows.Forms/Properties/AssemblyInfo.cs
index 7c9560649ac..30fe290396a 100644
--- a/src/System.Windows.Forms/Properties/AssemblyInfo.cs
+++ b/src/System.Windows.Forms/Properties/AssemblyInfo.cs
@@ -15,3 +15,10 @@
// This is needed in order to Moq internal interfaces for testing
[assembly: InternalsVisibleTo($"DynamicProxyGenAssembly2, PublicKey={PublicKeys.Moq}")]
+
+// TODO: Confirm the actual WiseTech CargoWise assembly name and strong-name public key before shipping.
+// Replace "WiseTech.CargoWise" with the real assembly name and add a "PublicKey={PublicKeys.*}"
+// token (adding a new PublicKeys constant if needed) once the key is confirmed.
+// This entry grants CargoWise access to RelatedCurrencyManager.UseParentCurrentChanged,
+// the supported .NET 10 replacement for the ZBindingContext reflection hack.
+[assembly: InternalsVisibleTo("WiseTech.CargoWise")]
diff --git a/src/System.Windows.Forms/System/Windows/Forms/DataBinding/RelatedCurrencyManager.cs b/src/System.Windows.Forms/System/Windows/Forms/DataBinding/RelatedCurrencyManager.cs
index ff7a6899adc..b8ba44cb64e 100644
--- a/src/System.Windows.Forms/System/Windows/Forms/DataBinding/RelatedCurrencyManager.cs
+++ b/src/System.Windows.Forms/System/Windows/Forms/DataBinding/RelatedCurrencyManager.cs
@@ -24,21 +24,38 @@ internal class RelatedCurrencyManager : CurrencyManager
///
/// When (the default) a
/// refreshes from the parent manager's
- /// event, which is the standard framework behavior.
+ /// event, which is the standard framework behavior.
+ /// fires on both parent position changes and parent item-property edits.
///
///
/// When set to the related manager instead refreshes from the
- /// parent's event. This reproduces a
- /// behavior that consumers (for example CargoWise's ZBindingContext) previously
- /// achieved through reflection, which is no longer possible in .NET 10 because the
- /// list-manager dictionary can no longer be intercepted.
+ /// parent's event, which fires only on
+ /// parent position changes — not on item-property edits. This is the supported .NET 10
+ /// replacement for a reflection-based event swap that consumers such as CargoWise's
+ /// ZBindingContext previously performed, which is no longer viable because the
+ /// list-manager storage can no longer be intercepted.
///
///
- /// This switch must be set before related currency managers are created (for example in
- /// the binding context constructor) so that the desired event is wired up from the start.
- /// Changing it after managers have been created only affects managers created or rebound
- /// afterwards.
+ /// This switch is process-wide and must be set before related currency managers are
+ /// created (for example, in the binding context constructor) so that the desired event is
+ /// wired up from the start. Changing it after managers have been created only affects
+ /// managers that are created or rebound afterwards.
///
+ ///
+ /// Example — setting the flag once in a custom binding context constructor
+ /// (the .NET 10 replacement for the ZBindingContext reflection hack):
+ ///
+ ///
+ /// public class ZBindingContext : BindingContext
+ /// {
+ /// public ZBindingContext()
+ /// {
+ /// // .NET 10 replacement for the old reflection hack: drive related currency
+ /// // managers off the parent's CurrentChanged event instead of CurrentItemChanged.
+ /// RelatedCurrencyManager.UseParentCurrentChanged = true;
+ /// }
+ /// }
+ ///
///
internal static bool UseParentCurrentChanged { get; set; }
diff --git a/src/test/unit/System.Windows.Forms/System/Windows/Forms/RelatedCurrencyManagerTests.cs b/src/test/unit/System.Windows.Forms/System/Windows/Forms/RelatedCurrencyManagerTests.cs
new file mode 100644
index 00000000000..8491f260e8d
--- /dev/null
+++ b/src/test/unit/System.Windows.Forms/System/Windows/Forms/RelatedCurrencyManagerTests.cs
@@ -0,0 +1,256 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.ComponentModel;
+
+namespace System.Windows.Forms.Tests;
+
+// NB: doesn't require thread affinity
+public class RelatedCurrencyManagerTests
+{
+ ///
+ /// Verifies that defaults to
+ /// , preserving the standard framework behavior out of the box.
+ ///
+ [Fact]
+ public void UseParentCurrentChanged_DefaultValue_IsFalse()
+ {
+ bool originalValue = RelatedCurrencyManager.UseParentCurrentChanged;
+ try
+ {
+ // Reset to the documented default so the assertion is deterministic.
+ RelatedCurrencyManager.UseParentCurrentChanged = false;
+
+ Assert.False(RelatedCurrencyManager.UseParentCurrentChanged);
+ }
+ finally
+ {
+ RelatedCurrencyManager.UseParentCurrentChanged = originalValue;
+ }
+ }
+
+ ///
+ /// Default behavior: the child manager refreshes when the parent fires
+ /// (e.g. an item-property edit on
+ /// the parent list).
+ ///
+ [Fact]
+ public void UseParentCurrentChanged_False_RefreshesOnCurrentItemChanged()
+ {
+ bool originalValue = RelatedCurrencyManager.UseParentCurrentChanged;
+ try
+ {
+ RelatedCurrencyManager.UseParentCurrentChanged = false;
+
+ BindingList parentList = [new ParentRow(), new ParentRow()];
+ BindingContext context = [];
+
+ CurrencyManager parentManager = (CurrencyManager)context[parentList];
+ CurrencyManager childManager = (CurrencyManager)context[parentList, nameof(ParentRow.Children)];
+
+ int refreshCount = 0;
+ childManager.CurrentChanged += (s, e) => refreshCount++;
+
+ // ResetItem fires ListChanged(ItemChanged) on the BindingList, which causes the
+ // parent CurrencyManager to fire OnCurrentItemChanged — but NOT OnCurrentChanged.
+ // Default mode is subscribed to CurrentItemChanged, so the child should refresh.
+ parentList.ResetItem(parentManager.Position);
+
+ Assert.Equal(1, refreshCount);
+ }
+ finally
+ {
+ RelatedCurrencyManager.UseParentCurrentChanged = originalValue;
+ }
+ }
+
+ ///
+ /// Opt-in behavior: when is
+ /// the child manager does not refresh on a parent item-property
+ /// edit that fires only .
+ ///
+ [Fact]
+ public void UseParentCurrentChanged_True_DoesNotRefreshOnCurrentItemChangedOnly()
+ {
+ bool originalValue = RelatedCurrencyManager.UseParentCurrentChanged;
+ try
+ {
+ RelatedCurrencyManager.UseParentCurrentChanged = true;
+
+ BindingList parentList = [new ParentRow(), new ParentRow()];
+ BindingContext context = [];
+
+ CurrencyManager parentManager = (CurrencyManager)context[parentList];
+ CurrencyManager childManager = (CurrencyManager)context[parentList, nameof(ParentRow.Children)];
+
+ int refreshCount = 0;
+ childManager.CurrentChanged += (s, e) => refreshCount++;
+
+ // ResetItem fires ListChanged(ItemChanged) which causes the parent to fire only
+ // CurrentItemChanged, not CurrentChanged. Opt-in mode is subscribed to CurrentChanged,
+ // so the child must NOT refresh.
+ parentList.ResetItem(parentManager.Position);
+
+ Assert.Equal(0, refreshCount);
+ }
+ finally
+ {
+ RelatedCurrencyManager.UseParentCurrentChanged = originalValue;
+ }
+ }
+
+ ///
+ /// Default behavior: the child manager refreshes when the parent position changes
+ /// (which fires and consequently
+ /// ).
+ ///
+ [Fact]
+ public void UseParentCurrentChanged_False_RefreshesOnParentPositionChange()
+ {
+ bool originalValue = RelatedCurrencyManager.UseParentCurrentChanged;
+ try
+ {
+ RelatedCurrencyManager.UseParentCurrentChanged = false;
+
+ BindingList parentList = [new ParentRow(), new ParentRow()];
+ BindingContext context = [];
+
+ CurrencyManager parentManager = (CurrencyManager)context[parentList];
+ CurrencyManager childManager = (CurrencyManager)context[parentList, nameof(ParentRow.Children)];
+
+ int refreshCount = 0;
+ childManager.CurrentChanged += (s, e) => refreshCount++;
+
+ // Moving the parent position fires CurrentChanged (and CurrentItemChanged) on the
+ // parent. In default mode the child is subscribed to CurrentItemChanged so it refreshes.
+ parentManager.Position = 1;
+
+ Assert.Equal(1, refreshCount);
+ }
+ finally
+ {
+ RelatedCurrencyManager.UseParentCurrentChanged = originalValue;
+ }
+ }
+
+ ///
+ /// Opt-in behavior: the child manager also refreshes when the parent position changes,
+ /// because a position change fires which
+ /// the opt-in mode subscribes to.
+ ///
+ [Fact]
+ public void UseParentCurrentChanged_True_RefreshesOnParentPositionChange()
+ {
+ bool originalValue = RelatedCurrencyManager.UseParentCurrentChanged;
+ try
+ {
+ RelatedCurrencyManager.UseParentCurrentChanged = true;
+
+ BindingList parentList = [new ParentRow(), new ParentRow()];
+ BindingContext context = [];
+
+ CurrencyManager parentManager = (CurrencyManager)context[parentList];
+ CurrencyManager childManager = (CurrencyManager)context[parentList, nameof(ParentRow.Children)];
+
+ int refreshCount = 0;
+ childManager.CurrentChanged += (s, e) => refreshCount++;
+
+ // Moving the parent position fires CurrentChanged on the parent. Opt-in mode is
+ // subscribed to CurrentChanged so the child must refresh exactly once.
+ parentManager.Position = 1;
+
+ Assert.Equal(1, refreshCount);
+ }
+ finally
+ {
+ RelatedCurrencyManager.UseParentCurrentChanged = originalValue;
+ }
+ }
+
+ ///
+ /// Symmetry: retrieving the same related manager from the
+ /// cache twice does not produce a double subscription — the child refreshes exactly once
+ /// per parent event.
+ ///
+ [Fact]
+ public void UseParentCurrentChanged_False_CachedManagerHasNoDoubleSubscription()
+ {
+ bool originalValue = RelatedCurrencyManager.UseParentCurrentChanged;
+ try
+ {
+ RelatedCurrencyManager.UseParentCurrentChanged = false;
+
+ BindingList parentList = [new ParentRow(), new ParentRow()];
+ BindingContext context = [];
+
+ CurrencyManager parentManager = (CurrencyManager)context[parentList];
+
+ // Retrieve the child manager twice; the second lookup must return the cached instance.
+ CurrencyManager childManager1 = (CurrencyManager)context[parentList, nameof(ParentRow.Children)];
+ CurrencyManager childManager2 = (CurrencyManager)context[parentList, nameof(ParentRow.Children)];
+ Assert.Same(childManager1, childManager2);
+
+ int refreshCount = 0;
+ childManager1.CurrentChanged += (s, e) => refreshCount++;
+
+ parentList.ResetItem(parentManager.Position);
+
+ // Must fire exactly once — not twice — confirming no stale double subscription.
+ Assert.Equal(1, refreshCount);
+ }
+ finally
+ {
+ RelatedCurrencyManager.UseParentCurrentChanged = originalValue;
+ }
+ }
+
+ ///
+ /// Symmetry: when the switch is , rebinding the manager
+ /// () does not leave a stale
+ /// subscription on the old parent.
+ ///
+ [Fact]
+ public void UseParentCurrentChanged_True_RebindDoesNotLeaveStaleSubscription()
+ {
+ bool originalValue = RelatedCurrencyManager.UseParentCurrentChanged;
+ try
+ {
+ RelatedCurrencyManager.UseParentCurrentChanged = true;
+
+ BindingList parentList = [new ParentRow(), new ParentRow()];
+ BindingContext context = [];
+
+ CurrencyManager parentManager = (CurrencyManager)context[parentList];
+ RelatedCurrencyManager childManager =
+ (RelatedCurrencyManager)context[parentList, nameof(ParentRow.Children)];
+
+ // Rebind to the same parent — Bind() unwires the old CurrentChanged subscription
+ // and wires a fresh one. The Bind() call itself fires an init refresh directly
+ // (not via the event), so add the counter handler after the rebind.
+ childManager.Bind(parentManager, nameof(ParentRow.Children));
+
+ int refreshCount = 0;
+ childManager.CurrentChanged += (s, e) => refreshCount++;
+
+ // Move the parent position: fires CurrentChanged (and CurrentItemChanged) on the parent.
+ // The child must refresh exactly once — not twice — confirming no double subscription.
+ parentManager.Position = 1;
+
+ Assert.Equal(1, refreshCount);
+ }
+ finally
+ {
+ RelatedCurrencyManager.UseParentCurrentChanged = originalValue;
+ }
+ }
+
+ private sealed class ParentRow
+ {
+ public List Children { get; set; } = [];
+ }
+
+ private sealed class ChildRow
+ {
+ public string Name { get; set; } = string.Empty;
+ }
+}