Detect when WPF listview scrollbar is at the bottom? - c#

Is there a way to detect if the scrollbar from the ScrollViewer in a ListView has reached the bottom of the virtual scroll space? I would like to detect this to fetch more items from the server to put into the bound ObservableCollection on the ListView.
Right now I'm doing this:
private void currentTagNotContactsList_scrollChanged(object sender, ScrollChangedEventArgs e) {
ListView v = (ListView)sender;
if (e.VerticalOffset + e.ViewportHeight == e.ExtentHeight) {
Debug.Print("At the bottom of the list!");
}
}
Is this even correct? I also need to differentiate between the vertical scrollbar causing the event and the horizontal scrollbar causing it (i.e. I don't want to keep generating calls to the server if you scroll horizontally at the bottom of the box).
Thanks.

//A small change in the "Max's" answer to stop the repeatedly call.
//this line to stop the repeatedly call
ScrollViewer.CanContentScroll="False"
private void dtGrid_ScrollChanged(object sender, ScrollChangedEventArgs e)
{
//this is for vertical check & will avoid the call at the load time (first time)
if (e.VerticalChange > 0)
{
if (e.VerticalOffset + e.ViewportHeight == e.ExtentHeight)
{
// Do your Stuff
}
}
}

I figured it out. It seems I should have been getting events from the ScrollBar (<ListView ScrollBar.Scroll="currentTagNotContactsList_Scroll" in XAML) itself, rather than the viewer. This works, but I just have to figure a way to avoid the event handler being called repeatedly once the scrollbar is down. Maybe a timer would be good:
private void currentTagNotContactsList_Scroll(object sender, ScrollEventArgs e) {
ScrollBar sb = e.OriginalSource as ScrollBar;
if (sb.Orientation == Orientation.Horizontal)
return;
if (sb.Value == sb.Maximum) {
Debug.Print("At the bottom of the list!");
}
}

For UWP I got it like this
<ScrollViewer Name="scroll" ViewChanged="scroll_ViewChanged">
<ListView />
</ScrollViewer>
private void scroll_ViewChanged(object sender, ScrollViewerViewChangedEventArgs e)
{
var scrollViewer = (ScrollViewer)sender;
if (scrollViewer.VerticalOffset == scrollViewer.ScrollableHeight)
btnNewUpdates.Visibility = Visibility.Visible;
}

you can try this way:
<ListView ScrollViewer.ScrollChanged="Scroll_ScrollChanged">
and in Back:
private void Scroll_ScrollChanged(object sender, ScrollChangedEventArgs e)
{
// Get the border of the listview (first child of a listview)
Decorator border = VisualTreeHelper.GetChild(sender as ListView, 0) as Decorator;
// Get scrollviewer
ScrollViewer scrollViewer = border.Child as ScrollViewer;
if (scrollViewer.VerticalOffset == scrollViewer.ScrollableHeight)
Debug.Print("At the bottom of the list!");
}

Not recommand to use ScrollBar.Scroll , beacause if you scroll the middle wheel of the mouse, it won't work.
ScrollBar.Scroll="currentTagNotContactsList_Scroll"
The following support both right side scroll bar and mouse's wheel scroll.
in listbox's xmal:
ScrollViewer.ScrollChanged="ScrollViewer_ScrollChanged"
in c#:
private void ScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e)
{
var listBox = (ListBox)sender;
var scrollViewer = (ScrollViewer)VisualTreeHelper.GetChild(listBox, 0);
if (scrollViewer.VerticalOffset == scrollViewer.ScrollableHeight)
{
Console.WriteLine("____At the bottom of the list!");
}
}

Related

Capturing and setting DataGrid scroll position when toggling between TabControl TabItems

I have TabControl that has a DataGrid inside each TabItem. It's all populated via binding. I use the row details expansion functionality, so have set the VirtualizingPanel ScrollUnit to Pixel, so scrolling is a bit more natural.
When changing between TabItems I have row selection behaving correctly. However, setting the vertical offset on the DataGrid's ScrollViewer so it is in the exact same position as it was when you left the TabItem is not working correctly.
The way it works at the moment is, I have a behavior class on the DataGrid. On a Scrollviewer ScrollChangedEvent it saves the VerticalOffset. Upon changing to a new TabItem and changing back to the original TabItem, in the DataGrid's DataContextChanged event I set the ScrollViewer's VerticalOffset to the saved VerticalOffset.
public class DataGridBehaviors : Behavior<DataGrid>
{
protected override void OnAttached()
{
base.OnAttached();
this.AssociatedObject.DataContextChanged += DataGrid_DataContextChanged;
this.AssociatedObject.AddHandler(ScrollViewer.ScrollChangedEvent, new ScrollChangedEventHandler(DataGridScrollViewer_ScrollChanged));
}
protected override void OnDetaching()
{
Console.WriteLine("OnDetaching");
this.AssociatedObject.RemoveHandler(ScrollViewer.ScrollChangedEvent, new ScrollChangedEventHandler(DataGridScrollViewer_ScrollChanged));
this.AssociatedObject.DataContextChanged -= DataGrid_DataContextChanged;
base.OnDetaching();
}
private void DataGrid_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
ModuleGeometry oldModuleGeometry = (ModuleGeometry)e.OldValue;
ModuleGeometry newModuleGeometry = (ModuleGeometry)e.NewValue;
ScrollViewer scrollViewer = GetVisualChild<ScrollViewer>(this.AssociatedObject);
if (scrollViewer != null)
{
scrollViewer.ScrollToVerticalOffset(newModuleGeometry.VerticalScrollPosition);
}
}
private void DataGridScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e)
{
ModuleGeometry modGeom = (ModuleGeometry)this.AssociatedObject.DataContext;
modGeom.VerticalScrollPosition = e.VerticalOffset;
}
private 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;
}
}
What is happening is you scroll down and the top DataGridRow is partially displayed (You only see half of it). Then when you toggle between two TabItems, the program sets the VerticalOffset correctly, but then it resets again automatically to the top of the partially displayed row (showing it fully).
Before toggling. Saves VertcialOffset to 4327.2
After toggling back to the original TabItem Sets VerticalOffset to 4327.2, then for some reason and somehow automatically resets VerticalOffset to 4321.5, which is the top of the 'previously' partially visible row
It gets even weirder when you have an expanded row loaded in the VirtualizingPanel, the jump is more dramatic.
Before toggling
After toggling back to the original TabItem
I would like to see the scroll position in exactly the same spot as when I left it, how can I accomplish this?
The easiest approach would probably be to stop the TabControl from unloading the visual tree when you switch tabs. Then the content of each tab should be preserved.
Please refer to the following links for more information about this.
How to stop Wpf Tabcontrol to unload Visual tree on Tab change
WPF - Elements inside DataTemplate property issue when no binding?

How can I add an item to a Canvas after it has loaded?

Items are shown on the canvas with help of a DragEvent.
The items get loaded by the OnDrop event. But I need to load some items without dragging them on the canvas. Is there an Event Argument like that is called automatically when the canvas loads??
protected override void OnDrop(DragEventArgs e)
{
base.OnDrop(e);
DragObject dragObject = e.Data.GetData(typeof(DragObject)) as DragObject;
if ( dragObject != null && !String.IsNullOrEmpty(dragObject.Xaml) )
{
// elided
}
}
I need to show item when the canvas loads without user doing the drag drop.
I also need to show the item automatically when the canvas loads.
You can attach a handler to the FrameworkElement.Loaded Event:
<Canvas Loaded="YourEventHandler" ... />
...
public void YourEventHandler(object sender, RoutedEventArgs e)
{
Canvas canvas = (Canvas)sender;
// Add your item(s) here using canvas.Children.Add(someItem);
}

Popup with StaysOpen to false is not closing

I have a telerik grid view and when I right click the header, I'm showing a with a ListBox inside containing the list of columns.
The item template is redefined to show a check box so I can set the column visible or not.
I can also drag/drop columns to reorder them.
So here is how I create my Popup:
var view = new ColumnsOrderer.ColumnsOrderer
{
DataContext = new ColumnsOrderer.ViewModelColumnsOrderer(Columns)
};
var codePopup = new Popup
{
Child = view,
MaxHeight = 400,
StaysOpen = false,
Placement = PlacementMode.Mouse
};
codePopup.IsOpen = true;
Now everything seems to work correctly, but it's not.
If I set columns visible or hidden and then click outside the popup, it closes correctly.
Though if I drag an item to reorder it, the popup seems to lose focus and then it won't close if I click outside the popup. I have to click back in the list box inside the popup and then it closes by clicking outside.
Here is my drag/drop events:
public ColumnsOrderer()
{
InitializeComponent();
InitialiazeListBoxDragDrop();
}
private void InitialiazeListBoxDragDrop()
{
var itemContainerStyle = new Style(typeof(ListBoxItem));
itemContainerStyle.Setters.Add(new Setter(AllowDropProperty, true));
itemContainerStyle.Setters.Add(new EventSetter(PreviewMouseMoveEvent, new MouseEventHandler(OnMouseMove)));
itemContainerStyle.Setters.Add(new EventSetter(DropEvent, new DragEventHandler(OnDrop)));
listColumns.ItemContainerStyle = itemContainerStyle;
}
void OnMouseMove(object sender, MouseEventArgs e)
{
if (e.OriginalSource is CheckBox || e.LeftButton == MouseButtonState.Released)
return;
if (sender is ListBoxItem)
{
var draggedItem = sender as ListBoxItem;
draggedItem.IsSelected = true;
DragDrop.DoDragDrop(draggedItem, draggedItem.DataContext, DragDropEffects.Move);
}
}
void OnDrop(object sender, DragEventArgs e)
{
if (!(sender is ListBoxItem))
return;
}
An interesting thing is that if I remove the OnDrop handler, the problem is not there.
I tried many ways to set back the focus to the popup, but it's not working.
Could anyone help me on that?
How about trying to re-focus your Popup control after the drag and drop operation?
void OnDrop(object sender, DragEventArgs e)
{
if (!(sender is ListBoxItem))
return;
codePopup.Focus();
}

TextBox ScrollToEnd Glitches When Selecting if Text is Appeneded

I'm making a textbox to autoscroll to the end when text is added. However I wanted the option to not scroll the textbox when the mouse is over the textbox. I've done all that, but when the user selects text and the textbox receives an event to update the text, everything goes haywire.
Here's what I'm working with:
<TextBox Text="{Binding ConsoleContents, Mode=OneWay}" TextWrapping="Wrap"
IsReadOnly="True" ScrollViewer.VerticalScrollBarVisibility="Visible"
TextChanged="TextBox_TextChanged" MouseEnter="TextBox_MouseEnterLeave"
MouseLeave="TextBox_MouseEnterLeave" AllowDrop="False" Focusable="True"
IsUndoEnabled="False"></TextBox>
private void TextBox_TextChanged(object sender, TextChangedEventArgs e)
{
TextBox textBox = sender as TextBox;
if (textBox == null) return;
// ensure we can scroll
if (_canScroll)
{
textBox.Select(textBox.Text.Length, 0); //This was an attempt to fix the issue
textBox.ScrollToEnd();
}
}
private void TextBox_MouseEnterLeave(object sender, MouseEventArgs e)
{
TextBox textBox = sender as TextBox;
// Don't scroll if the mouse is in the box
if (e.RoutedEvent.Name == "MouseEnter")
{
_canScroll = false;
}
else if (e.RoutedEvent.Name == "MouseLeave")
{
_canScroll = true;
}
}
To further explain what haywire means, when the textbox receives and propertychanged event it sets the text and scrolls down to the end if the mouse is not hovering over it. If the mouse is hovering over it does not scroll. But if I select text and the textbox recives the propertychanged event the content gets updated, but the box does not scroll down. This is expected. The problem is that my selection then goes from where the cursor currently is to the top of the text. If I remove the cursor from the box, it continues fine but once the cursor returns, the box gets stuck at the top and cannot scroll down. I thought it might have been the cursor so I try to move it to the end but that solves nothing.
Any ideas?!
I've been ripping my hair out! Thanks!
You will need to handle the extra case of PropertyChanged event. Because the text is changed the control is updated. Because it updates it resets certain values, like cursor location and selected text and such.
You can try to temporarily save these settings like CaretIndex, SelectionStart and SelectionLength. Although I have no experience with this, so you will have to find out for yourself what values you want to keep.
You could also apply the same _canScroll check when the PropertyChanged event is triggered for the TextBox. This allows you to work with the text. But if the mouse leaves the Textbox you must wait for a new event before the latest text is shown.
You might also want to look into using IsFocused property. In my opinion it gives a nicer solution than the MouseEnter and MouseLeave event. But that's up to you of course.
Well it took all morning but here's how to do it:
<TextBox Text="{Binding ConsoleContents, Mode=OneWay}"
TextWrapping="Wrap" IsReadOnly="True" ScrollViewer.VerticalScrollBarVisibility="Visible" TextChanged="TextBox_TextChanged"
MouseEnter="TextBox_MouseEnterLeave" MouseLeave="TextBox_MouseEnterLeave" SelectionChanged="TextBox_SelectionChanged" AllowDrop="False" Focusable="True"
IsUndoEnabled="False"></TextBox>
public partial class ConsoleView : UserControl
{
private bool _canScroll;
// saves
private int _selectionStart;
private int _selectionLength;
private string _selectedText;
public ConsoleView(ConsoleViewModel vm)
{
InitializeComponent();
this.DataContext = vm;
_canScroll = true;
}
private void TextBox_TextChanged(object sender, TextChangedEventArgs e)
{
TextBox textBox = sender as TextBox;
if (textBox == null) return;
// ensure we can scroll
if (_canScroll)
{
// set the cursor to the end and scroll down
textBox.Select(textBox.Text.Length, 0);
textBox.ScrollToEnd();
// save these so the box doesn't jump around if the user goes back in
_selectionLength = textBox.SelectionLength;
_selectionStart = textBox.SelectionStart;
}
else if (!_canScroll)
{
// move the cursor to where the mouse is if we're not selecting anything (for if we are selecting something the cursor has already moved to where it needs to be)
if (string.IsNullOrEmpty(_selectedText))
//if (textBox.SelectionLength > 0)
{
textBox.CaretIndex = textBox.GetCharacterIndexFromPoint(Mouse.GetPosition(textBox), true);
}
else
{
textBox.Select(_selectionStart, _selectionLength); // restore what was saved
}
}
}
private void TextBox_MouseEnterLeave(object sender, MouseEventArgs e)
{
TextBox textBox = sender as TextBox;
if (textBox == null) return;
// Don't scroll if the mouse is in the box
if (e.RoutedEvent.Name == "MouseEnter")
{
_canScroll = false;
}
else if (e.RoutedEvent.Name == "MouseLeave")
{
_canScroll = true;
}
}
private void TextBox_SelectionChanged(object sender, RoutedEventArgs e)
{
TextBox textBox = sender as TextBox;
if (textBox == null) return;
// save all of the things
_selectionLength = textBox.SelectionLength;
_selectionStart = textBox.SelectionStart;
_selectedText = textBox.SelectedText; // save the selected text because it gets destroyed on text update before the TexChanged event.
}
}

Add Items to ListBox when scroll reaches the end in Windows phone?

I need the requirement that..
Initially i have set of datas that are bound to ListBox... If we scroll to the end i will add some more datas to the collection and will update the ListBox... Is there any way to achieve this in Windows phone ?
I suppose that by "achieve this" you mean the possibility to detect if the ListBox is at the end. In that case, this should help.
You'll first need to gain access to the ScrollViewer control in order to see if a user scrolled, and what the current position is. If my page is called ListContent, then this code should give you a good start:
public partial class ListContent
{
private ScrollViewer scrollViewer;
public ListContent()
{
InitializeComponent();
Loaded += OnLoaded();
}
protected virtual void OnLoaded(object sender, RoutedEventArgs routedEventArgs)
{
scrollViewer = ControlHelper.List<ScrollViewer>(lbItems).FirstOrDefault();
if (scrollViewer == null) return;
FrameworkElement framework = VisualTreeHelper.GetChild(viewer, 0) as FrameworkElement;
if (framework == null) return;
VisualStateGroup group = FindVisualState(framework, "ScrollStates");
if (group == null) return;
group.CurrentStateChanged += OnListBoxStateChanged;
}
private VisualStateGroup FindVisualState(FrameworkElement element, string name)
{
if (element == null)
return null;
IList groups = VisualStateManager.GetVisualStateGroups(element);
return groups.Cast<VisualStateGroup>().FirstOrDefault(#group => #group.Name == name);
}
private void OnListBoxStateChanged(object sender, VisualStateChangedEventArgs e)
{
if (e.NewState.Name == ScrollState.NotScrolling.ToString())
{
// Check the ScrollableHeight and VerticalOffset here to determine
// the position of the ListBox.
// Add items, if the ListBox is at the end.
// This event will fire when the listbox complete stopped it's
// scrolling animation
}
}
}
If you're talking about adding the data dynamically, make sure you are using an ObservableCollection for your data. Added items will automatically show up in your ListBox.

Categories