UWP ListView scroll bar not selectable in touch mode - c#

I have a simple ListView in a Universal Windows App. It scrolls just fine both on a touch-enabled device and on my local machine. On the touch device I would like to be able to tap and drag the scroll bar thumb in order to quickly scroll through the list (same as you would get clicking and dragging the scroll thumb with a mouse). But, when I try to select the scroll bar thumb it does not work; the thumb is not selectable.
At first I thought the scroll thumb was just too small, so I fiddled with the default style of ScrollBar to increase the width. Then I tried tweaking other values in the default ScrollViewer style, like the ScrollingIndicatorMode and ScrollingIndicatorStates so that they would always use MouseIndicator, and making sure all IsHitTestVisible are True. To no avail.
It seems like there must be something in the default styles that allows this but I can't find it through trial and error, and no where in the MSDN docs appears to address this styling.
Is this doable in touch mode?

Is this doable in touch mode?
AFAIK, this is not possible currently. The scroll event of ScrollBar can only be triggered by scrolling the content or moving the Thumb by mouse input.
This is by design, but on the touch device if we want to quickly scroll through the list, we just quickly swipe one item for a short distance, the ListView will firstly speed up the scrolling and then slow down and finally stop. Other controls which contain a ScrollViewer behave the same, in the viewport(content of ScrollViewer) when in touch device, moving the thumb is not work, to speed up the scrolling, you can only give quick swipe gesture on the viewport.
Our suggestion is that you may submit a request to add this new features for developing through the Windows Feedback tool.

For posterity's sake I want to post my work around which actually turned out not being quite as difficult as I thought and looks really good now.
The basic idea is to use a Slider control (has a Thumb that is select-able, drag-able, and moves along a track) with a vertical orientation and to sync the Slider's Value property (which changes when dragged) to the ListView's ScrollIntoView method so you can scroll the ListView as the Slider Thumb is dragged. Vice-versa (move the Slider Thumb when the ListView scrolls normally) is also required for a clean, seamless experience. I have some code sample below to get you started (not everything is there to work right away). In addition, edit the Slider's default Style template to make it look exactly like a scroll bar (and even even add a tooltip to display the current value when dragging).
Note: I'm using WinRTXamlToolkit in code-behind to traverse the VisualTree easily.
Code behind:
public sealed partial class MainPage : Page
{
private bool _isScroll = false;
private bool _isSlide = false;
public MainPage()
{
this.InitializeComponent();
var vm = new ViewModel();
vm.ValueChanged += LetterSliderValueChanged;
DataContext = vm;
}
/// <summary>
/// Bring list items into view on the screen based on the value (letter) of the slider
/// </summary>
/// <param name="sender">The view model to bring into view</param>
private void LetterSliderValueChanged(object sender, RoutedEventArgs e)
{
if (_isScroll) return;
if (sender == null) return;
_isSlide = true;
ListView?.ScrollIntoView(sender, ScrollIntoViewAlignment.Leading);
}
/// <summary>
/// Update the position of the slider when the ListView is scrolling from a normal touch
/// </summary>
private void ScrollViewerViewChanged(object sender, ScrollViewerViewChangedEventArgs e)
{
if (_isSlide)
{
_isSlide = false;
return;
}
_isScroll = true;
var scrollViewer = sender as ScrollViewer;
var scrollBars = scrollViewer.GetDescendantsOfType<ScrollBar>();
var verticalBar = scrollBars.FirstOrDefault(x => x.Orientation == Orientation.Vertical);
// Normalize the scales to move the slider thumb in sync with scrolling
var sliderTotal = LetterSlider.Maximum - LetterSlider.Minimum;
var barTotal = verticalBar.Maximum - verticalBar.Minimum;
var barPercent = verticalBar.Value / barTotal;
LetterSlider.Value = (barPercent * sliderTotal) + LetterSlider.Minimum;
_isScroll = false;
}
/// <summary>
/// Add the slider method to the ListView's ScrollViewer Viewchanged event
/// </summary>
private void ListViewLoaded(object sender, RoutedEventArgs e)
{
var listview = sender as ListView;
if (listview == null) return;
var scrollViewer = listview.GetFirstDescendantOfType<ScrollViewer>();
scrollViewer.ViewChanged -= ScrollViewerViewChanged;
scrollViewer.ViewChanged += ScrollViewerViewChanged;
}
}
XAML:
<Slider x:Name="LetterSlider" Orientation="Vertical"
Value="{Binding SliderValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Maximum="{Binding MaxSliderLetter, Mode=OneTime}"
Minimum="{Binding MinSliderLetter, Mode=OneTime}"
RenderTransformOrigin="0.5,0.5">
<Slider.RenderTransform>
<RotateTransform Angle="180"/>
</Slider.RenderTransform>
</Slider>
View Model:
//Method to update the ListView to show items based on the letter of the slider
public RoutedEventHandler ValueChanged;
public int SliderValue
{
get { return _sliderValue; }
set
{
_sliderValue = value;
NotifyPropertyChanged("SliderValue");
char letter = (char)_sliderValue;
var items = ItemsGroup.FirstOrDefault(i => (char)i.Key == letter);
if (items == null) return;
ValueChanged(items.FirstOrDefault(), new RoutedEventArgs());
}
}

Related

Horizontal scroll on ListView

I'm currently capturing the PointerMoved event on the page to use with a horizontal menu. So the user can swipe left/right and the page will animate accordingly.
This works when the user touches a static element (TextBlock etc.) but if they touch a ListView it captures the touch events.
How can I implement the ListView so when the user scrolls vertically it works as normal, but when the user scrolls horizontally it passes the events to my code?
It is possible, but you will need a small trick. As a refference I put here Rob Caplan's article.
Let's start:
First - where are your events? - answer is simple - while you have ScrollViewer enabled, all events are intercepted by it and handeled. You ListView will get only PointerEntered event and just after it PointerExited, all further proccesing is handeled by ScrollViewer. That is the problem. But as I've said there is a method to do what you want.
For this purpose lets assume that you have defined your ListView only with VerticalScroll:
<ListView Name="myList" ScrollViewer.HorizontalScrollMode="Disabled">
Of course it is possible to do for both directions, but it's a simple example.
Now let's have a look at constructor of a Page:
PointerPoint firstPoint = null;
ScrollViewer listScrollviewer = null;
public MainPage()
{
this.InitializeComponent();
myList.ItemsSource = yourItemSource;
myList.PointerEntered += myList_PointerEntered;
myList.PointerMoved += myList_PointerMoved;
}
Nothing weird here - I just subscribe to events, and declare two variables firstPoint and listScrollviewer, which I'll need later.
We will need also to get our ScrollViewer of our ListView - the following method will do the job:
public static ScrollViewer GetScrollViewer(DependencyObject depObj)
{
if (depObj is ScrollViewer) return depObj as ScrollViewer;
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
{
var child = VisualTreeHelper.GetChild(depObj, i);
var result = GetScrollViewer(child);
if (result != null) return result;
}
return null;
}
Now - to enable our events we will need to disable the ScrollViewer:
private ScrollViewer DisableScrolling(DependencyObject depObj)
{
ScrollViewer foundOne = GetScrollViewer(depObj);
if (foundOne != null) foundOne.VerticalScrollMode = ScrollMode.Disabled;
return foundOne;
}
We will disable the ScrollViewer upon PointerEntered event which is fired. In this step we will also remember the pressed PointerPoint - as we have disable Scrollviewer, we will have to scroll it manually - that is what we need this PointerPoint for.
private void myList_PointerEntered(object sender, PointerRoutedEventArgs e)
{
firstPoint = e.GetCurrentPoint(myList);
if (listScrollviewer == null) listScrollviewer = DisableScrolling(myList);
}
Finally our PointerMoved event, which now wil be fired as we had disabled ScrollViewer - moving ScrollViewer + other code you need to put there:
private void myList_PointerMoved(object sender, PointerRoutedEventArgs e)
{
if (listScrollviewer != null)
{
PointerPoint secondPoint = e.GetCurrentPoint(myList);
double verticalDifference = secondPoint.Position.Y - firstPoint.Position.Y;
listScrollviewer.ChangeView(null, listScrollviewer.VerticalOffset - verticalDifference, null);
}
// some other code you need
}
Few remarks:
this method still needs much tuning, but hopefuly will show you how to achieve your goal,
you may need also to separate some small horizontal movements from vertical ones,
if your ListView or other Control has horizontal scroll, then you will also need to disable and handle it,
this method won't probably work so smooth like original ScrollViewer.
I've also put a simple working example here at OneDrive.

WPF, ScrollViewer consuming touch before longpress

I'm kinda new to this WPF world.
And I'm kinda confused on why my scrollview is consuming my touch event.
My current situation is this:
I have this ScrollViewer with an ItemsControl. In this ItemsControl I'm using a Wrappanel to show a number of Rectangles. My ScrollViewer should make it possible to scroll vertically to show the Rectangles that are wrapped downward.
On each of these Rectangles I made a CustomBehaviour with all kinds of handlers.
One of these handlers is a 'creatively' made way to handle LongPressGestures.
The problem is as follows, before my longpress is detected by the behaviour, my ScrollViewer is capturing my TouchDevice in its PreviewTouchMove handler.
How can I prevent my ScrollViewer from capturing my TouchDevice too early?
How can I make sure that I can scroll my ScrollViewer and do my LongPress, DoubleClick (which still works), SingleClick (which still works as well) and other gestures I might add to this custom behaviour?
I've found similar questions on stackoverflow and google that I just not figure out for my specific case.
Something with a CustomThumb <-- This link solves the problem by making a CustomThumb. Can I somehow re-use this for my behaviour? By capturing the TouchDevice early in my behaviour handlers?
If all things fail. Is there an alternative to this ScrollViewer & CustomBehaviour combination?
EDIT:
In the meantime. I retried the CustomThumb-method. I got the longpress to work now from my CustomBehaviour while the UIElements (with that behaviour) are located on a ScrollViewer.
However the scroll functionality of the ScrollViewer still does not work.
The bounty I added will also be awarded to the person that helps me get that to work properly again (since the answer should lie in the same direction as this CustomThumb solution).
I had a similar issue before. I could not use scrolling, touch and style in the same time. At the end I developped a custom ScrollViewer. It is easier than you think, since you only need to watch / change some basic methods.
In your case you can check whether the user pressed on an empy surface or a list item. If it is a list item, you need to check whether it was a short press (so, touchup also occured after touchdown) or a long one.
Scrolling can be configured with PanningMode. It allows you to scroll with finger all over the usercontrol.
Here is my version of scrollviewer. It turns scrolling mode off when user pressed a button and turned on afterwards.
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;
}
}

How can I show a vertical line on mouse over?

I'm trying to show a vertical line in position X of the cursor. I'm trying to do that in a slider control, but I couldn't made it. This will be showed while the user has its cursor on it.
I only know that I need to modify the template. Is so much difficult to do that? If not, can you help me?
Thanks.
This is not easy to attain using templating because of the fact that mouse position is not a dependency property and mouse move is not a routed event. This really comes down to what you want to do, if it's to just show a vertical line whether the mouse is then I agree with Dany to use adorners, as you're not really interested in the slider itself. However, if it's to move the thumb to where the mouse is I would use an attached property.
The reason I use attached properties rather than hooking up the events directly on the control is that it provides more modularised and reusable codebase and makes it more obvious of the visual effect in the XAML, rather than needing to look at C# code as well.
Here's what I would suggest
public class SliderHelperPackage
{
public static readonly DependencyProperty BindThumbToMouseProperty = DependencyProperty.RegisterAttached(
"BindThumbToMouse", typeof(bool), typeof(SliderHelperPackage), new PropertyMetadata(false, OnBindThumbToMouseChanged));
public static void SetBindThumbToMouse(UIElement element, bool value)
{
element.SetValue(BindThumbToMouseProperty, value);
}
public static bool GetBindThumbToMouse(UIElement element)
{
return (bool)element.GetValue(BindThumbToMouseProperty);
}
private static void OnBindThumbToMouseChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (e.NewValue.Equals(e.OldValue))
return;
var slider = d as Slider;
if (slider == null)
throw new ArgumentException(#"dependency object must be a slider", "d");
if ((bool) e.NewValue)
{
slider.MouseMove += SliderMouseMove;
}
else
{
slider.MouseMove -= SliderMouseMove;
}
}
static void SliderMouseMove(object sender, MouseEventArgs e)
{
var slider = (Slider) sender;
var position = e.GetPosition(slider);
// When the mouse moves, we update the slider's value so it's where the ticker is (we take into account margin's on the track)
slider.Value = (slider.Maximum - slider.Minimum)*Math.Max(position.X-5,0)/(slider.ActualWidth-10) + slider.Minimum;
}
}
And you can then use it in your XAML like so. So when you set this to true we hook up to the mouse move event on the slider and change its value so the thumb follows the mouse
<Slider SliderPosn:SliderHelperPackage.BindThumbToMouse="True" Margin="5" Height="25" VerticalAlignment="Top"/>
http://www.codeproject.com/KB/WPF/adornedcontrol.aspx
this reference should give you all necessary information to fix your issue
cheers!

Silverlight ScrollViewer Not Scrolling

enter code hereI have a ScrollViewer in Silverlight that is not scrolling vertically whenever I call the ScrollToVerticalOffset method from the code behind.
Basically, I have my View (UserControl) that contains the ScrollViewer. I invoke an action from my ViewModel that triggers an event in the View's code-behind that sets the VerticalOffset to a specific value.
First of all, I know this is very ugly. Ideally I wish that I could have an attachable property that I could bind to a property in my ViewModel, that, when set, would cause the VerticalOffset property (which I know is read-only) to be updated, and the ScrollViewer to scroll.
The ScrollViewer contains dynamic content. So, if the user is viewing content in the ScrollViewer, and scrolls half-way down, and then clicks on a button, new content is loaded into the ScrollViewer. The problem is that the ScrollViewer's vertical offset doesn't get reset, so the user has to scroll up to read the content. So, my solution was to be able to control the vertical offset from the ViewModel, and I have racked my brain and can't come up with a viable solution, so I am looking for someone to help, please.
By the way - I have included code from a class I put together for an attachable property. This property binds to a property in my ViewModel. When I set the property in the ViewModel, it correctly triggers the PropertyChanged callback method in this class, which then calls the ScrollToVerticalOffset method for the ScrollViewer, but the ScrollViewer still doesn't scroll.
public class ScrollViewerHelper
{
public static readonly DependencyProperty BindableOffsetProperty =
DependencyProperty.RegisterAttached("BindableOffset", typeof(double), typeof(ScrollViewerHelper),
new PropertyMetadata(OnBindableOffsetChanged));
public static double GetBindableOffset(DependencyObject d)
{
return (double)d.GetValue(BindableOffsetProperty);
}
public static void SetBindableOffset(DependencyObject d, double value)
{
d.SetValue(BindableOffsetProperty, value);
}
private static void OnBindableOffsetChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
ScrollViewer scrollViewer = d as ScrollViewer;
if (scrollViewer != null)
{
scrollViewer.ScrollToVerticalOffset((double)e.NewValue);
}
}
}
This approach is a little bit funky in my opinion, as I think of both a ScrollViewer and a VerticalScrollOffset as "View" entities that should have very little (or nothing) to do with a ViewModel. It seems like this might be forcing MVVM a little too much, and creating a lot of extra work in creating an attached dependency property and basically trying to keep a bound Offset ViewModel property in sync with the readonly VerticalScrollOffset of the ScrollViewer.
I am not exactly sure of what you are trying to achieve, but it sounds like you are trying to scroll to a specified offset when some dynamic element is added to the underlying panel of your ScrollViewer. Personally, I would just want to handle this behavior with a little bit of code in my View and forget about tying it to the ViewModel.
One really nice way to do this type of thing in Silverlight 3 is with Blend behaviors. You write a little bit of behavior code in C# and then can attach it declaratively to an element in XAML. This keeps it reusable and out of your code-behind. Your project will have to reference the System.Windows.Interactivity DLL which is part of the Blend SKD.
Here's a simple example of a simple Blend behavior you could add to a ScrollViewer which scrolls to a specified offset whenever the size of the underlying content of the ScrollViewer changes:
public class ScrollToOffsetBehavior : Behavior<ScrollViewer>
{
private FrameworkElement contentElement = null;
public static readonly DependencyProperty OffsetProperty = DependencyProperty.Register(
"Offset",
typeof(double),
typeof(ScrollToOffsetBehavior),
new PropertyMetadata(0.0));
public double Offset
{
get { return (double)GetValue(OffsetProperty); }
set { SetValue(OffsetProperty, value); }
}
protected override void OnAttached()
{
base.OnAttached();
if (this.AssociatedObject != null)
{
this.AssociatedObject.Loaded += new RoutedEventHandler(AssociatedObject_Loaded);
}
}
protected override void OnDetaching()
{
base.OnDetaching();
if (this.contentElement != null)
{
this.contentElement.SizeChanged -= contentElement_SizeChanged;
}
if (this.AssociatedObject != null)
{
this.AssociatedObject.Loaded -= AssociatedObject_Loaded;
}
}
void AssociatedObject_Loaded(object sender, RoutedEventArgs e)
{
this.contentElement = this.AssociatedObject.Content as FrameworkElement;
if (this.contentElement != null)
{
this.contentElement.SizeChanged += new SizeChangedEventHandler(contentElement_SizeChanged);
}
}
void contentElement_SizeChanged(object sender, SizeChangedEventArgs e)
{
this.AssociatedObject.ScrollToVerticalOffset(this.Offset);
}
}
You could then apply this behavior to the ScrollViewer in XAML (and specify an offset of 0 to scroll back to the top):
<ScrollViewer>
<i:Interaction.Behaviors>
<local:ScrollToOffsetBehavior Offset="0"/>
</i:Interaction.Behaviors>
...Scroll Viewer Content...
</ScrollViewer>
This would be assuming that you always want to scroll to the offset whenever the content size changes. This may not be exactly what you are looking for, but it is one example of how something like this can be done in the view using a behavior.

Is it possible to implement smooth scroll in a WPF listview?

Is it possible to implement smooth scroll in a WPF listview like how it works in Firefox?
When the Firefox browser contained all listview items and you hold down the middle mouse button (but not release), and drag it, it should smoothly scroll the listview items. When you release it should stop.
It looks like this is not possible in winforms, but I am wondering if it is available in WPF?
You can achieve smooth scrolling but you lose item virtualisation, so basically you should use this technique only if you have few elements in the list:
Info here: Smooth scrolling on listbox
Have you tried setting:
ScrollViewer.CanContentScroll="False"
on the list box?
This way the scrolling is handled by the panel rather than the listBox... You lose virtualisation if you do that though so it could be slower if you have a lot of content.
It is indeed possible to do what you're asking, though it will require a fair amount of custom code.
Normally in WPF a ScrollViewer uses what is known as Logical Scrolling, which means it's going to scroll item by item instead of by an offset amount. The other answers cover some of the ways you can change the Logical Scrolling behavior into that of Physical Scrolling. The other way is to make use of the ScrollToVertialOffset and ScrollToHorizontalOffset methods exposed by both ScrollViwer and IScrollInfo.
To implement the larger part, the scrolling when the mouse wheel is pressed, we will need to make use of the MouseDown and MouseMove events.
<ListView x:Name="uiListView"
Mouse.MouseDown="OnListViewMouseDown"
Mouse.MouseMove="OnListViewMouseMove"
ScrollViewer.CanContentScroll="False">
....
</ListView>
In the MouseDown, we are going to record the current mouse position, which we will use as a relative point to determine which direction we scroll in. In the mouse move, we are going to get the ScrollViwer component of the ListView and then Scroll it accordingly.
private Point myMousePlacementPoint;
private void OnListViewMouseDown(object sender, MouseButtonEventArgs e)
{
if (e.MiddleButton == MouseButtonState.Pressed)
{
myMousePlacementPoint = this.PointToScreen(Mouse.GetPosition(this));
}
}
private void OnListViewMouseMove(object sender, MouseEventArgs e)
{
ScrollViewer scrollViewer = ScrollHelper.GetScrollViewer(uiListView) as ScrollViewer;
if (e.MiddleButton == MouseButtonState.Pressed)
{
var currentPoint = this.PointToScreen(Mouse.GetPosition(this));
if (currentPoint.Y < myMousePlacementPoint.Y)
{
scrollViewer.ScrollToVerticalOffset(scrollViewer.VerticalOffset - 3);
}
else if (currentPoint.Y > myMousePlacementPoint.Y)
{
scrollViewer.ScrollToVerticalOffset(scrollViewer.VerticalOffset + 3);
}
if (currentPoint.X < myMousePlacementPoint.X)
{
scrollViewer.ScrollToHorizontalOffset(scrollViewer.HorizontalOffset - 3);
}
else if (currentPoint.X > myMousePlacementPoint.X)
{
scrollViewer.ScrollToHorizontalOffset(scrollViewer.HorizontalOffset + 3);
}
}
}
public static DependencyObject GetScrollViewer(DependencyObject o)
{
// Return the DependencyObject if it is a ScrollViewer
if (o is ScrollViewer)
{ return o; }
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(o); i++)
{
var child = VisualTreeHelper.GetChild(o, i);
var result = GetScrollViewer(child);
if (result == null)
{
continue;
}
else
{
return result;
}
}
return null;
}
There's some areas it's lacking as it's just a proof of concept but it should definitely get you started in the right direction. To have it constantly scroll once the mouse is moved away from the initial MouseDown point, the scrolling logic could go into a DispatcherTimer or something similar.
Try setting the ScrollViewer.CanContentScroll attached property to false on the ListView. But like Pop Catalin said, you lose item virtualization, meaning all the items in the list get loaded and populated at once, not when a set of items are needed to be displayed - so if the list is huge, it could cause some memory and performance issues.
try setting the listview's height as auto and wrapping it in a scroll viewer.
<ScrollViewer IsTabStop="True" VerticalScrollBarVisibility="Auto">
<ListView></ListView>
</ScrollViewer>
Don't forget to mention the height of ScrollViewer
Hope this helps....
I know this post is 13 years old, but this is still something people want to do.
in newer versions of .Net you can set VirtualizingPanel.ScrollUnit="Pixel"
this way you won't lose virtualization and you get scroll per pixel instead of per item.

Categories