zposten zposten - 2 months ago 37
C# Question

UWP ScrollViewer - Differentiate between user scrolling and programmatic scrolling

I have an application where I need to take a certain action when the user gets to a certain place in a

ScrollViewer
. This action sometimes includes scrolling the
ScrollViewer
to a different location programmatically.

In order to moniter the user's scrolling action, I am listening for the
ViewChanged
event of the
ScrollViewer
. The issue is that when I scroll progrmatically from within the
ViewChanged
event handler, that same event handler ends up getting called again, causing undesired additional scrolling to happen.

I have tried creating a custom method to remove the event handler before calling
ScrollViewer.ChangeView()
, but this seems to have no effect.

Can anyone come up with a way around this issue, or a way to differentiate the user's scrolling action from my programmatic one?

private void MyScrollViewer_ViewChanged(object sender, ScrollViewerViewChangedEventArgs e)
{
if (conditionals)
{
ScrollTo(location);
}
}

private void ScrollTo(double offset)
{
MyScrollViewer.ViewChanged -= MyScrollViewer_ViewChanged;
MyScrollViewer.ChangeView(offset, null, null);
MyScrollViewer.ViewChanged += MyScrollViewer_ViewChanged;
}

Answer

It is, unfortunately, not possible to determine what triggered a ViewChanged event. It is however possible to solve this problem.

The issue is that ChangeView() is asynchronous, so re-adding the event handler immediately after calling ChangeView is too soon. ChangeView will raise a bunch of ViewChanged events with a final one where e.IsIntermediate == false; only once that happens should you re-hook the event handler. The best way to handle this might be to use a temporary event handler that waits for that e.IsIntermediate == false and then re-hooks the original handler.

To prevent the user from interacting with the ScrollViewer during the execution of ChangeView, the scroll and zoom modes can be temporarily disabled.

Finally, if the user is manipulating the ScrollViewer when the conditionals are met, that manipulation needs to be canceled before calling ScrollTo().

EDIT: In my implementation, an issue arose where because of the number of times these handlers were called, event handlers were added more than once. To combat this, I've taken the simple strategy from here.

private void MyScrollViewer_ViewChanged(object sender, ScrollViewerViewChangedEventArgs e) 
{
    if (!conditionals) return;

    if (e.IsIntermediate) 
    {
        var uiElement = MyScrollViewer.Content as UIElement;
        uiElement?.CancelDirectManipulations();
    }

    ScrollTo(location);
}

private void Temporary_ViewChanged(object sender, ScrollViewerViewChangedEventArgs e)
{
    if (e.IsIntermediate) return;
    MyScrollViewer.ViewChanged -= Temporary_ViewChanged;
    MyScrollViewer.ViewChanged -= MyScrollViewer_ViewChanged;
    MyScrollViewer.ViewChanged += MyScrollViewer_ViewChanged;

    MyScrollViewer.HorizontalScrollMode = ScrollMode.Enabled;
    MyScrollViewer.VerticalScrollMode = ScrollMode.Enabled;
    MyScrollViewer.ZoomMode = ZoomMode.Enabled;
}

private void ScrollTo(double offset)
{
    MyScrollViewer.ViewChanged -= MyScrollViewer_ViewChanged;
    MyScrollViewer.ViewChanged -= Temporary_ViewChanged;
    MyScrollViewer.ViewChanged += Temporary_ViewChanged;

    MyScrollViewer.HorizontalScrollMode = ScrollMode.Disabled;
    MyScrollViewer.VerticalScrollMode = ScrollMode.Disabled;
    MyScrollViewer.ZoomMode = ZoomMode.Disabled;

    MyScrollViewer.ChangeView(offset, null, null);
}