I do have a listbox which must have CanContentScroll==false because i need to be able to scroll it smoothly. This enables physical scrolling.
I also want to scroll the listbox by page, but if i call the PageDown method on the listbox internal ScrollViewer the first row gets cutted because the listbox height is not a multiple of the row height.
I want the first row to be always completely visible, like when using logical scrolling.
Can someone give me a hint on how to do that?
You'll get the same effect for a non virtualizing ItemsControl
(ScrollViewer.CanContentScroll="False"
) as for a virtualizing one if you scroll down and then select the upper visible container with the mouse. This can also be done in code.
When CanContentScroll
is set to false, virtualizing is turned off so all containers will be generated at all times. To get the top visible container we can iterate the containers from the top until we reach the VerticalOffset
of the ScrollViewer
. Once we got it we can simply call BringIntoView
on it and it will align nicely at the top just like it would if virtualization was being used.
Example
<ListBox ItemsSource="{Binding MyCollection}"
ScrollViewer.CanContentScroll="False"
ScrollViewer.ScrollChanged="listBox_ScrollChanged" >
Call BringIntoView
on the top visible container in the event handler
private void listBox_ScrollChanged(object sender, ScrollChangedEventArgs e)
{
ItemsControl itemsControl = sender as ItemsControl;
ScrollViewer scrollViewer = e.OriginalSource as ScrollViewer;
FrameworkElement lastElement = null;
foreach (object obj in itemsControl.Items)
{
FrameworkElement element = itemsControl.ItemContainerGenerator.ContainerFromItem(obj) as FrameworkElement;
double offset = element.TransformToAncestor(scrollViewer).Transform(new Point(0, 0)).Y + scrollViewer.VerticalOffset;
if (offset > e.VerticalOffset)
{
if (lastElement != null)
lastElement.BringIntoView();
break;
}
lastElement = element;
}
}
To only achieve this effect when you want to call PageDown
, in a Button click for example, you can create an extension method for ListBox
called LogicalPageDown
.
listBox.LogicalPageDown();
ListBoxExtensions
public static class ListBoxExtensions
{
public static void LogicalPageDown(this ListBox listBox)
{
ScrollViewer scrollViewer = VisualTreeHelpers.GetVisualChild<ScrollViewer>(listBox);
ScrollChangedEventHandler scrollChangedHandler = null;
scrollChangedHandler = (object sender2, ScrollChangedEventArgs e2) =>
{
scrollViewer.ScrollChanged -= scrollChangedHandler;
FrameworkElement lastElement = null;
foreach (object obj in listBox.Items)
{
FrameworkElement element = listBox.ItemContainerGenerator.ContainerFromItem(obj) as FrameworkElement;
double offset = element.TransformToAncestor(scrollViewer).Transform(new Point(0, 0)).Y + scrollViewer.VerticalOffset;
if (offset > scrollViewer.VerticalOffset)
{
if (lastElement != null)
lastElement.BringIntoView();
break;
}
lastElement = element;
}
};
scrollViewer.ScrollChanged += scrollChangedHandler;
scrollViewer.PageDown();
}
}
I noticed in your question that you already got the ScrollViewer
but I'm adding an implementation to GetVisualChild
if anyone else comes across this question
public static T GetVisualChild<T>(DependencyObject parent) where T : Visual
{
T child = default(T);
int numVisuals = VisualTreeHelper.GetChildrenCount(parent);
for (int i = 0; i < numVisuals; i++)
{
Visual v = (Visual)VisualTreeHelper.GetChild(parent, i);
child = v as T;
if (child == null)
{
child = GetVisualChild<T>(v);
}
if (child != null)
{
break;
}
}
return child;
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With