diff --git a/src/System.Windows.Forms.Legacy/System.Windows.Forms.Package/System.Windows.Forms.Package.csproj b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Package/System.Windows.Forms.Package.csproj index 3bc006f413f..fbd8cf4e9c9 100644 --- a/src/System.Windows.Forms.Legacy/System.Windows.Forms.Package/System.Windows.Forms.Package.csproj +++ b/src/System.Windows.Forms.Legacy/System.Windows.Forms.Package/System.Windows.Forms.Package.csproj @@ -10,6 +10,7 @@ 0.1.0 $(WtgPackageVersionPrefix)-dev WiseTech Global forked version of System.Windows.Forms that includes controls that were deprecated in .NET Core 3.1 and .NET 5, like DataGrid, Menu, ToolBar and StatusBar. + HotFix - Fix ZBindingContext related-manager binding behavior on .NET 10 WiseTech Global © WiseTech Global Limited. All rights reserved. README.md diff --git a/src/System.Windows.Forms/PublicAPI.Unshipped.txt b/src/System.Windows.Forms/PublicAPI.Unshipped.txt index 9a731e8d912..7a4f8ce09c2 100644 --- a/src/System.Windows.Forms/PublicAPI.Unshipped.txt +++ b/src/System.Windows.Forms/PublicAPI.Unshipped.txt @@ -22,7 +22,11 @@ System.Windows.Forms.SystemColorMode.Classic = 0 -> System.Windows.Forms.SystemC System.Windows.Forms.SystemColorMode.Dark = 2 -> System.Windows.Forms.SystemColorMode System.Windows.Forms.SystemColorMode.System = 1 -> System.Windows.Forms.SystemColorMode System.Windows.Forms.VisualStyles.ComboBoxState.Focused = 5 -> System.Windows.Forms.VisualStyles.ComboBoxState -static System.Windows.Forms.BindingContext.RewireRelatedCurrencyManagerParent(System.Windows.Forms.BindingManagerBase! bindingManagerBase) -> void +static System.Windows.Forms.BindingContext.DetachRelatedManagerFromParentItemChanged(System.Windows.Forms.BindingManagerBase! bindingManagerBase) -> void +static System.Windows.Forms.BindingContext.GetParentCurrencyManager(System.Windows.Forms.BindingManagerBase! bindingManagerBase) -> System.Windows.Forms.CurrencyManager? +static System.Windows.Forms.BindingContext.RaiseCurrentItemChangedSuppressingPushData(System.Windows.Forms.CurrencyManager! currencyManager, System.EventArgs! e) -> void +static System.Windows.Forms.BindingContext.RefreshRelatedManagerFromParent(System.Windows.Forms.BindingManagerBase! bindingManagerBase) -> void +static System.Windows.Forms.BindingContext.ResetRelatedManagerToEmptyList(System.Windows.Forms.BindingManagerBase! bindingManagerBase, System.Collections.IList! emptyList) -> void virtual System.Windows.Forms.BindingContext.OnListManagerAdded(System.Windows.Forms.BindingManagerBase! bindingManagerBase) -> void virtual System.Windows.Forms.Form.OnFormBorderColorChanged(System.EventArgs! e) -> void virtual System.Windows.Forms.Form.OnFormCaptionBackColorChanged(System.EventArgs! e) -> void diff --git a/src/System.Windows.Forms/System/Windows/Forms/DataBinding/BindingContext.cs b/src/System.Windows.Forms/System/Windows/Forms/DataBinding/BindingContext.cs index a3a04cafd24..260ae557830 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/DataBinding/BindingContext.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/DataBinding/BindingContext.cs @@ -305,20 +305,49 @@ protected virtual void OnListManagerAdded(BindingManagerBase bindingManagerBase) } /// - /// Helper for subclasses overriding : if the supplied manager is a - /// child currency manager, detaches its parent's - /// subscription and re-attaches it to , then primes the child - /// once with the parent's current state. No-op for managers that are not - /// instances. Exposed because is internal and cannot be referenced from - /// subclasses in other assemblies. + /// Helper for subclasses overriding : returns the parent + /// that drives the supplied related (child) currency manager, or + /// when the manager is top-level or is not a child currency manager. Lets subclasses + /// re-wire parent/child change notifications without reflecting over the internal + /// type. /// - protected static void RewireRelatedCurrencyManagerParent(BindingManagerBase bindingManagerBase) - { - if (bindingManagerBase is RelatedCurrencyManager relatedCurrencyManager) - { - relatedCurrencyManager.RewireParentChangeHandler(); - } - } + protected static CurrencyManager? GetParentCurrencyManager(BindingManagerBase bindingManagerBase) + => (bindingManagerBase as RelatedCurrencyManager)?.ParentCurrencyManager; + + /// + /// Helper for subclasses overriding : detaches the default + /// subscription that a child currency manager wires onto + /// its parent, so the subclass can drive the child from the parent's + /// event instead. No-op for managers that are not + /// instances. + /// + protected static void DetachRelatedManagerFromParentItemChanged(BindingManagerBase bindingManagerBase) + => (bindingManagerBase as RelatedCurrencyManager)?.DetachParentItemChangedHandler(); + + /// + /// Helper for subclasses overriding : refreshes a child currency manager's + /// list from its parent's current row, exactly as the default CurrentItemChanged handler would. No-op for + /// managers that are not instances. + /// + protected static void RefreshRelatedManagerFromParent(BindingManagerBase bindingManagerBase) + => (bindingManagerBase as RelatedCurrencyManager)?.RefreshFromParentCurrent(); + + /// + /// Helper for subclasses overriding : resets a child currency manager to the + /// supplied empty placeholder list and raises position/current change events, without running the + /// AddNew/CancelCurrentEdit appcompat path that the default empty-parent handling uses. No-op for managers + /// that are not instances. + /// + protected static void ResetRelatedManagerToEmptyList(BindingManagerBase bindingManagerBase, IList emptyList) + => (bindingManagerBase as RelatedCurrencyManager)?.ResetToEmptyList(emptyList); + + /// + /// Helper for subclasses: raises on a currency manager + /// while suppressing the push of data back into bound controls, so the subclass can notify bindings that the + /// current item has gone (position -1) without WinForms trying to push data into a row that no longer exists. + /// + protected static void RaiseCurrentItemChangedSuppressingPushData(CurrencyManager currencyManager, EventArgs e) + => currencyManager.RaiseCurrentItemChangedSuppressingPushData(e); private static void CheckPropertyBindingCycles(BindingContext newBindingContext, Binding propBinding) { diff --git a/src/System.Windows.Forms/System/Windows/Forms/DataBinding/CurrencyManager.cs b/src/System.Windows.Forms/System/Windows/Forms/DataBinding/CurrencyManager.cs index ca55380fdeb..4e566062de4 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/DataBinding/CurrencyManager.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/DataBinding/CurrencyManager.cs @@ -880,6 +880,25 @@ protected internal override void OnCurrentItemChanged(EventArgs e) _onCurrentItemChangedHandler?.Invoke(this, e); } + // WiseTech: raises CurrentItemChanged while suppressing the push of data back into bound controls, so callers + // can notify bindings that the current item has gone (position -1) without WinForms trying to push data into a + // row that no longer exists. Preserves the previous suspension flag. Used by ZBindingContext on .NET 10 in + // place of the .NET Framework reflection that toggled the removed suspendPushDataInCurrentChanged field. + internal void RaiseCurrentItemChangedSuppressingPushData(EventArgs e) + { + bool previouslySuppressed = _state.HasFlag(CurrencyManagerStates.SuspendPushDataInCurrentChanged); + _state.ChangeFlags(CurrencyManagerStates.SuspendPushDataInCurrentChanged, true); + + try + { + OnCurrentItemChanged(e); + } + finally + { + _state.ChangeFlags(CurrencyManagerStates.SuspendPushDataInCurrentChanged, previouslySuppressed); + } + } + protected virtual void OnItemChanged(ItemChangedEventArgs e) { // It is possible that CurrencyManager_PushData will change the position 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 ffc115dfcb5..0c96d279eda 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/DataBinding/RelatedCurrencyManager.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/DataBinding/RelatedCurrencyManager.cs @@ -138,22 +138,55 @@ private void ParentManager_MetaDataChanged(object? sender, EventArgs e) // WiseTech: RelatedCurrencyManager normally refreshes its child list whenever the parent manager raises // CurrentItemChanged. In CargoWise that can be too broad: item-change notifications may be raised while // bindings/business collections are already reacting to the same edit or AddNew flow, and refreshing the - // child list from that path can re-enter the same notification chain until the stack overflows. ZBindingContext - // avoided this on .NET Framework by intercepting BindingContextHashtable.Add, removing the default - // CurrentItemChanged subscription, and refreshing the child only when the parent's CurrentChanged event fires. - // BindingContext uses a Dictionary on .NET 10, so this helper gives subclasses the same event swap without - // reflecting over RelatedCurrencyManager internals. - internal void RewireParentChangeHandler() + // child list from that path can re-enter the same notification chain until the stack overflows. Worse, the + // default empty-parent branch of ParentManager_CurrentItemChanged runs an AddNew/CancelCurrentEdit appcompat + // dance which, against a CargoWise business-object collection, actually adds and then deletes a real business + // object. ZBindingContext avoided this on .NET Framework by intercepting BindingContextHashtable.Add, removing + // the default CurrentItemChanged subscription, and driving the child from the parent's CurrentChanged event + // with CargoWise-owned policy (skip redundant refreshes; reset to a safe empty list instead of AddNew/Cancel). + // BindingContext uses a Dictionary on .NET 10, so the members below give that subclass the same mechanism + // (parent lookup, handler detach, refresh, safe empty reset) without reflecting over RelatedCurrencyManager + // internals. The when-to-refresh policy stays in ZBindingContext because it depends on CargoWise types. + + /// + /// The parent currency manager that drives this child, or when the parent is not a + /// . + /// + internal CurrencyManager? ParentCurrencyManager => _parentManager as CurrencyManager; + + /// + /// Detaches the default subscription that + /// wired onto the parent. Callers that drive the child from the parent's CurrentChanged event use this to + /// avoid double refreshing. + /// + internal void DetachParentItemChangedHandler() { - if (_parentManager is CurrencyManager parentCurrencyManager) + if (_parentManager is not null) { - parentCurrencyManager.CurrentItemChanged -= ParentManager_CurrentItemChanged; - parentCurrencyManager.CurrentChanged -= ParentManager_CurrentItemChanged; - parentCurrencyManager.CurrentChanged += ParentManager_CurrentItemChanged; - ParentManager_CurrentItemChanged(parentCurrencyManager, EventArgs.Empty); + _parentManager.CurrentItemChanged -= ParentManager_CurrentItemChanged; } } + /// + /// Refreshes the child list from the parent's current row, exactly as the default CurrentItemChanged handler + /// does. + /// + internal void RefreshFromParentCurrent() => ParentManager_CurrentItemChanged(_parentManager, EventArgs.Empty); + + /// + /// Resets this child to the supplied empty placeholder list and raises position/current change notifications, + /// bypassing the AddNew/CancelCurrentEdit appcompat path that + /// runs when the parent is empty. + /// + internal void ResetToEmptyList(IList emptyList) + { + SetDataSource(emptyList); + listposition = -1; + OnPositionChanged(EventArgs.Empty); + OnCurrentChanged(EventArgs.Empty); + OnCurrentItemChanged(EventArgs.Empty); + } + private void ParentManager_CurrentItemChanged(object? sender, EventArgs e) { if (IgnoreItemChangedTable.Contains(_parentManager))