Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
<WtgPackageVersionPrefix>0.1.0</WtgPackageVersionPrefix>
<PackageVersion Condition="'$(PackageVersion)' == ''">$(WtgPackageVersionPrefix)-dev</PackageVersion>
<PackageDescription>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.</PackageDescription>
<PackageReleaseNotes>HotFix - Fix ZBindingContext related-manager binding behavior on .NET 10</PackageReleaseNotes>
<Authors>WiseTech Global</Authors>
<Copyright>© WiseTech Global Limited. All rights reserved.</Copyright>
<PackageReadmeFile>README.md</PackageReadmeFile>
Expand Down
6 changes: 5 additions & 1 deletion src/System.Windows.Forms/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -305,20 +305,49 @@ protected virtual void OnListManagerAdded(BindingManagerBase bindingManagerBase)
}

/// <summary>
/// Helper for subclasses overriding <see cref="OnListManagerAdded"/>: if the supplied manager is a
/// child currency manager, detaches its parent's <see cref="BindingManagerBase.CurrentItemChanged"/>
/// subscription and re-attaches it to <see cref="BindingManagerBase.CurrentChanged"/>, then primes the child
/// once with the parent's current state. No-op for managers that are not <see cref="RelatedCurrencyManager"/>
/// instances. Exposed because <see cref="RelatedCurrencyManager"/> is internal and cannot be referenced from
/// subclasses in other assemblies.
/// Helper for subclasses overriding <see cref="OnListManagerAdded"/>: returns the parent
/// <see cref="CurrencyManager"/> that drives the supplied related (child) currency manager, or
/// <see langword="null"/> 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
/// <see cref="RelatedCurrencyManager"/> type.
/// </summary>
protected static void RewireRelatedCurrencyManagerParent(BindingManagerBase bindingManagerBase)
{
if (bindingManagerBase is RelatedCurrencyManager relatedCurrencyManager)
{
relatedCurrencyManager.RewireParentChangeHandler();
}
}
protected static CurrencyManager? GetParentCurrencyManager(BindingManagerBase bindingManagerBase)
=> (bindingManagerBase as RelatedCurrencyManager)?.ParentCurrencyManager;

/// <summary>
/// Helper for subclasses overriding <see cref="OnListManagerAdded"/>: detaches the default
/// <see cref="BindingManagerBase.CurrentItemChanged"/> subscription that a child currency manager wires onto
/// its parent, so the subclass can drive the child from the parent's
/// <see cref="BindingManagerBase.CurrentChanged"/> event instead. No-op for managers that are not
/// <see cref="RelatedCurrencyManager"/> instances.
/// </summary>
protected static void DetachRelatedManagerFromParentItemChanged(BindingManagerBase bindingManagerBase)
=> (bindingManagerBase as RelatedCurrencyManager)?.DetachParentItemChangedHandler();

/// <summary>
/// Helper for subclasses overriding <see cref="OnListManagerAdded"/>: 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 <see cref="RelatedCurrencyManager"/> instances.
/// </summary>
protected static void RefreshRelatedManagerFromParent(BindingManagerBase bindingManagerBase)
=> (bindingManagerBase as RelatedCurrencyManager)?.RefreshFromParentCurrent();

/// <summary>
/// Helper for subclasses overriding <see cref="OnListManagerAdded"/>: 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 <see cref="RelatedCurrencyManager"/> instances.
/// </summary>
protected static void ResetRelatedManagerToEmptyList(BindingManagerBase bindingManagerBase, IList emptyList)
=> (bindingManagerBase as RelatedCurrencyManager)?.ResetToEmptyList(emptyList);

/// <summary>
/// Helper for subclasses: raises <see cref="BindingManagerBase.CurrentItemChanged"/> 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.
/// </summary>
protected static void RaiseCurrentItemChangedSuppressingPushData(CurrencyManager currencyManager, EventArgs e)
=> currencyManager.RaiseCurrentItemChangedSuppressingPushData(e);

private static void CheckPropertyBindingCycles(BindingContext newBindingContext, Binding propBinding)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,22 +138,55 @@
// 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.

/// <summary>
/// The parent currency manager that drives this child, or <see langword="null"/> when the parent is not a
/// <see cref="CurrencyManager"/>.
/// </summary>
internal CurrencyManager? ParentCurrencyManager => _parentManager as CurrencyManager;

/// <summary>
/// Detaches the default <see cref="ParentManager_CurrentItemChanged"/> subscription that <see cref="Bind"/>
/// wired onto the parent. Callers that drive the child from the parent's CurrentChanged event use this to
/// avoid double refreshing.
/// </summary>
internal void DetachParentItemChangedHandler()
{
if (_parentManager is CurrencyManager parentCurrencyManager)
if (_parentManager is not null)

Check warning on line 164 in src/System.Windows.Forms/System/Windows/Forms/DataBinding/RelatedCurrencyManager.cs

View workflow job for this annotation

GitHub Actions / Build - Release

Null check can be simplified (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0031)
{
parentCurrencyManager.CurrentItemChanged -= ParentManager_CurrentItemChanged;
parentCurrencyManager.CurrentChanged -= ParentManager_CurrentItemChanged;
parentCurrencyManager.CurrentChanged += ParentManager_CurrentItemChanged;
ParentManager_CurrentItemChanged(parentCurrencyManager, EventArgs.Empty);
_parentManager.CurrentItemChanged -= ParentManager_CurrentItemChanged;
}
}

/// <summary>
/// Refreshes the child list from the parent's current row, exactly as the default CurrentItemChanged handler
/// does.
/// </summary>
internal void RefreshFromParentCurrent() => ParentManager_CurrentItemChanged(_parentManager, EventArgs.Empty);

/// <summary>
/// Resets this child to the supplied empty placeholder list and raises position/current change notifications,
/// bypassing the AddNew/CancelCurrentEdit appcompat path that <see cref="ParentManager_CurrentItemChanged"/>
/// runs when the parent is empty.
/// </summary>
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))
Expand Down
Loading