Home > OS >  WPF Scrollviewer: How to allow scrolling via touch/mouse and allow inner children (buttons) to trigg
WPF Scrollviewer: How to allow scrolling via touch/mouse and allow inner children (buttons) to trigg

Time:12-16

I have a scrollviewer with an itemscontrol, and inside of the itemscontrol are buttons. Here is the xaml:

`

    <ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
        <ItemsControl Style="{DynamicResource FileItemsControlStyle}" ItemsSource="{Binding Files.Files}"
                Padding="4" ManipulationBoundaryFeedback="FileListBox_ManipulationBoundaryFeedback">
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <Button Style="{StaticResource ImportsFileBorder}" Margin="0, 2" Height="75" HorizontalAlignment="Stretch">
                    <b:Interaction.Triggers>
                        <b:EventTrigger EventName="PreviewMouseDown">
                            <b:InvokeCommandAction Command="{Binding Path=DataContext.RenderSelectedFileCommand,
                                        RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}" CommandParameter="{Binding}"/>
                        </b:EventTrigger>
                    </b:Interaction.Triggers>
                    <DockPanel LastChildFill="True" ScrollViewer.HorizontalScrollBarVisibility="Disabled">
                        <ToggleButton IsChecked="{Binding IsSelected, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" 
                                      PreviewMouseDown="ToggleSliderButton_PreviewMouseDown" Style="{DynamicResource ToggleSliderStyle}"
                                      DockPanel.Dock="Left" Margin="10,15,10,0" />

                        <TextBlock Text="{Binding DisplayFileName}" Width="Auto" VerticalAlignment="Center" FontWeight="Bold" TextTrimming="CharacterEllipsis"
                                   FontSize="{DynamicResource LargeFontSize}" Foreground="Lime"/>
                    </DockPanel>
                </Button>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
        </ItemsControl>
    </ScrollViewer>

`

My issue is that if I have PanningMode = "Both" for the scrollviewer I can scroll via my mouse and touch device perfectly fine. And if I click a button with my mouse, the style triggers and it highlights. However with touch, the buttons are not highlighting (but the buttons commands do fire). If I turn PanningMode too "None" then the styles trigger on the buttons fine (but I can't scroll obviously). I need to be able to scroll through the list of buttons even if my touch event starts on a button but somehow determine if I'm actually just clicking the button or if I'm scrolling. Ideally if I'm scrolling, the button I started on to begin the scrolling wouldn't highlight.

I found another Stackoverflow article here: WPF, ScrollViewer consuming touch before longpress

and tried implementing the answer from there:

`

public class ScrollViewerWithTouch : ScrollViewer   
{
      /// <summary>
      /// Original panning mode.
      /// </summary>
      private PanningMode panningMode;

  /// <summary>
  /// Set panning mode only once.
  /// </summary>
  private bool panningModeSet;

  /// <summary>
  /// Initializes static members of the <see cref="ScrollViewerWithTouch"/> class.
  /// </summary>
  static ScrollViewerWithTouch()
  {
     DefaultStyleKeyProperty.OverrideMetadata(typeof(ScrollViewerWithTouch), new FrameworkPropertyMetadata(typeof(ScrollViewerWithTouch)));
  }

  protected override void OnManipulationCompleted(ManipulationCompletedEventArgs e)
  {
     base.OnManipulationCompleted(e);

     // set it back
     this.PanningMode = this.panningMode;
  }

  protected override void OnManipulationStarted(ManipulationStartedEventArgs e)
  {
     // figure out what has the user touched
     var result = VisualTreeHelper.HitTest(this, e.ManipulationOrigin);
     if (result != null && result.VisualHit != null)
     {
        var hasButtonParent = this.HasButtonParent(result.VisualHit);

        // if user touched a button then turn off panning mode, let style bubble down, in other case let it scroll
        this.PanningMode = hasButtonParent ? PanningMode.None : this.panningMode;
     }

     base.OnManipulationStarted(e);
  }

  protected override void OnTouchDown(TouchEventArgs e)
  {
     // store panning mode or set it back to it's original state. OnManipulationCompleted does not do it every time, so we need to set it once more.
     if (this.panningModeSet == false)
     {
        this.panningMode = this.PanningMode;
        this.panningModeSet = true;
     }
     else
     {
        this.PanningMode = this.panningMode;
     }

     base.OnTouchDown(e);         
  }

  private bool HasButtonParent(DependencyObject obj)
  {
     var parent = VisualTreeHelper.GetParent(obj);

     if ((parent != null) && (parent is ButtonBase) == false)
     {
        return HasButtonParent(parent);
     }

     return parent != null;
  }
}

`

However, this doesn't allow me to start scrolling if my touch starts on the buttons. Is there some way I could modify this too allow me to scroll when needed but then turn panning mode off if I'm just touching/clicking a button and allow the style too trigger?

CodePudding user response:

I ended up modifying the custom scrollviewer and creating a custom button class that inherits from ButtonBase that exposes a SetIsPressable() function to allow me to set the IsPressable on the button that is readonly since my style triggers off of that property.

Here is how I modified the scrollviewer:

namespace NoNeedForYouToKnow.UI.Module.Control.TouchScrollViewer
{
    /// <summary>
    /// This scroll viewer is meant to be able to handle touch/mouse scrolling as well as notifying child components that implement
    /// IPressable that their IsPressed property has changed. If you aren't wrapping anything implementing IPressable, then you
    /// probably just need to use a regular scrollviewer.
    /// </summary>
    public class PressableScrollViewer : ScrollViewer
    {
        private IPressable _button;

        /// <summary>
        /// Initializes static members of the <see cref="PressableScrollViewer"/> class.
        /// </summary>
        static PressableScrollViewer()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(PressableScrollViewer), new FrameworkPropertyMetadata(typeof(PressableScrollViewer)));
        }

        protected override void OnManipulationCompleted(ManipulationCompletedEventArgs e)
        {
            _button?.SetIsPressed(false);
            base.OnManipulationCompleted(e);
        }

        protected override void OnManipulationDelta(ManipulationDeltaEventArgs e)
        {
            var result = VisualTreeHelper.HitTest(this, e.ManipulationOrigin);
            if (result != null && result.VisualHit != null)
            {
                var isOverButton = this.HasParent(result.VisualHit, _button);
                if(!isOverButton)
                    _button.SetIsPressed(false);
                base.OnManipulationDelta(e);
            }
        }

        protected override void OnScrollChanged(ScrollChangedEventArgs e)
        {
            _button?.SetIsPressed(false);
            base.OnScrollChanged(e);
        }

        protected override void OnManipulationStarted(ManipulationStartedEventArgs e)
        {
            // figure out what has the user touched
            var result = VisualTreeHelper.HitTest(this, e.ManipulationOrigin);
            if (result != null && result.VisualHit != null)
            {
                var button = this.HasButtonParent(result.VisualHit);
                if (button != null)
                {
                    _button = button;
                    button.SetIsPressed(true);
                }
            }

            base.OnManipulationStarted(e);
        }

        private PressableButton HasButtonParent(DependencyObject obj)
        {
            var parent = VisualTreeHelper.GetParent(obj);

            if ((parent != null) && (parent is PressableButton) == false)
            {
                return HasButtonParent(parent);
            }

            return (PressableButton) parent;
        }

        private bool HasParent(DependencyObject obj, IPressable pressableObject)
        {
            if(pressableObject == null) return false;
            var parent = VisualTreeHelper.GetParent(obj);

            if (parent == pressableObject || obj == pressableObject) return true;
            else if (parent == null)
                return false;
            else
                return HasParent(parent, pressableObject);
        }
    }
}

It handles toggling the IsPressable property when you start your touch on the button and handles turning to false at the proper times.

  • Related