From d32571013106280639ec372f13aa599c47d0b686 Mon Sep 17 00:00:00 2001 From: Nicolai Henriksen Date: Sun, 8 Mar 2026 12:50:43 +0100 Subject: [PATCH 1/4] Revert "Remove ScrollViewerAssist weak event handlers on WM_DESTROY (#4015)" This reverts commit 177d663a1a839410ea454ad6233062d28898d97e. --- .../ScrollViewerAssist.cs | 43 +------------------ 1 file changed, 1 insertion(+), 42 deletions(-) diff --git a/src/MaterialDesignThemes.Wpf/ScrollViewerAssist.cs b/src/MaterialDesignThemes.Wpf/ScrollViewerAssist.cs index b5acb153b7..2bfa6a398a 100644 --- a/src/MaterialDesignThemes.Wpf/ScrollViewerAssist.cs +++ b/src/MaterialDesignThemes.Wpf/ScrollViewerAssist.cs @@ -114,12 +114,6 @@ static void OnUnloaded(object? sender, RoutedEventArgs e) } } - static void RemoveHandlers(ScrollViewer scrollViewer) - { - WeakEventManager.RemoveHandler(scrollViewer, nameof(ScrollViewer.Loaded), OnLoaded); - WeakEventManager.RemoveHandler(scrollViewer, nameof(ScrollViewer.Unloaded), OnUnloaded); - } - static void RemoveHook(ScrollViewer scrollViewer) { if (scrollViewer.GetValue(HorizontalScrollHookProperty) is HwndSourceHook hook && @@ -144,20 +138,12 @@ static void RegisterHook(ScrollViewer scrollViewer) IntPtr Hook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) { const int WM_MOUSEHWHEEL = 0x020E; - const int WM_DESTROY = 0x0002; - const int WM_NCDESTROY = 0x0082; switch (msg) { case WM_MOUSEHWHEEL when scrollViewer.IsMouseOver: int tilt = (short)((wParam.ToInt64() >> 16) & 0xFFFF); scrollViewer.ScrollToHorizontalOffset(scrollViewer.HorizontalOffset + tilt); return (IntPtr)1; - case WM_DESTROY: - case WM_NCDESTROY: - RemoveHandlers(scrollViewer); - var source = PresentationSource.FromVisual(scrollViewer) as HwndSource; - source?.RemoveHook(Hook); - break; } return IntPtr.Zero; } @@ -210,12 +196,6 @@ static void OnUnloaded(object? sender, RoutedEventArgs e) } } - static void RemoveHandlers(ScrollViewer scrollViewer) - { - WeakEventManager.RemoveHandler(scrollViewer, nameof(ScrollViewer.Loaded), OnLoaded); - WeakEventManager.RemoveHandler(scrollViewer, nameof(ScrollViewer.Unloaded), OnUnloaded); - } - static void RemoveHook(ScrollViewer scrollViewer) { scrollViewer.RemoveHandler(UIElement.MouseWheelEvent, (RoutedEventHandler)ScrollViewerOnMouseWheel); @@ -224,28 +204,7 @@ static void RemoveHook(ScrollViewer scrollViewer) static void RegisterHook(ScrollViewer scrollViewer) { RemoveHook(scrollViewer); - if (PresentationSource.FromVisual(scrollViewer) is HwndSource source) - { - HwndSourceHook hook = Hook; - source.AddHook(hook); - scrollViewer.AddHandler(UIElement.MouseWheelEvent, (RoutedEventHandler)ScrollViewerOnMouseWheel, true); - } - - IntPtr Hook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) - { - const int WM_DESTROY = 0x0002; - const int WM_NCDESTROY = 0x0082; - switch (msg) - { - case WM_DESTROY: - case WM_NCDESTROY: - RemoveHandlers(scrollViewer); - var source = PresentationSource.FromVisual(scrollViewer) as HwndSource; - source?.RemoveHook(Hook); - break; - } - return IntPtr.Zero; - } + scrollViewer.AddHandler(UIElement.MouseWheelEvent, (RoutedEventHandler)ScrollViewerOnMouseWheel, true); } // This relay is only needed because the UIElement.AddHandler() has strict requirements for the signature of the passed Delegate From daa34c9086bd74e3dfafc7eba17e9c2f197ba47a Mon Sep 17 00:00:00 2001 From: Nicolai Henriksen Date: Thu, 5 Mar 2026 21:41:52 +0100 Subject: [PATCH 2/4] Fix issue by for BubbleVerticalScroll --- .../ScrollViewerAssist.cs | 117 +++++++++++++++--- 1 file changed, 99 insertions(+), 18 deletions(-) diff --git a/src/MaterialDesignThemes.Wpf/ScrollViewerAssist.cs b/src/MaterialDesignThemes.Wpf/ScrollViewerAssist.cs index 2bfa6a398a..78d703200b 100644 --- a/src/MaterialDesignThemes.Wpf/ScrollViewerAssist.cs +++ b/src/MaterialDesignThemes.Wpf/ScrollViewerAssist.cs @@ -1,4 +1,4 @@ -using System.Windows.Interop; +using System.Windows.Interop; namespace MaterialDesignThemes.Wpf; @@ -67,6 +67,9 @@ public static PaddingMode GetPaddingMode(DependencyObject element) private static readonly DependencyProperty HorizontalScrollSourceProperty = DependencyProperty.RegisterAttached( "HorizontalScrollSource", typeof(HwndSource), typeof(ScrollViewerAssist), new PropertyMetadata(null)); + private static readonly DependencyProperty BubbleVerticalScrollHookProperty = DependencyProperty.RegisterAttached( + "BubbleVerticalScrollHook", typeof(HwndSourceHook), typeof(ScrollViewerAssist), new PropertyMetadata(null)); + public static readonly DependencyProperty SupportHorizontalScrollProperty = DependencyProperty.RegisterAttached( "SupportHorizontalScroll", typeof(bool), typeof(ScrollViewerAssist), new PropertyMetadata(false, OnSupportHorizontalScrollChanged)); @@ -114,6 +117,12 @@ static void OnUnloaded(object? sender, RoutedEventArgs e) } } + static void RemoveHandlers(ScrollViewer scrollViewer) + { + WeakEventManager.RemoveHandler(scrollViewer, nameof(ScrollViewer.Loaded), OnLoaded); + WeakEventManager.RemoveHandler(scrollViewer, nameof(ScrollViewer.Unloaded), OnUnloaded); + } + static void RemoveHook(ScrollViewer scrollViewer) { if (scrollViewer.GetValue(HorizontalScrollHookProperty) is HwndSourceHook hook && @@ -138,12 +147,20 @@ static void RegisterHook(ScrollViewer scrollViewer) IntPtr Hook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) { const int WM_MOUSEHWHEEL = 0x020E; + const int WM_DESTROY = 0x0002; + const int WM_NCDESTROY = 0x0082; switch (msg) { case WM_MOUSEHWHEEL when scrollViewer.IsMouseOver: int tilt = (short)((wParam.ToInt64() >> 16) & 0xFFFF); scrollViewer.ScrollToHorizontalOffset(scrollViewer.HorizontalOffset + tilt); return (IntPtr)1; + case WM_DESTROY: + case WM_NCDESTROY: + RemoveHandlers(scrollViewer); + var source = PresentationSource.FromVisual(scrollViewer) as HwndSource; + source?.RemoveHook(Hook); + break; } return IntPtr.Zero; } @@ -152,19 +169,19 @@ IntPtr Hook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled public static readonly DependencyProperty BubbleVerticalScrollProperty = DependencyProperty.RegisterAttached( "BubbleVerticalScroll", typeof(bool), typeof(ScrollViewerAssist), new PropertyMetadata(false, OnBubbleVerticalScrollChanged)); - + private static void OnBubbleVerticalScrollChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { - if (d is ScrollViewer scrollViewer) + if (d is ScrollViewer sv) { - if (scrollViewer.IsLoaded) + if (sv.IsLoaded) { - DoOnLoaded(scrollViewer); + DoOnLoaded(sv); } else { - WeakEventManager.AddHandler(scrollViewer, nameof(ScrollViewer.Loaded), OnLoaded); - WeakEventManager.AddHandler(scrollViewer, nameof(ScrollViewer.Unloaded), OnUnloaded); + RegisterForLoadedEvent(sv); + RegisterForUnloadedEvent(sv); } } @@ -176,10 +193,48 @@ static void OnLoaded(object? sender, RoutedEventArgs e) } } + static void UnregisterForLoadedEvent(ScrollViewer sv) + { + WeakEventManager.RemoveHandler(sv, nameof(ScrollViewer.Loaded), OnLoaded); + } + + static void RegisterForLoadedEvent(ScrollViewer sv) + { + // Avoid double registrations + UnregisterForLoadedEvent(sv); + WeakEventManager.AddHandler(sv, nameof(ScrollViewer.Loaded), OnLoaded); + } + + static void UnregisterForUnloadedEvent(ScrollViewer sv) + { + WeakEventManager.RemoveHandler(sv, nameof(ScrollViewer.Unloaded), OnUnloaded); + } + + static void RegisterForUnloadedEvent(ScrollViewer sv) + { + // Avoid double registrations + UnregisterForUnloadedEvent(sv); + WeakEventManager.AddHandler(sv, nameof(ScrollViewer.Unloaded), OnUnloaded); + } + + static void UnregisterForMouseWheelEvent(ScrollViewer sv) + { + sv.RemoveHandler(UIElement.MouseWheelEvent, (RoutedEventHandler)ScrollViewerOnMouseWheel); + } + + static void RegisterForMouseWheelEvent(ScrollViewer sv) + { + // Avoid double registrations + UnregisterForMouseWheelEvent(sv); + sv.AddHandler(UIElement.MouseWheelEvent, (RoutedEventHandler)ScrollViewerOnMouseWheel, true); + } + static void DoOnLoaded(ScrollViewer sv) { if (GetBubbleVerticalScroll(sv)) { + RegisterForUnloadedEvent(sv); + RegisterForMouseWheelEvent(sv); RegisterHook(sv); } else @@ -192,29 +247,55 @@ static void OnUnloaded(object? sender, RoutedEventArgs e) { if (sender is ScrollViewer sv) { - RemoveHook(sv); + UnregisterForUnloadedEvent(sv); + UnregisterForMouseWheelEvent(sv); } } - static void RemoveHook(ScrollViewer scrollViewer) + static void RemoveHook(ScrollViewer sv) { - scrollViewer.RemoveHandler(UIElement.MouseWheelEvent, (RoutedEventHandler)ScrollViewerOnMouseWheel); + var source = PresentationSource.FromVisual(sv) as HwndSource; + if (source is not null && sv.GetValue(BubbleVerticalScrollHookProperty) is HwndSourceHook hook) + { + source.RemoveHook(hook); + sv.SetValue(BubbleVerticalScrollHookProperty, null); + } } - static void RegisterHook(ScrollViewer scrollViewer) + static void RegisterHook(ScrollViewer sv) { - RemoveHook(scrollViewer); - scrollViewer.AddHandler(UIElement.MouseWheelEvent, (RoutedEventHandler)ScrollViewerOnMouseWheel, true); + RemoveHook(sv); + if (PresentationSource.FromVisual(sv) is HwndSource source) + { + HwndSourceHook hook = Hook; + source.AddHook(hook); + sv.SetValue(BubbleVerticalScrollHookProperty, hook); + } + + IntPtr Hook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) + { + const int WM_DESTROY = 0x0002; + const int WM_NCDESTROY = 0x0082; + switch (msg) + { + case WM_DESTROY: + case WM_NCDESTROY: + UnregisterForMouseWheelEvent(sv); + UnregisterForLoadedEvent(sv); + UnregisterForUnloadedEvent(sv); + RemoveHook(sv); + break; + } + return IntPtr.Zero; + } } // This relay is only needed because the UIElement.AddHandler() has strict requirements for the signature of the passed Delegate - static void ScrollViewerOnMouseWheel(object sender, RoutedEventArgs e) => HandleMouseWheel(sender, (MouseWheelEventArgs)e); + static void ScrollViewerOnMouseWheel(object? sender, RoutedEventArgs e) => HandleMouseWheel(sender, (MouseWheelEventArgs)e); - static void HandleMouseWheel(object sender, MouseWheelEventArgs e) + static void HandleMouseWheel(object? sender, MouseWheelEventArgs e) { - var scrollViewer = (ScrollViewer)sender; - - if (scrollViewer.GetVisualAncestry().Skip(1).FirstOrDefault() is not UIElement parentUiElement) + if (sender is not ScrollViewer sv || sv.GetVisualAncestry().Skip(1).FirstOrDefault() is not UIElement parentUiElement) return; // Re-raise the mouse wheel event on the visual parent to bubble it upwards From 84c7f99531e4b5fe81eb3f59cb545f975a7be394 Mon Sep 17 00:00:00 2001 From: Nicolai Henriksen Date: Sun, 8 Mar 2026 12:43:37 +0100 Subject: [PATCH 3/4] Fix issue by for HorizontalScroll --- .../ScrollViewerAssist.cs | 71 +++++++++++-------- 1 file changed, 43 insertions(+), 28 deletions(-) diff --git a/src/MaterialDesignThemes.Wpf/ScrollViewerAssist.cs b/src/MaterialDesignThemes.Wpf/ScrollViewerAssist.cs index 78d703200b..36e00d0df3 100644 --- a/src/MaterialDesignThemes.Wpf/ScrollViewerAssist.cs +++ b/src/MaterialDesignThemes.Wpf/ScrollViewerAssist.cs @@ -64,9 +64,6 @@ public static PaddingMode GetPaddingMode(DependencyObject element) private static readonly DependencyProperty HorizontalScrollHookProperty = DependencyProperty.RegisterAttached( "HorizontalScrollHook", typeof(HwndSourceHook), typeof(ScrollViewerAssist), new PropertyMetadata(null)); - private static readonly DependencyProperty HorizontalScrollSourceProperty = DependencyProperty.RegisterAttached( - "HorizontalScrollSource", typeof(HwndSource), typeof(ScrollViewerAssist), new PropertyMetadata(null)); - private static readonly DependencyProperty BubbleVerticalScrollHookProperty = DependencyProperty.RegisterAttached( "BubbleVerticalScrollHook", typeof(HwndSourceHook), typeof(ScrollViewerAssist), new PropertyMetadata(null)); @@ -76,16 +73,16 @@ public static PaddingMode GetPaddingMode(DependencyObject element) private static void OnSupportHorizontalScrollChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { //Based on: https://blog.walterlv.com/post/handle-horizontal-scrolling-of-touchpad-en.html - if (d is ScrollViewer scrollViewer) + if (d is ScrollViewer sv) { - if (scrollViewer.IsLoaded) + if (sv.IsLoaded) { - DoOnLoaded(scrollViewer); + DoOnLoaded(sv); } else { - WeakEventManager.AddHandler(scrollViewer, nameof(ScrollViewer.Loaded), OnLoaded); - WeakEventManager.AddHandler(scrollViewer, nameof(ScrollViewer.Unloaded), OnUnloaded); + RegisterForLoadedEvent(sv); + RegisterForUnloadedEvent(sv); } } @@ -97,10 +94,35 @@ static void OnLoaded(object? sender, RoutedEventArgs e) } } + static void UnregisterForLoadedEvent(ScrollViewer sv) + { + WeakEventManager.RemoveHandler(sv, nameof(ScrollViewer.Loaded), OnLoaded); + } + + static void RegisterForLoadedEvent(ScrollViewer sv) + { + // Avoid double registrations + UnregisterForLoadedEvent(sv); + WeakEventManager.AddHandler(sv, nameof(ScrollViewer.Loaded), OnLoaded); + } + + static void UnregisterForUnloadedEvent(ScrollViewer sv) + { + WeakEventManager.RemoveHandler(sv, nameof(ScrollViewer.Unloaded), OnUnloaded); + } + + static void RegisterForUnloadedEvent(ScrollViewer sv) + { + // Avoid double registrations + UnregisterForUnloadedEvent(sv); + WeakEventManager.AddHandler(sv, nameof(ScrollViewer.Unloaded), OnUnloaded); + } + static void DoOnLoaded(ScrollViewer sv) { if (GetSupportHorizontalScroll(sv)) { + RegisterForUnloadedEvent(sv); RegisterHook(sv); } else @@ -117,30 +139,23 @@ static void OnUnloaded(object? sender, RoutedEventArgs e) } } - static void RemoveHandlers(ScrollViewer scrollViewer) - { - WeakEventManager.RemoveHandler(scrollViewer, nameof(ScrollViewer.Loaded), OnLoaded); - WeakEventManager.RemoveHandler(scrollViewer, nameof(ScrollViewer.Unloaded), OnUnloaded); - } - - static void RemoveHook(ScrollViewer scrollViewer) + static void RemoveHook(ScrollViewer sv) { - if (scrollViewer.GetValue(HorizontalScrollHookProperty) is HwndSourceHook hook && - scrollViewer.GetValue(HorizontalScrollSourceProperty) is HwndSource source) + var source = PresentationSource.FromVisual(sv) as HwndSource; + if (source is not null && sv.GetValue(HorizontalScrollHookProperty) is HwndSourceHook hook) { source.RemoveHook(hook); - scrollViewer.SetValue(HorizontalScrollHookProperty, null); + sv.SetValue(HorizontalScrollHookProperty, null); } } - static void RegisterHook(ScrollViewer scrollViewer) + static void RegisterHook(ScrollViewer sv) { - RemoveHook(scrollViewer); - if (PresentationSource.FromVisual(scrollViewer) is HwndSource source) + RemoveHook(sv); + if (PresentationSource.FromVisual(sv) is HwndSource source) { HwndSourceHook hook = Hook; - scrollViewer.SetValue(HorizontalScrollSourceProperty, source); - scrollViewer.SetValue(HorizontalScrollHookProperty, hook); + sv.SetValue(HorizontalScrollHookProperty, hook); source.AddHook(hook); } @@ -151,15 +166,15 @@ IntPtr Hook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled const int WM_NCDESTROY = 0x0082; switch (msg) { - case WM_MOUSEHWHEEL when scrollViewer.IsMouseOver: + case WM_MOUSEHWHEEL when sv.IsMouseOver: int tilt = (short)((wParam.ToInt64() >> 16) & 0xFFFF); - scrollViewer.ScrollToHorizontalOffset(scrollViewer.HorizontalOffset + tilt); + sv.ScrollToHorizontalOffset(sv.HorizontalOffset + tilt); return (IntPtr)1; case WM_DESTROY: case WM_NCDESTROY: - RemoveHandlers(scrollViewer); - var source = PresentationSource.FromVisual(scrollViewer) as HwndSource; - source?.RemoveHook(Hook); + UnregisterForLoadedEvent(sv); + UnregisterForUnloadedEvent(sv); + RemoveHook(sv); break; } return IntPtr.Zero; From 487cc2ceb097f96dba216a1ef1dd61095cd5c559 Mon Sep 17 00:00:00 2001 From: Nicolai Henriksen Date: Sun, 8 Mar 2026 13:13:21 +0100 Subject: [PATCH 4/4] Cleanup: revert not needed signature change --- src/MaterialDesignThemes.Wpf/ScrollViewerAssist.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/MaterialDesignThemes.Wpf/ScrollViewerAssist.cs b/src/MaterialDesignThemes.Wpf/ScrollViewerAssist.cs index 36e00d0df3..11f3658d4e 100644 --- a/src/MaterialDesignThemes.Wpf/ScrollViewerAssist.cs +++ b/src/MaterialDesignThemes.Wpf/ScrollViewerAssist.cs @@ -306,11 +306,13 @@ IntPtr Hook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled } // This relay is only needed because the UIElement.AddHandler() has strict requirements for the signature of the passed Delegate - static void ScrollViewerOnMouseWheel(object? sender, RoutedEventArgs e) => HandleMouseWheel(sender, (MouseWheelEventArgs)e); + static void ScrollViewerOnMouseWheel(object sender, RoutedEventArgs e) => HandleMouseWheel(sender, (MouseWheelEventArgs)e); - static void HandleMouseWheel(object? sender, MouseWheelEventArgs e) + static void HandleMouseWheel(object sender, MouseWheelEventArgs e) { - if (sender is not ScrollViewer sv || sv.GetVisualAncestry().Skip(1).FirstOrDefault() is not UIElement parentUiElement) + var scrollViewer = (ScrollViewer)sender; + + if (scrollViewer.GetVisualAncestry().Skip(1).FirstOrDefault() is not UIElement parentUiElement) return; // Re-raise the mouse wheel event on the visual parent to bubble it upwards