Fix System.OverflowException in CustomScrollViewerEx during Mouse Wheel scroll#4497
Fix System.OverflowException in CustomScrollViewerEx during Mouse Wheel scroll#44974yinn wants to merge 5 commits into
Conversation
📝 WalkthroughWalkthroughAdds an early-return guard in CustomScrollViewerEx.OnMouseWheel when ActualHeight <= 0, minor non-functional formatting in the control, a ProjectReference from the test project to the main project, and a new NUnit STA test that invokes the non-public OnMouseWheel via reflection and asserts it does not throw. ChangesMouseWheel Handler Robustness
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related issues
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
Flow.Launcher/Resources/Controls/CustomScrollViewerEx.cs (1)
91-101:⚠️ Potential issue | 🟠 Major | ⚡ Quick winReset animation state on guard-path returns.
SetIsAnimating(this, true)is set before guard checks, but both guard returns exit without resetting it. That can leave animation state stuck astrueand suppress offset state updates inOnScrollChanged.Suggested fix
- ScrollViewerBehavior.SetIsAnimating(this, true); - if (Direction == Orientation.Vertical) { + if (ActualHeight <= 0) + return; + + ScrollViewerBehavior.SetIsAnimating(this, true); + if (ScrollableHeight > 0) { e.Handled = true; } - - if (ActualHeight <= 0) - return;else { + if (ActualWidth <= 0) + return; + + ScrollViewerBehavior.SetIsAnimating(this, true); + if (ScrollableWidth > 0) { e.Handled = true; } - - if (ActualHeight <= 0) - return;Also applies to: 133-134
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Flow.Launcher/Resources/Controls/CustomScrollViewerEx.cs` around lines 91 - 101, The handler sets ScrollViewerBehavior.SetIsAnimating(this, true) before guard checks but returns on two guard paths without resetting it; update the method in CustomScrollViewerEx (the scroll event handler that calls SetIsAnimating) so that you either move the SetIsAnimating(this, true) call to after the guard checks or ensure you call ScrollViewerBehavior.SetIsAnimating(this, false) before each early return (including the guard near the Vertical Orientation block and the other guard around lines ~133-134) so the animation flag is never left true when exiting early.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@Flow.Launcher.Test/MouseWheelTest.cs`:
- Around line 26-34: The reflection lookup for CustomScrollViewerEx's
"OnMouseWheel" is not asserted before Invoke, which causes a confusing
null-reference if the method isn't found; add an explicit null assertion (e.g.
Assert.IsNotNull or Assert.NotNull) for onMouseWheelMethod after the GetMethod
call with a clear message like "OnMouseWheel method not found on
CustomScrollViewerEx" so failures are diagnostic, then proceed to call
onMouseWheelMethod.Invoke(scrollView, new object[] { e }) inside the existing
Assert.DoesNotThrow block.
In `@Flow.Launcher/Resources/Controls/CustomScrollViewerEx.cs`:
- Around line 133-136: In CustomScrollViewerEx (the mouse wheel/scroll handling
code that computes WheelChange using e.Delta * (ViewportWidth / 1.5) /
ActualWidth), change the early-return guard to validate ActualWidth instead of
ActualHeight (i.e., if (ActualWidth <= 0) return;) so you never divide by zero
when computing WheelChange; update the check in the same method where
WheelChange is declared to prevent the infinite offset on horizontal scrolling.
---
Outside diff comments:
In `@Flow.Launcher/Resources/Controls/CustomScrollViewerEx.cs`:
- Around line 91-101: The handler sets ScrollViewerBehavior.SetIsAnimating(this,
true) before guard checks but returns on two guard paths without resetting it;
update the method in CustomScrollViewerEx (the scroll event handler that calls
SetIsAnimating) so that you either move the SetIsAnimating(this, true) call to
after the guard checks or ensure you call
ScrollViewerBehavior.SetIsAnimating(this, false) before each early return
(including the guard near the Vertical Orientation block and the other guard
around lines ~133-134) so the animation flag is never left true when exiting
early.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: e62e2923-df1f-435a-97a3-52643a176be2
📒 Files selected for processing (3)
Flow.Launcher.Test/Flow.Launcher.Test.csprojFlow.Launcher.Test/MouseWheelTest.csFlow.Launcher/Resources/Controls/CustomScrollViewerEx.cs
| var onMouseWheelMethod = typeof(CustomScrollViewerEx).GetMethod( | ||
| "OnMouseWheel", | ||
| BindingFlags.NonPublic | BindingFlags.Instance | ||
| ); | ||
|
|
||
| Assert.DoesNotThrow(() => | ||
| { | ||
| onMouseWheelMethod.Invoke(scrollView, new object[] { e }); | ||
| }); |
There was a problem hiding this comment.
Assert reflected method lookup before invocation.
Add an explicit null assertion for onMouseWheelMethod so failures are diagnostic (method not found) instead of a generic null-reference during invoke.
Suggested fix
var onMouseWheelMethod = typeof(CustomScrollViewerEx).GetMethod(
"OnMouseWheel",
BindingFlags.NonPublic | BindingFlags.Instance
);
+ Assert.That(onMouseWheelMethod, Is.Not.Null, "OnMouseWheel method was not found via reflection.");
Assert.DoesNotThrow(() =>
{
onMouseWheelMethod.Invoke(scrollView, new object[] { e });
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| var onMouseWheelMethod = typeof(CustomScrollViewerEx).GetMethod( | |
| "OnMouseWheel", | |
| BindingFlags.NonPublic | BindingFlags.Instance | |
| ); | |
| Assert.DoesNotThrow(() => | |
| { | |
| onMouseWheelMethod.Invoke(scrollView, new object[] { e }); | |
| }); | |
| var onMouseWheelMethod = typeof(CustomScrollViewerEx).GetMethod( | |
| "OnMouseWheel", | |
| BindingFlags.NonPublic | BindingFlags.Instance | |
| ); | |
| Assert.That(onMouseWheelMethod, Is.Not.Null, "OnMouseWheel method was not found via reflection."); | |
| Assert.DoesNotThrow(() => | |
| { | |
| onMouseWheelMethod.Invoke(scrollView, new object[] { e }); | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@Flow.Launcher.Test/MouseWheelTest.cs` around lines 26 - 34, The reflection
lookup for CustomScrollViewerEx's "OnMouseWheel" is not asserted before Invoke,
which causes a confusing null-reference if the method isn't found; add an
explicit null assertion (e.g. Assert.IsNotNull or Assert.NotNull) for
onMouseWheelMethod after the GetMethod call with a clear message like
"OnMouseWheel method not found on CustomScrollViewerEx" so failures are
diagnostic, then proceed to call onMouseWheelMethod.Invoke(scrollView, new
object[] { e }) inside the existing Assert.DoesNotThrow block.
There was a problem hiding this comment.
1 issue found across 3 files
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@Flow.Launcher/Resources/Controls/CustomScrollViewerEx.cs`:
- Around line 133-134: In OnMouseWheel of CustomScrollViewerEx the method sets
the animating flag (IsAnimating) earlier but returns early when ActualWidth <= 0
without clearing it, which can leave IsAnimating stuck; modify OnMouseWheel to
reset IsAnimating (or the animating field) to false before the early return so
the animation state is cleared and OnScrollChanged can continue syncing offsets.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: ea371989-0f85-4582-a0d5-ae6ec2cb5aef
📒 Files selected for processing (1)
Flow.Launcher/Resources/Controls/CustomScrollViewerEx.cs
There was a problem hiding this comment.
1 issue found across 1 file (changes from recent commits).
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
Flow.Launcher/Resources/Controls/CustomScrollViewerEx.cs (1)
100-101:⚠️ Potential issue | 🟠 Major | ⚡ Quick winVertical guard must also reset animation state.
The horizontal path (lines 133-137) correctly resets
IsAnimatingtofalsebefore returning, but the vertical path here does not. SinceSetIsAnimating(true)is called at line 91, this early return will leaveIsAnimatingstuck, preventingOnScrollChanged(line 168) from syncing offsets.Proposed fix
- if (ActualHeight <= 0) - return; + if (ActualHeight <= 0) + { + ScrollViewerBehavior.SetIsAnimating(this, false); + return; + }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Flow.Launcher/Resources/Controls/CustomScrollViewerEx.cs` around lines 100 - 101, The vertical early-return when ActualHeight <= 0 fails to reset the animation flag and leaves IsAnimating true; update the vertical guard in the same method that calls SetIsAnimating(true) to call SetIsAnimating(false) (or otherwise reset IsAnimating) before returning so the animation state is cleared and OnScrollChanged can resync offsets; mirror the horizontal path's behavior that resets IsAnimating to false prior to returning.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@Flow.Launcher/Resources/Controls/CustomScrollViewerEx.cs`:
- Around line 100-101: The vertical early-return when ActualHeight <= 0 fails to
reset the animation flag and leaves IsAnimating true; update the vertical guard
in the same method that calls SetIsAnimating(true) to call SetIsAnimating(false)
(or otherwise reset IsAnimating) before returning so the animation state is
cleared and OnScrollChanged can resync offsets; mirror the horizontal path's
behavior that resets IsAnimating to false prior to returning.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 9ed14a09-ed58-4711-9720-2e30a96598f7
📒 Files selected for processing (1)
Flow.Launcher/Resources/Controls/CustomScrollViewerEx.cs
There was a problem hiding this comment.
♻️ Duplicate comments (1)
Flow.Launcher/Resources/Controls/CustomScrollViewerEx.cs (1)
90-91:⚠️ Potential issue | 🔴 Critical | ⚡ Quick winDirection-agnostic guard reintroduces horizontal divide-by-zero path.
The new guard checks only height, but horizontal scrolling still divides by
ActualWidth(Line 133). IfActualWidth == 0,WheelChangecan become infinite again and propagate into animation timing.Suggested minimal fix
protected override void OnMouseWheel(MouseWheelEventArgs e) { - if (ActualHeight <= 0) - return; - - var Direction = GetDirection(); + var Direction = GetDirection(); + if ((Direction == Orientation.Vertical && ActualHeight <= 0) || + (Direction == Orientation.Horizontal && ActualWidth <= 0)) + { + return; + } + ScrollViewerBehavior.SetIsAnimating(this, true);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Flow.Launcher/Resources/Controls/CustomScrollViewerEx.cs` around lines 90 - 91, The early return only checks ActualHeight but the code later computes WheelChange by dividing by ActualWidth (risking divide-by-zero); update the guard in CustomScrollViewerEx (the method containing the WheelChange calculation) to check both ActualHeight and ActualWidth (return if either <= 0) or otherwise protect the WheelChange computation by ensuring ActualWidth is non-zero before dividing; reference ActualWidth, ActualHeight and WheelChange to locate and fix the division-by-zero path.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Duplicate comments:
In `@Flow.Launcher/Resources/Controls/CustomScrollViewerEx.cs`:
- Around line 90-91: The early return only checks ActualHeight but the code
later computes WheelChange by dividing by ActualWidth (risking divide-by-zero);
update the guard in CustomScrollViewerEx (the method containing the WheelChange
calculation) to check both ActualHeight and ActualWidth (return if either <= 0)
or otherwise protect the WheelChange computation by ensuring ActualWidth is
non-zero before dividing; reference ActualWidth, ActualHeight and WheelChange to
locate and fix the division-by-zero path.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 326c3eca-b827-4876-857e-e9b013a3b57e
📒 Files selected for processing (1)
Flow.Launcher/Resources/Controls/CustomScrollViewerEx.cs
This PR fixes the OverflowException reported in #4481, which occurs when the user scrolls on a ScrollViewer that has not been fully rendered yet or whose ActualHeight is 0.
Root Cause
The issue originates from the following calculation inside OnMouseWheel:
When ActualHeight is 0, the result becomes double.Infinity.
This value is later passed into the animation logic, which eventually converts it into a TimeSpan using TimeSpan.FromTicks. Since Infinity cannot be represented as a valid duration, a System.OverflowException is thrown.
Changes
Added a guard clause in CustomScrollViewerEx.cs to return early when:
Prevented invalid Infinity/NaN scroll offsets from reaching the animation pipeline.
Ensured the animation logic only receives finite values.
Testing
Added MouseWheelTest.cs to simulate a MouseWheelEvent on a CustomScrollViewerEx with zero dimensions.
Verified that:
without the fix, the scroll logic attempts to process an infinite offset;
with the fix, the method exits safely without triggering an exception.
I’m also open to feedback if you think there’s a cleaner or more robust approach for handling this scenario.
Summary by cubic
Prevents an OverflowException in
CustomScrollViewerExwhen mouse-wheel scrolling before layout. Adds an early height guard and blocks non-finite deltas from reaching animation.Summary of changes
OnMouseWheelnow returns immediately whenActualHeight <= 0; guard moved to method start to avoid computingWheelChangeon zero height; only finite deltas proceed to animation. Minor formatting updates from mergingdevwith no behavior changes.MouseWheelTestthat invokesOnMouseWheelon an unrendered control; test project now referencesFlow.Launcher.Release Note
Fixes a crash when using the mouse wheel to scroll before the window finishes rendering.
Written for commit 7e3aa4a. Summary will update on new commits.