Skip to content
Draft
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
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,9 +22,13 @@ 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
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
virtual System.Windows.Forms.Form.OnFormCaptionTextColorChanged(System.EventArgs! e) -> void
virtual System.Windows.Forms.Form.OnFormCornerPreferenceChanged(System.EventArgs! e) -> void
System.Windows.Forms.BindingContext.IInterceptedRelatedManager
System.Windows.Forms.BindingContext.IInterceptedRelatedManager.BindToEmptyParentPlaceholder(System.Collections.IList! placeholder) -> void
System.Windows.Forms.BindingContext.IInterceptedRelatedManager.ParentHasRows.get -> bool
System.Windows.Forms.BindingContext.IInterceptedRelatedManager.RaiseParentCurrentItemChanged(object? sender, System.EventArgs! e) -> void
virtual System.Windows.Forms.BindingContext.CreateInterceptedParentHandler(System.Windows.Forms.BindingContext.IInterceptedRelatedManager! relatedManager) -> System.EventHandler?
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ private BindingManagerBase EnsureListManager(object dataSource, string? dataMemb
?? throw new ArgumentException(string.Format(SR.RelatedListManagerChild, dataField));

bindingManagerBase = typeof(IList).IsAssignableFrom(prop.PropertyType)
? new RelatedCurrencyManager(formerManager, dataField)
? CreateRelatedCurrencyManager(formerManager, dataField)
: new RelatedPropertyManager(formerManager, dataField);
}

Expand All @@ -288,6 +288,74 @@ private BindingManagerBase EnsureListManager(object dataSource, string? dataMemb
return bindingManagerBase;
}

/// <summary>
/// Opt-in interface that exposes the members of an internal <see cref="RelatedCurrencyManager"/>
/// that a custom handler supplied via <see cref="CreateInterceptedParentHandler"/> needs to drive
/// the child manager without reflection. This is the public surface for .NET Framework-style
/// interception from a derived <see cref="BindingContext"/> in another assembly.
/// </summary>
public interface IInterceptedRelatedManager
{
/// <summary>
/// Gets a value indicating whether the parent currency manager currently has any rows.
/// </summary>
bool ParentHasRows { get; }

/// <summary>
/// Invokes the default <c>ParentManager_CurrentItemChanged</c> handler on the related manager.
/// Use this when the parent has rows so the child refreshes its list normally.
/// </summary>
void RaiseParentCurrentItemChanged(object? sender, EventArgs e);

/// <summary>
/// Binds the related manager to the supplied <paramref name="placeholder"/> list and raises
/// position/current notifications — without entering the Everett <c>AddNew()</c> /
/// <c>CancelCurrentEdit()</c> app-compat branch. Use this when the parent has no rows.
/// </summary>
void BindToEmptyParentPlaceholder(IList placeholder);
}

/// <summary>
/// Optional, opt-in factory that a derived <see cref="BindingContext"/> can override to supply a
/// custom handler for a newly-created <see cref="RelatedCurrencyManager"/>. When a non-<see langword="null"/>
/// handler is returned it is subscribed to the parent's <c>CurrentChanged</c> event (instead of the
/// default <c>CurrentItemChanged</c> subscription), and is used for the initial prime during construction —
/// reproducing the .NET Framework parent-change interception behaviour exactly.
/// </summary>
/// <remarks>
/// <para>
/// The base implementation returns <see langword="null"/>, leaving stock WinForms behaviour completely
/// unchanged — no extra allocations occur on the base path.
/// </para>
/// <para>
/// When a non-<see langword="null"/> handler is returned, the empty-parent Everett
/// <c>AddNew()</c> / <c>CancelCurrentEdit()</c> app-compat branch in
/// <c>ParentManager_CurrentItemChanged</c> is bypassed; the handler is responsible for calling
/// <see cref="IInterceptedRelatedManager.BindToEmptyParentPlaceholder"/> when the parent has no rows.
/// </para>
/// </remarks>
/// <param name="relatedManager">
/// The newly-created related manager, exposed through <see cref="IInterceptedRelatedManager"/> so the
/// returned handler can drive it without reflection.
/// </param>
/// <returns>
/// A custom event handler, or <see langword="null"/> to use the default behaviour.
/// </returns>
protected virtual EventHandler? CreateInterceptedParentHandler(IInterceptedRelatedManager relatedManager)
=> null;

private RelatedCurrencyManager CreateRelatedCurrencyManager(BindingManagerBase parentManager, string dataField)
{
// Only a derived BindingContext can supply an intercepted handler; the base class must remain identical to
// stock WinForms, so it does not even allocate the factory delegate.
Func<RelatedCurrencyManager, EventHandler>? interceptedParentHandlerFactory =
GetType() == typeof(BindingContext)
? null
: relatedCurrencyManager => CreateInterceptedParentHandler(relatedCurrencyManager);

return new RelatedCurrencyManager(parentManager, dataField, interceptedParentHandlerFactory);
}

/// <summary>
/// Invoked from <see cref="EnsureListManager"/> immediately after a newly-created
/// <see cref="BindingManagerBase"/> has been inserted into the internal collection. The base implementation
Expand All @@ -304,22 +372,6 @@ 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.
/// </summary>
protected static void RewireRelatedCurrencyManagerParent(BindingManagerBase bindingManagerBase)
{
if (bindingManagerBase is RelatedCurrencyManager relatedCurrencyManager)
{
relatedCurrencyManager.RewireParentChangeHandler();
}
}

private static void CheckPropertyBindingCycles(BindingContext newBindingContext, Binding propBinding)
{
Debug.Assert(newBindingContext is not null, "Always called with a non-null BindingContext");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,27 @@ namespace System.Windows.Forms;
/// Represents the child version of the System.Windows.Forms.ListManager
/// that is used when a parent/child relationship exists in a System.Windows.Forms.DataSet.
/// </summary>
internal class RelatedCurrencyManager : CurrencyManager
internal class RelatedCurrencyManager : CurrencyManager, BindingContext.IInterceptedRelatedManager
{
private BindingManagerBase _parentManager;
private PropertyDescriptor _fieldInfo;
private readonly EventHandler? _interceptedParentHandler;
private static List<BindingManagerBase> IgnoreItemChangedTable { get; } = [];

internal RelatedCurrencyManager(BindingManagerBase parentManager, string dataField)
: this(parentManager, dataField, interceptedParentHandlerFactory: null)
{
}

internal RelatedCurrencyManager(
BindingManagerBase parentManager,
string dataField,
Func<RelatedCurrencyManager, EventHandler>? interceptedParentHandlerFactory)
: base(dataSource: null)
{
// The factory receives the fully-constructed child so a handler can close over it, mirroring
// `new ParentCurrentChangedHandler(cm)` on .NET Framework. It is built before Bind() primes.
_interceptedParentHandler = interceptedParentHandlerFactory?.Invoke(this);
Bind(parentManager, dataField);
}

Expand All @@ -43,14 +55,30 @@ internal void Bind(BindingManagerBase parentManager, string dataField)
// Wire new BindingManagerBase
WireParentManager(_parentManager);

ParentManager_CurrentItemChanged(parentManager, EventArgs.Empty);
if (_interceptedParentHandler is null)
{
// DEFAULT PATH - identical to current behaviour.
ParentManager_CurrentItemChanged(parentManager, EventArgs.Empty);
}
else
{
// OPT-IN PATH - prime through the supplied handler (Framework parity); avoids constructor-time AddNew().
_interceptedParentHandler(parentManager, EventArgs.Empty);
}
}

private void UnwireParentManager(BindingManagerBase? bmb)
{
if (bmb is not null)
{
bmb.CurrentItemChanged -= ParentManager_CurrentItemChanged;
if (_interceptedParentHandler is not null && bmb is CurrencyManager interceptedParent)
{
interceptedParent.CurrentChanged -= _interceptedParentHandler;
}
else
{
bmb.CurrentItemChanged -= ParentManager_CurrentItemChanged;
}

if (bmb is CurrencyManager currencyManager)
{
Expand All @@ -63,7 +91,15 @@ private void WireParentManager(BindingManagerBase bmb)
{
if (bmb is not null)
{
bmb.CurrentItemChanged += ParentManager_CurrentItemChanged;
if (_interceptedParentHandler is not null && bmb is CurrencyManager interceptedParent)
{
// Framework parity: drive the child via the parent's CurrentChanged + the supplied handler.
interceptedParent.CurrentChanged += _interceptedParentHandler;
}
else
{
bmb.CurrentItemChanged += ParentManager_CurrentItemChanged;
}

if (bmb is CurrencyManager currencyManager)
{
Expand Down Expand Up @@ -135,25 +171,6 @@ private void ParentManager_MetaDataChanged(object? sender, EventArgs e)
OnMetaDataChanged(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()
{
if (_parentManager is CurrencyManager parentCurrencyManager)
{
parentCurrencyManager.CurrentItemChanged -= ParentManager_CurrentItemChanged;
parentCurrencyManager.CurrentChanged -= ParentManager_CurrentItemChanged;
parentCurrencyManager.CurrentChanged += ParentManager_CurrentItemChanged;
ParentManager_CurrentItemChanged(parentCurrencyManager, EventArgs.Empty);
}
}

private void ParentManager_CurrentItemChanged(object? sender, EventArgs e)
{
if (IgnoreItemChangedTable.Contains(_parentManager))
Expand Down Expand Up @@ -226,4 +243,23 @@ private void ParentManager_CurrentItemChanged(object? sender, EventArgs e)
OnCurrentChanged(EventArgs.Empty);
OnCurrentItemChanged(EventArgs.Empty);
}

// Explicit implementation of BindingContext.IInterceptedRelatedManager —
// exposes the members a custom handler in a derived BindingContext needs to drive this manager
// without reflection, mirroring the .NET Framework ParentCurrentChangedHandler targets.

bool BindingContext.IInterceptedRelatedManager.ParentHasRows
=> _parentManager is CurrencyManager currencyManager && currencyManager.Count > 0;

void BindingContext.IInterceptedRelatedManager.RaiseParentCurrentItemChanged(object? sender, EventArgs e)
=> ParentManager_CurrentItemChanged(sender, e);

void BindingContext.IInterceptedRelatedManager.BindToEmptyParentPlaceholder(IList placeholder)
{
SetDataSource(placeholder);
listposition = -1;
OnPositionChanged(EventArgs.Empty);
OnCurrentChanged(EventArgs.Empty);
OnCurrentItemChanged(EventArgs.Empty);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1017,6 +1017,92 @@ public void BindingContext_UpdateBinding_NullBinding_ThrowsArgumentNullException
Assert.Throws<ArgumentNullException>("binding", () => BindingContext.UpdateBinding([], null));
}

[Fact]
public void BindingContext_DefaultPath_SubscribesCurrentItemChangedAndPrimesOnce()
{
// Default (base) BindingContext must subscribe via CurrentItemChanged and call
// ParentManager_CurrentItemChanged exactly once during RelatedCurrencyManager construction.
BindingContext context = [];
List<IListDataSource> parentList = [new() { Property = [1, 2, 3] }];

// Access the related manager — this triggers EnsureListManager → new RelatedCurrencyManager
CurrencyManager parentManager = (CurrencyManager)context[parentList];
CurrencyManager relatedManager = (CurrencyManager)context[parentList, "Property"];

// With the default path, the related manager should have been primed with the current parent row's list
Assert.Equal(3, relatedManager.Count);
}

[Fact]
public void BindingContext_InterceptedHandler_InvokedForInitialPrime_EmptyParent()
{
// A derived BindingContext returning a non-null handler must use it for the initial prime;
// the empty-parent Everett AddNew()/CancelCurrentEdit() branch must NOT be reached.
List<IListDataSource> emptyParentList = [];
List<int> placeholder = [];

InterceptingBindingContext context = new(placeholder);
CurrencyManager parentManager = (CurrencyManager)context[emptyParentList];

// Access the related manager — handler should be installed and used for priming
CurrencyManager relatedManager = (CurrencyManager)context[emptyParentList, "Property"];

// The intercepted handler should have been invoked (initial prime call count >= 1)
Assert.True(context.HandlerInvokeCount >= 1, "Intercepted handler was not invoked during construction.");

// The placeholder was bound (not the Everett AddNew path), so data source == placeholder
Assert.Same(placeholder, relatedManager.List);
}

[Fact]
public void BindingContext_InterceptedHandler_InvokedForInitialPrime_NonEmptyParent()
{
// When the parent has rows and an intercepted handler is supplied, the handler is invoked and
// should delegate to RaiseParentCurrentItemChanged so the child refreshes normally.
List<IListDataSource> parentList = [new() { Property = [10, 20] }];

InterceptingBindingContext context = new(placeholder: null);
CurrencyManager parentManager = (CurrencyManager)context[parentList];
CurrencyManager relatedManager = (CurrencyManager)context[parentList, "Property"];

Assert.True(context.HandlerInvokeCount >= 1, "Intercepted handler was not invoked.");
Assert.Equal(2, relatedManager.Count);
}

/// <summary>
/// A derived <see cref="BindingContext"/> that opts in to intercepted parent handling for testing.
/// When the parent is empty it binds the child to the supplied placeholder (avoiding Everett AddNew);
/// when the parent has rows it delegates to <see cref="BindingContext.IInterceptedRelatedManager.RaiseParentCurrentItemChanged"/>.
/// </summary>
private sealed class InterceptingBindingContext : BindingContext
{
private readonly IList? _placeholder;

public int HandlerInvokeCount { get; private set; }

public InterceptingBindingContext(IList? placeholder)
{
_placeholder = placeholder;
}

protected override EventHandler? CreateInterceptedParentHandler(IInterceptedRelatedManager relatedManager)
{
return (sender, e) =>
{
HandlerInvokeCount++;

if (relatedManager.ParentHasRows)
{
relatedManager.RaiseParentCurrentItemChanged(sender, e);
}
else if (_placeholder is not null)
{
relatedManager.BindToEmptyParentPlaceholder(_placeholder);
}
};
}
}

private class ParentDataSource
{
public DataSource ParentProperty { get; set; }
Expand Down