From fe414dc4cdffff46a47dd5e407b63c4b457c2ed1 Mon Sep 17 00:00:00 2001 From: Abinesh p <120440951+abineshPalanisamy@users.noreply.github.com> Date: Thu, 4 Jun 2026 13:42:23 +0530 Subject: [PATCH 1/4] Added fix for the Fix_Issue_3260 Added fix for the Fix_Issue_3260 --- .../Forms/Application.ComponentThreadContext.cs | 16 ++++++++++------ .../Windows/Forms/Application.ThreadContext.cs | 17 +++++++++++++++++ .../System/Windows/Forms/ApplicationContext.cs | 8 ++++++++ 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/src/System.Windows.Forms/System/Windows/Forms/Application.ComponentThreadContext.cs b/src/System.Windows.Forms/System/Windows/Forms/Application.ComponentThreadContext.cs index ea2a1961b02..c0dc3525f00 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Application.ComponentThreadContext.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Application.ComponentThreadContext.cs @@ -260,11 +260,11 @@ protected override bool RunMessageLoop(msoloop reason, bool fullModal) } else if (reason is msoloop.DoEvents or msoloop.DoEventsModal) { - result = LocalModalMessageLoop(null); + result = LocalModalMessageLoop(form: null, resolveMainFormDynamically: false); } else { - result = LocalModalMessageLoop(CurrentForm); + result = LocalModalMessageLoop(form: null, resolveMainFormDynamically: true); } return result; @@ -279,7 +279,7 @@ protected override void EndOuterMessageLoop() } } - private bool LocalModalMessageLoop(Form? form) + private bool LocalModalMessageLoop(Form? form, bool resolveMainFormDynamically = false) { try { @@ -289,6 +289,10 @@ private bool LocalModalMessageLoop(Form? form) while (continueLoop) { + Form? currentForm = resolveMainFormDynamically + ? ApplicationContext?.MainForm ?? CurrentForm + : form; + if (PInvoke.GetMessage(&msg, HWND.Null, 0, 0)) { if (!PreTranslateMessage(ref msg)) @@ -297,12 +301,12 @@ private bool LocalModalMessageLoop(Form? form) PInvoke.DispatchMessage(&msg); } - if (form is not null) + if (currentForm is not null) { - continueLoop = !form.CheckCloseDialog(false); + continueLoop = !currentForm.CheckCloseDialog(false); } } - else if (form is null) + else if (currentForm is null) { break; } diff --git a/src/System.Windows.Forms/System/Windows/Forms/Application.ThreadContext.cs b/src/System.Windows.Forms/System/Windows/Forms/Application.ThreadContext.cs index 45d2a615bd5..93f82d5a45e 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Application.ThreadContext.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Application.ThreadContext.cs @@ -95,6 +95,23 @@ public virtual void EnsureReadyForIdle() { } internal bool CustomThreadExceptionHandlerAttached => _threadExceptionHandler is not null; + internal static ThreadContext? Current => t_currentThreadContext; + + internal void OnMainFormChanged(ApplicationContext applicationContext, Form? oldMainForm, Form? newMainForm) + { + if (!ReferenceEquals(ApplicationContext, applicationContext)) + { + return; + } + + // Only update the cached form if it is still tracking the previous MainForm. + // This avoids clobbering unrelated state such as a current modal form. + if (ReferenceEquals(CurrentForm, oldMainForm)) + { + CurrentForm = newMainForm; + } + } + /// /// Retrieves the actual parking form. This will demand create the parking window if it needs to. /// diff --git a/src/System.Windows.Forms/System/Windows/Forms/ApplicationContext.cs b/src/System.Windows.Forms/System/Windows/Forms/ApplicationContext.cs index 31be20c2f0c..28873206a43 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/ApplicationContext.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/ApplicationContext.cs @@ -54,10 +54,18 @@ public Form? MainForm get => _mainForm; set { + if (ReferenceEquals(_mainForm, value)) + { + return; + } + + Form? oldMainForm = _mainForm; EventHandler onClose = OnMainFormDestroy; _mainForm?.HandleDestroyed -= onClose; _mainForm = value; _mainForm?.HandleDestroyed += onClose; + + Application.ThreadContext.Current?.OnMainFormChanged(this, oldMainForm, value); } } From 143812575d03426476c8fde5af364c07f83f23f5 Mon Sep 17 00:00:00 2001 From: Abinesh p <120440951+abineshPalanisamy@users.noreply.github.com> Date: Thu, 9 Jul 2026 17:46:29 +0530 Subject: [PATCH 2/4] Addressed the review correction and updated the test cases Addressed the review correction and updated the test cases --- .../Application.ComponentThreadContext.cs | 16 ++++------ .../Forms/Application.ThreadContext.cs | 32 +++++++------------ .../Windows/Forms/ApplicationContext.cs | 8 ----- 3 files changed, 17 insertions(+), 39 deletions(-) diff --git a/src/System.Windows.Forms/System/Windows/Forms/Application.ComponentThreadContext.cs b/src/System.Windows.Forms/System/Windows/Forms/Application.ComponentThreadContext.cs index c0dc3525f00..e583df9b43a 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Application.ComponentThreadContext.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Application.ComponentThreadContext.cs @@ -260,11 +260,11 @@ protected override bool RunMessageLoop(msoloop reason, bool fullModal) } else if (reason is msoloop.DoEvents or msoloop.DoEventsModal) { - result = LocalModalMessageLoop(form: null, resolveMainFormDynamically: false); + result = LocalModalMessageLoop(null); } else { - result = LocalModalMessageLoop(form: null, resolveMainFormDynamically: true); + result = LocalModalMessageLoop(CurrentForm); } return result; @@ -279,7 +279,7 @@ protected override void EndOuterMessageLoop() } } - private bool LocalModalMessageLoop(Form? form, bool resolveMainFormDynamically = false) + private bool LocalModalMessageLoop(Form? form) { try { @@ -289,10 +289,6 @@ private bool LocalModalMessageLoop(Form? form, bool resolveMainFormDynamically = while (continueLoop) { - Form? currentForm = resolveMainFormDynamically - ? ApplicationContext?.MainForm ?? CurrentForm - : form; - if (PInvoke.GetMessage(&msg, HWND.Null, 0, 0)) { if (!PreTranslateMessage(ref msg)) @@ -301,12 +297,12 @@ private bool LocalModalMessageLoop(Form? form, bool resolveMainFormDynamically = PInvoke.DispatchMessage(&msg); } - if (currentForm is not null) + if (form is not null) { - continueLoop = !currentForm.CheckCloseDialog(false); + continueLoop = !form .CheckCloseDialog(false); } } - else if (currentForm is null) + else if (form is null) { break; } diff --git a/src/System.Windows.Forms/System/Windows/Forms/Application.ThreadContext.cs b/src/System.Windows.Forms/System/Windows/Forms/Application.ThreadContext.cs index 93f82d5a45e..09130a6db5b 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Application.ThreadContext.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Application.ThreadContext.cs @@ -62,7 +62,14 @@ internal abstract unsafe partial class ThreadContext : MarshalByRefObject, IHand // A private field on Application that stores the callback delegate private MessageLoopCallback? _messageLoopCallback; - protected Form? CurrentForm { get; private set; } + private Form? _currentFormOverride; + + protected Form? CurrentForm + { + get => _currentFormOverride ?? ApplicationContext?.MainForm; + private set => _currentFormOverride = value; + } + protected bool PostedQuit { get; private set; } /// @@ -95,23 +102,6 @@ public virtual void EnsureReadyForIdle() { } internal bool CustomThreadExceptionHandlerAttached => _threadExceptionHandler is not null; - internal static ThreadContext? Current => t_currentThreadContext; - - internal void OnMainFormChanged(ApplicationContext applicationContext, Form? oldMainForm, Form? newMainForm) - { - if (!ReferenceEquals(ApplicationContext, applicationContext)) - { - return; - } - - // Only update the cached form if it is still tracking the previous MainForm. - // This avoids clobbering unrelated state such as a current modal form. - if (ReferenceEquals(CurrentForm, oldMainForm)) - { - CurrentForm = newMainForm; - } - } - /// /// Retrieves the actual parking form. This will demand create the parking window if it needs to. /// @@ -736,8 +726,8 @@ private void RunMessageLoopInner(msoloop reason, ApplicationContext? context) ApplicationContext.MainForm?.Visible = true; } - Form? oldForm = CurrentForm; - if (context is not null) + Form? oldFormOverride = _currentFormOverride; + if (context is not null && reason != msoloop.Main) { CurrentForm = context.MainForm; } @@ -821,7 +811,7 @@ private void RunMessageLoopInner(msoloop reason, ApplicationContext? context) } } - CurrentForm = oldForm; + _currentFormOverride = oldFormOverride; s_totalMessageLoopCount--; _messageLoopCount--; diff --git a/src/System.Windows.Forms/System/Windows/Forms/ApplicationContext.cs b/src/System.Windows.Forms/System/Windows/Forms/ApplicationContext.cs index 28873206a43..31be20c2f0c 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/ApplicationContext.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/ApplicationContext.cs @@ -54,18 +54,10 @@ public Form? MainForm get => _mainForm; set { - if (ReferenceEquals(_mainForm, value)) - { - return; - } - - Form? oldMainForm = _mainForm; EventHandler onClose = OnMainFormDestroy; _mainForm?.HandleDestroyed -= onClose; _mainForm = value; _mainForm?.HandleDestroyed += onClose; - - Application.ThreadContext.Current?.OnMainFormChanged(this, oldMainForm, value); } } From 612966ec42b750c5f08394ef1aa297a17e4240b4 Mon Sep 17 00:00:00 2001 From: Abinesh p <120440951+abineshPalanisamy@users.noreply.github.com> Date: Thu, 9 Jul 2026 17:47:43 +0530 Subject: [PATCH 3/4] Updated the changes for Fix_Issue_3260 Updated the changes for Fix_Issue_3260 --- .../System/Windows/Forms/Application.ComponentThreadContext.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/System.Windows.Forms/System/Windows/Forms/Application.ComponentThreadContext.cs b/src/System.Windows.Forms/System/Windows/Forms/Application.ComponentThreadContext.cs index e583df9b43a..ea2a1961b02 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Application.ComponentThreadContext.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Application.ComponentThreadContext.cs @@ -299,7 +299,7 @@ private bool LocalModalMessageLoop(Form? form) if (form is not null) { - continueLoop = !form .CheckCloseDialog(false); + continueLoop = !form.CheckCloseDialog(false); } } else if (form is null) From 3938ac39a81bd6f8c7d1559c6038f4cf544e76f6 Mon Sep 17 00:00:00 2001 From: Abinesh p <120440951+abineshPalanisamy@users.noreply.github.com> Date: Thu, 9 Jul 2026 17:50:52 +0530 Subject: [PATCH 4/4] Added the unit test cases for Fix_Issue_3260 Added the unit test cases for Fix_Issue_3260 --- .../ThreadContextTests.cs | 153 ++++++++++++++++++ 1 file changed, 153 insertions(+) diff --git a/src/test/unit/System.Windows.Forms/ThreadContextTests.cs b/src/test/unit/System.Windows.Forms/ThreadContextTests.cs index fe00f88a411..ae0d722b863 100644 --- a/src/test/unit/System.Windows.Forms/ThreadContextTests.cs +++ b/src/test/unit/System.Windows.Forms/ThreadContextTests.cs @@ -110,4 +110,157 @@ public void ThreadContext_MultipleProcessFiltersProcesses() mockContext2.Verify(c => c.PreFilterMessage(ref It.Ref.IsAny), Times.Exactly(3)); mockContext3.Verify(c => c.PreFilterMessage(ref It.Ref.IsAny), Times.Exactly(2)); } + + [WinFormsFact] + public void ThreadContext_CurrentForm_ReflectsChangedMainForm() + { + RunOnStaThread(() => + { + using Form form1 = new() + { + Text = "Form1" + }; + + using Form form2 = new() + { + Text = "Form2" + }; + + using ApplicationContext context = new(form1); + + Form currentFormBeforeChange = null; + Form currentFormAfterChange = null; + + form1.Shown += (_, _) => + { + currentFormBeforeChange = GetThreadContextCurrentForm(); + + context.MainForm = form2; + + currentFormAfterChange = GetThreadContextCurrentForm(); + + context.ExitThread(); + }; + + Application.Run(context); + + Assert.Same(form1, currentFormBeforeChange); + Assert.Same(form2, currentFormAfterChange); + }); + } + + [WinFormsFact] + public void ThreadContext_CurrentForm_ReflectsNullMainForm() + { + RunOnStaThread(() => + { + using Form form = new() + { + Text = "Form1" + }; + + using ApplicationContext context = new(form); + + Form currentFormBeforeChange = null; + Form currentFormAfterChange = null; + + form.Shown += (_, _) => + { + currentFormBeforeChange = GetThreadContextCurrentForm(); + + context.MainForm = null; + + currentFormAfterChange = GetThreadContextCurrentForm(); + + context.ExitThread(); + }; + + Application.Run(context); + + Assert.Same(form, currentFormBeforeChange); + Assert.Null(currentFormAfterChange); + }); + } + + [WinFormsFact] + public void ThreadContext_CurrentForm_ReflectsMultipleMainFormChanges() + { + RunOnStaThread(() => + { + using Form form1 = new() + { + Text = "Form1" + }; + + using Form form2 = new() + { + Text = "Form2" + }; + + using Form form3 = new() + { + Text = "Form3" + }; + + using ApplicationContext context = new(form1); + + Form currentFormAfterFirstChange = null; + Form currentFormAfterSecondChange = null; + + form1.Shown += (_, _) => + { + context.MainForm = form2; + currentFormAfterFirstChange = GetThreadContextCurrentForm(); + + context.MainForm = form3; + currentFormAfterSecondChange = GetThreadContextCurrentForm(); + + context.ExitThread(); + }; + + Application.Run(context); + + Assert.Same(form2, currentFormAfterFirstChange); + Assert.Same(form3, currentFormAfterSecondChange); + }); + } + + private static void RunOnStaThread(Action action) + { + Exception exception = null; + + Thread thread = new(() => + { + try + { + action(); + } + catch (Exception ex) + { + exception = ex; + } + }); + + thread.SetApartmentState(ApartmentState.STA); + thread.IsBackground = true; + thread.Start(); + + Assert.True(thread.Join(TimeSpan.FromSeconds(30)), "The STA test thread timed out."); + + if (exception is not null) + { + throw exception; + } + } + + private static Form GetThreadContextCurrentForm() + { + Application.ThreadContext threadContext = Application.ThreadContext.FromCurrent(); + + PropertyInfo currentFormProperty = typeof(Application.ThreadContext).GetProperty( + "CurrentForm", + BindingFlags.Instance | BindingFlags.NonPublic); + + return (Form)currentFormProperty.GetValue(threadContext); + } }