From 598c0ec62aaf06b5e78a994f44113c6dfef527cf Mon Sep 17 00:00:00 2001 From: ET Date: Fri, 27 Feb 2026 10:47:47 -0800 Subject: [PATCH] Input/Stylus: Make WISP pen processing more resilient against disposal - Add IsDisposed property to PenThreadWorker exposing the __disposed flag - Add disposed guard to WorkerGetTabletsInfo returning Array.Empty immediately (consistent with all other Worker* methods that already have this guard) - Expose IsDisposed on PenThread via forwarding property - PenThreadPool: prune disposed PenThread entries alongside dead WeakReferences - PenThread: make _penThreadWorker field readonly; remove now-redundant null check --- .../Windows/Input/Stylus/Wisp/PenThread.cs | 7 ++++--- .../Windows/Input/Stylus/Wisp/PenThreadPool.cs | 17 ++++++----------- .../Input/Stylus/Wisp/PenThreadWorker.cs | 7 +++++++ 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Input/Stylus/Wisp/PenThread.cs b/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Input/Stylus/Wisp/PenThread.cs index a46727e0e11..8a34f405dd8 100644 --- a/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Input/Stylus/Wisp/PenThread.cs +++ b/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Input/Stylus/Wisp/PenThread.cs @@ -11,7 +11,7 @@ namespace System.Windows.Input internal sealed class PenThread { - private PenThreadWorker _penThreadWorker; + private readonly PenThreadWorker _penThreadWorker; internal PenThread() { @@ -37,13 +37,14 @@ internal void Dispose() private void DisposeHelper() { - // NOTE: PenThreadWorker deals with already being disposed logic. - _penThreadWorker?.Dispose(); + _penThreadWorker.Dispose(); GC.KeepAlive(this); } ///////////////////////////////////////////////////////////////////// + internal bool IsDisposed => _penThreadWorker.IsDisposed; + internal bool AddPenContext(PenContext penContext) { return _penThreadWorker.WorkerAddPenContext(penContext); diff --git a/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Input/Stylus/Wisp/PenThreadPool.cs b/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Input/Stylus/Wisp/PenThreadPool.cs index b2673c26e8a..fd42deca801 100644 --- a/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Input/Stylus/Wisp/PenThreadPool.cs +++ b/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Input/Stylus/Wisp/PenThreadPool.cs @@ -93,20 +93,15 @@ private PenThread GetPenThreadForPenContextHelper(PenContext penContext) // We scan back to front to enable list cleanup. for (int i = _penThreadWeakRefList.Count - 1; i >= 0; i--) { - PenThread candidatePenThread = null; - - // Select a thread if it's a valid WeakReference and we're not ignoring it - // Allow selection to happen multiple times so we get the first valid candidate - // in forward order. - if (_penThreadWeakRefList[i].TryGetTarget(out candidatePenThread) - && !ignoredThreads.Contains(candidatePenThread)) + // Check if the WeakReference is still alive and not disposed. + if (!_penThreadWeakRefList[i].TryGetTarget(out PenThread candidatePenThread) || + candidatePenThread.IsDisposed) { - selectedPenThread = candidatePenThread; + _penThreadWeakRefList.RemoveAt(i); } - // This is an invalid WeakReference and should be removed - else if (candidatePenThread == null) + else if (!ignoredThreads.Contains(candidatePenThread)) { - _penThreadWeakRefList.RemoveAt(i); + selectedPenThread = candidatePenThread; } } diff --git a/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Input/Stylus/Wisp/PenThreadWorker.cs b/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Input/Stylus/Wisp/PenThreadWorker.cs index a873eeb5860..b0787c4d8b8 100644 --- a/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Input/Stylus/Wisp/PenThreadWorker.cs +++ b/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Input/Stylus/Wisp/PenThreadWorker.cs @@ -559,8 +559,15 @@ internal bool WorkerRemovePenContext(PenContext penContext) ///////////////////////////////////////////////////////////////////// + internal bool IsDisposed => __disposed; + internal TabletDeviceInfo[] WorkerGetTabletsInfo() { + if (__disposed) + { + return Array.Empty(); + } + // Set data up for this call WorkerOperationGetTabletsInfo getTablets = new WorkerOperationGetTabletsInfo();