40

I think I am stupid. I searched now for 15 minutes, and found several different solutions for scrolling on datagrids, but none seems to work for me.

I am using WPF with .NET 3.5 and the WPF Toolkit DataGrid. My grid gets updated when my observable collection changes, works perfectly. Now, my DataGrid is located inside a normal Grid and scrollbars appear if the DataGrid gets too big. Also fine...

And now comes the 1.000.000 $ question:

How do I get the datagrid to scroll to the last row? There is:

  • no AutoScroll Property
  • no CurrentRowSelected Index
  • a CurrentCell, but no Collection I could use for CurrentCell = AllCells.Last

Any ideas? I feel really stupid, and it seems strange that this question is so hard. What am I missing?

Teun D
  • 5,045
  • 1
  • 34
  • 44
Christian Ruppert
  • 3,749
  • 5
  • 47
  • 72

17 Answers17

56

You should use the datagrid method

datagrid.ScrollIntoView(itemInRow);

or

datagrid.ScrollIntoView(itemInRow, column);

this way provides no messing around finding the scroll viewer etc.

Aran Mulholland
  • 23,555
  • 29
  • 141
  • 228
  • 3
    @user1034912 If you have a problem with this answer you should provide details as to why it doesn't work or maybe your circumstances are different and you need to ask another question. Maybe something has changed about the underlying framework since this question was answered 6 years ago. Sometimes I had problems with scroll into view as the grid was virtualised. From the number of upvotes it's obvious this worked for quite a few people so maybe the problem is with your code not this answer. – Aran Mulholland Feb 09 '17 at 11:18
52

;)

if (mainDataGrid.Items.Count > 0)
{
    var border = VisualTreeHelper.GetChild(mainDataGrid, 0) as Decorator;
    if (border != null)
    {
        var scroll = border.Child as ScrollViewer;
        if (scroll != null) scroll.ScrollToEnd();
    }
}
Ankush Madankar
  • 3,689
  • 4
  • 40
  • 74
Joseph jun. Melettukunnel
  • 6,267
  • 20
  • 69
  • 90
  • 1
    Thanks a lot, if only life would always be that easy :-) – Christian Ruppert Sep 17 '09 at 23:30
  • 1
    Brilliant piece of code, wrap it in an ArgumentOutOfRange exception and it would be perfect, for when the listbox might be empty. – wonea Jul 12 '10 at 16:01
  • I also want to know where this code goes (what event is being hooked)? I have tried SizeChanged and LayoutUpdated among others. The best I can get is that my DataGrid scrolls down halfway. I have tried this decorator/ScrollToEnd version and the ScrollIntoView version. – e-holder May 04 '12 at 15:57
  • thanx this worked for me...from ur answers it seems u r master of all technologies :) gr8 – C Sharper Dec 25 '13 at 06:58
  • 4
    Hook it up with `((INotifyCollectionChanged)MyDataGrid.Items).CollectionChanged += Your_Event_Handler;` – maxp Aug 21 '15 at 08:56
  • Quick note for C#7 users, you can use pattern matching for this, e.g. instead of the border declaration and check you can condense the whole statement to: `if (VisualTreeHelper.GetChild(mainDataGrid, 0) is Decorator border)` – Lauraducky Feb 21 '18 at 23:32
23

I've written an attached property for grid autoscroll:

using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Windows;
using System.Windows.Controls;

public static class DataGridBehavior
{
    public static readonly DependencyProperty AutoscrollProperty = DependencyProperty.RegisterAttached(
        "Autoscroll", typeof(bool), typeof(DataGridBehavior), new PropertyMetadata(default(bool), AutoscrollChangedCallback));

    private static readonly Dictionary<DataGrid, NotifyCollectionChangedEventHandler> handlersDict = new Dictionary<DataGrid, NotifyCollectionChangedEventHandler>();

    private static void AutoscrollChangedCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs args)
    {
        var dataGrid = dependencyObject as DataGrid;
        if (dataGrid == null)
        {
            throw new InvalidOperationException("Dependency object is not DataGrid.");
        }

        if ((bool)args.NewValue)
        {
            Subscribe(dataGrid);
            dataGrid.Unloaded += DataGridOnUnloaded;
            dataGrid.Loaded += DataGridOnLoaded;
        }
        else
        {
            Unsubscribe(dataGrid);
            dataGrid.Unloaded -= DataGridOnUnloaded;
            dataGrid.Loaded -= DataGridOnLoaded;
        }
    }

    private static void Subscribe(DataGrid dataGrid)
    {
        var handler = new NotifyCollectionChangedEventHandler((sender, eventArgs) => ScrollToEnd(dataGrid));
        handlersDict.Add(dataGrid, handler);
        ((INotifyCollectionChanged)dataGrid.Items).CollectionChanged += handler;
        ScrollToEnd(dataGrid);
    }

    private static void Unsubscribe(DataGrid dataGrid)
    {
        NotifyCollectionChangedEventHandler handler;
        handlersDict.TryGetValue(dataGrid, out handler);
        if (handler == null)
        {
            return;
        }
        ((INotifyCollectionChanged)dataGrid.Items).CollectionChanged -= handler;
        handlersDict.Remove(dataGrid);
    }

    private static void DataGridOnLoaded(object sender, RoutedEventArgs routedEventArgs)
    {
        var dataGrid = (DataGrid)sender;
        if (GetAutoscroll(dataGrid))
        {
            Subscribe(dataGrid);
        }
    }

    private static void DataGridOnUnloaded(object sender, RoutedEventArgs routedEventArgs)
    {
        var dataGrid = (DataGrid)sender;
        if (GetAutoscroll(dataGrid))
        {
            Unsubscribe(dataGrid);
        }
    }

    private static void ScrollToEnd(DataGrid datagrid)
    {
        if (datagrid.Items.Count == 0)
        {
            return;
        }
        datagrid.ScrollIntoView(datagrid.Items[datagrid.Items.Count - 1]);
    }

    public static void SetAutoscroll(DependencyObject element, bool value)
    {
        element.SetValue(AutoscrollProperty, value);
    }

    public static bool GetAutoscroll(DependencyObject element)
    {
        return (bool)element.GetValue(AutoscrollProperty);
    }
}

Usage:

    <DataGrid c:DataGridBehavior.Autoscroll="{Binding AutoScroll}"/>
  • 3
    @user161953 I think visitor's need more explanation for this code..! – kamesh Jul 09 '14 at 04:38
  • Very, very nice. Thanks for that. – Chris Mantle Feb 06 '16 at 01:15
  • 1
    I had to make a couple of slight modifications to this such that it is bound to monitor the changes to an integer property instead of a bool. I'm happy to report it works even after modifying it. – Krondorian Nov 16 '16 at 23:55
  • Doesn't work: `The attachable property 'Autoscroll' was not found in type 'DataGridBehavior'.` – Matt Apr 13 '18 at 21:58
  • Been trying to get DataGrid to autoscroll while adhering to MVVM. This worked a treat creating AutoScroll property in my view model then binding a checkbox IsChecked to the AutoScroll property. – Moon Waxing Aug 15 '21 at 13:04
  • I had to modify the Subscribe() method to ensure there are no duplicate keys in the dictionary. Other than that, works well. – SnowGroomer Apr 23 '22 at 18:08
  • Autoscroll cannot be changed dynamically. – CodingNinja Jul 30 '22 at 09:34
  • @Matt, you need to add the xml namespace where the behavior lives to your XAML view. For example: xmlns:c="clr-namespace:Monitor.Behaviors" – Los2000 Mar 26 '23 at 19:05
8

For having an AutoScroll To the Last element added :

YourDataGrid.ScrollIntoView(YourDataGrid.Items.GetItemAt(YourDataGrid.Items.Count-1));

May This Help :)

Mosh Feu
  • 28,354
  • 16
  • 88
  • 135
Anas
  • 917
  • 7
  • 6
6
listbox.Add(foo);
listbox.SelectedIndex = count - 1;
listbox.ScrollIntoView(listbox.SelectedItem);
listbox.SelectedIndex = -1;
Conrad
  • 2,197
  • 28
  • 53
azze
  • 61
  • 1
  • 3
  • This is great for scrolling the selection to the 'middle' of the screen (with a few logical checks and changes). – DJ van Wyk Jul 22 '15 at 07:49
6

I know this is a late answer, but just for the people that are searching around, I found THE EASYEST way to scroll to the bottom of a DataGrid. in the DataContextChanged event put this in:

myDataGrid.ScrollIntoView(CollectionView.NewItemPlaceholder);

Easy huh?

This is why it works: On every data grid there is a place at the bottom of the DataGrid where you can add a new item to your list that it's bound to. That is a CollectionView.NewItemPlaceholder, and there will only be one of those in your DataGrid. So you can just scroll to that.

James Esh
  • 2,219
  • 1
  • 24
  • 41
4

I've found that the easiest way to do this is to call the ScrollIntoView method from the ScrollViewer.ScrollChanged attached event. This can be set in XAML as follows:

<DataGrid
...
ScrollViewer.ScrollChanged="control_ScrollChanged">

The ScrollChangedEventArgs object has various properties that can be helpful for computing layout and scroll position (Extent, Offset, Viewport). Note that these are typically measured in numbers of rows/columns when using the default DataGrid virtualization settings.

Here's an example implementation that keeps the bottom item in view as new items are added to the DataGrid, unless the user moves the scrollbar to view items higher up in the grid.

    private void control_ScrollChanged(object sender, ScrollChangedEventArgs e)
    {
        // If the entire contents fit on the screen, ignore this event
        if (e.ExtentHeight < e.ViewportHeight)
            return;

        // If no items are available to display, ignore this event
        if (this.Items.Count <= 0)
            return;

        // If the ExtentHeight and ViewportHeight haven't changed, ignore this event
        if (e.ExtentHeightChange == 0.0 && e.ViewportHeightChange == 0.0)
            return;

        // If we were close to the bottom when a new item appeared,
        // scroll the new item into view.  We pick a threshold of 5
        // items since issues were seen when resizing the window with
        // smaller threshold values.
        var oldExtentHeight = e.ExtentHeight - e.ExtentHeightChange;
        var oldVerticalOffset = e.VerticalOffset - e.VerticalChange;
        var oldViewportHeight = e.ViewportHeight - e.ViewportHeightChange;
        if (oldVerticalOffset + oldViewportHeight + 5 >= oldExtentHeight)
            this.ScrollIntoView(this.Items[this.Items.Count - 1]);
    }
Matt
  • 659
  • 6
  • 11
3

if large data datagrid.ScrollIntoView(itemInRow, column); not works fine then we need to use below one only:

if (mainDataGrid.Items.Count > 0) 
        { 
            var border = VisualTreeHelper.GetChild(mainDataGrid, 0) as Decorator; 
            if (border != null) 
            { 
                var scroll = border.Child as ScrollViewer; 
                if (scroll != null) scroll.ScrollToEnd(); 
            } 
        } 
Aran Mulholland
  • 23,555
  • 29
  • 141
  • 228
TRS Rao
  • 31
  • 2
1

Actually...

I had the same problem as well when I was learning about Collection Views about doing DataContext in WPF.

I too was faced with a task of slapping together a WPF program that I need to programmically to move up and down on the DataGrid using buttons since I needed to put it on a resistive touchscreen ONLY for the production builders \t my company, and there's no mouse or keyboard for them to use.

But this example worked for me using the ScrollIntoView method as previously mentioned in this post:

    private void OnMoveUp(object sender, RoutedEventArgs e)
    {
        ICollectionView myCollectView = CollectionViewSource.GetDefaultView(Orders);
        if (myCollectView.CurrentPosition > 0)
            myCollectView.MoveCurrentToPrevious();

        if (myCollectView.CurrentItem != null)
            theDataGrid.ScrollIntoView(myCollectView.CurrentItem);
    }

    private void OnMoveDown(object sender, RoutedEventArgs e)
    {
        ICollectionView  myCollectView = CollectionViewSource.GetDefaultView(Orders);
        if (myCollectView.CurrentPosition < Orders.Count)
            myCollectView.MoveCurrentToNext();

        if (myCollectView.CurrentItem !=null)
            theDataGrid.ScrollIntoView(myCollectView.CurrentItem);
    }

Where Orders is a List<T> collection

in XAML:

    <StackPanel Grid.Row="1"
        Orientation="Horizontal">
            <Button Click="OnMoveUp">
                <Image Source="Up.jpg" />
            </Button>
            <Button Click="OnMoveDown">
                <Image Source="Down.jpg" />
              </Button>
    </StackPanel>

    <DataGrid Grid.Row="2"
              x:Name="theDataGrid"
              ItemSource="{Binding Orders}"
              ScrollViewer.CanContentScroll="True"
              ScrollViewer.VerticalScrollBarVisibility="Auto" Margin="0,0,0,5">

    << code >>


    </DataGrid>

Do follow the previous advice and keep the DataGrid by itself and not in a stack panel. For the Row Definition for the DataGrid (the third row in this case), I set the Height at 150, and the scrollbar works.

Steve Brother
  • 838
  • 2
  • 11
  • 21
1

Here's another excellent solution.

public sealed class CustomDataGrid : DataGrid
{
    protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
    {
        base.OnItemsSourceChanged(oldValue, newValue);
    }
    protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e)
    {
        base.OnItemsChanged(e);
        if (this.Items.Count > 0) this.ScrollIntoView(this.Items[this.Items.Count - 1]);
    }
}
0

What you need is to get the reference to the ScrollViewer object for your DataGrid. You can then manipulate the VerticalOffset property to scroll to the bottom.

To add even more flare to your app...you could add a Spline animation to the scroll so everything looks up to par with the rest of the application.

Justin Niessner
  • 242,243
  • 40
  • 408
  • 536
  • Mm, ok, if found a few attached properties, like ScrollViewer.CanContentScroll = "True", but without any effect. I know, RTFM, but hey, I still got hope for a line of code here. I know, spoiled brat :-) – Christian Ruppert Jun 22 '09 at 13:40
0

WPF DataGrid Auto Scrolling

Auto Scrolling for as long as the the mouse button is down on a button control.

The XAML

<Button x:Name="XBTNPageDown" Height="50" MouseLeftButtonDown="XBTNPageDown_MouseLeftButtonDown"  MouseUp="XBTNPageDown_MouseUp">Page Down</Button>

The Code

    private bool pagedown = false;
    private DispatcherTimer pageDownTimer = new DispatcherTimer();

    private void XBTNPageDown_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
        pagedown = true;
        pageDownTimer.Interval = new TimeSpan(0, 0, 0, 0, 30);
        pageDownTimer.Start();
        pageDownTimer.Tick += (o, ea) =>
        {
            if (pagedown)
            {
                var sv = XDG.FindVisualChild<ScrollViewer>();
                sv.PageDown();
                pageDownTimer.Start();
            }
            else
            {
                pageDownTimer.Stop();
            }
        };
    }

    private void XBTNPageDown_MouseUp(object sender, MouseButtonEventArgs e)
    {
        pagedown = false;
    }

This is the extension method

Place it in a static class of your choice and add reference to code above.

   public static T FindVisualChild<T>(this DependencyObject depObj) where T : DependencyObject
    {
        if (depObj != null)
        {
            for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
            {
                DependencyObject child = VisualTreeHelper.GetChild(depObj, i);
                if (child != null && child is T)
                {
                    return (T)child;
                }

                T childItem = FindVisualChild<T>(child);
                if (childItem != null) return childItem;
            }
        }
        return null;
    }

NOTE: The property sv could be moved to avoid repeated work.

Anyone have an RX way to do this?

JWP
  • 6,672
  • 3
  • 50
  • 74
0

If you used dataview for the datagrid.datacontext, you can use this:

private void dgvRecords_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
    var dv = dgvRecords.DataContext as DataView;
    if (dv.Count > 0)
    {
        var drv = dv[dv.Count - 1] as DataRowView;
        dgvRecords.ScrollIntoView(drv);
    }
}
Laurel
  • 5,965
  • 14
  • 31
  • 57
0

Following code works for me;

Private Sub DataGrid1_LoadingRow(sender As Object, e As DataGridRowEventArgs) Handles DataGrid1.LoadingRow
    DataGrid1.ScrollIntoView(DataGrid1.Items.GetItemAt(DataGrid1.Items.Count - 1))
End Sub
0

If you are looking for a MVVM way of doing autoscroll, then you can use autoscroll behavior. The behavior scrolls to a selected item, just add a reference to System.Windows.Interactivity.dll:

public class ScrollIntoViewBehavior : Behavior<DataGrid>
{
    protected override void OnAttached()
    {
        base.OnAttached();
        AssociatedObject.SelectionChanged += new SelectionChangedEventHandler(AssociatedObject_SelectionChanged);
    }

    void AssociatedObject_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        if (sender is DataGrid)
        {
            DataGrid grid = (sender as DataGrid);
            if (grid?.SelectedItem != null)
            {
                grid.Dispatcher.InvokeAsync(() =>
                {
                    grid.UpdateLayout();
                    grid.ScrollIntoView(grid.SelectedItem, null);
                });
            }
        }
    }

    protected override void OnDetaching()
    {
        base.OnDetaching();
        AssociatedObject.SelectionChanged -=
            new SelectionChangedEventHandler(AssociatedObject_SelectionChanged);
    }
}

XAML

<DataGrid>
    <i:Interaction.Behaviors>
        <local:ScrollIntoViewBehavior/>
    </i:Interaction.Behaviors>
</DataGrid>
Sasha Yakobchuk
  • 471
  • 6
  • 12
0

When you are use datagridview with the scrollbar must use this technique. becuase i was try other technique. but those are not work properly....

var border = VisualTreeHelper.GetChild(mainDataGrid, 0) as Decorator;
if(border != null)
{   var scroll = border.Child as ScrollViewer;
    if (scroll != null) scroll.ScrollToEnd(); 
}
f.khantsis
  • 3,256
  • 5
  • 50
  • 67
0

If you are using MVVM pattern, you can have a combination of this article with this other: http://www.codeproject.com/KB/WPF/AccessControlsInViewModel.aspx.

The idea is to use attached properties to access the control in your ViewModel class. Once you do that, you would need to check that the datagrid is not null, and it has any items.

if ((mainDataGrid != null) && (mainDataGrid.Items.Count > 0)){
//Same snippet
}
PILuaces
  • 439
  • 4
  • 4