Polar
Polar

Reputation: 3537

DataGrid auto scroll to bottom for newly added item

I use an ObservableCollection in my ViewModel to add a new record in my DataGrid, so I don't have an access to this control. I wanted to scroll to the bottom of the DataGrid every time a new item is added.

Normally I can just hook into INotifyCollectionChanged from my View, then scroll to the bottom, something like;

public MyView(){
    InitializeComponent();
    CollectionView myCollectionView = (CollectionView)CollectionViewSource.GetDefaultView(MyDataGrid.Items);
    ((INotifyCollectionChanged)myCollectionView).CollectionChanged += new NotifyCollectionChangedEventHandler(DataGrid_CollectionChanged);
}

private void DataGrid_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e){
    if (MyDataGrid.Items.Count > 0){
        if (VisualTreeHelper.GetChild(MyDataGrid, 0) is Decorator border){
            if (border.Child is ScrollViewer scroll) scroll.ScrollToEnd();
        }
    }
}

My problem now is that I have a function to Duplicate and Delete an item, this whole thing is being done in my ViewModel. With the approach above, the DataGrid will always scroll to the bottom even if I deleted or duplicate an item in any position which I don't want to happen. Scrolling to the bottom should only be working for the newly added items.

What should be the approach for this?

Upvotes: -1

Views: 1444

Answers (3)

Polar
Polar

Reputation: 3537

You have two options, first is to compare the difference between the old and new items. Then simply get that item and using DataGrid.ScrollIntoView(item) to scroll into that specific position.

private void DataGrid_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e){
    if (YourDataGrid.Items.Count > 0 && e.Action.Equals(NotifyCollectionChangedAction.Add)){ //Do this only when new item is added.
        if (sender is not CollectionView oldItems) return;
            foreach (var oldItem in oldItems) {
                foreach (var newItem in (e.NewItems)??new ObservableCollection<TransactionItem>()){
                    if (!newItem.Equals(oldItem)){
                        YourDataGrid.ScrollIntoView(newItem); //Scroll into this specific item.   
                        return; //No need to do further checking
                    }
                }
            }
        }
    }
}

However, the same behavior with the answer of @Paolo Iommarini will be expected when you duplicate the last item. Making his approach much better compared to this in terms of performance.

The second option and much better is by using Events. You simply need to define it into your ViewModel, then invoke the event every time a new item is added passing the argument as the newly added item. From your View, subscribe into that event and use DataGrid.ScrollIntoView(item) to scroll.

Here is a more detailed example, in your ViewModel.

public class ViewModel{
 
  //define the event in the ViewModel.
  public delegate void MyEventAction(YourObjectModelItem item);
  public event MyEventAction? MyEvent;
  
  private void AddNewItem(){ //Assuming this function is being called to add the item into your ObservableCollection.
     var new_item = .... //(1) create your item.
     YourObServableCollection.Add(new_item); //(2) add the item into your collection.
     MyEvent?.Invoke(new_item); //(3) invoke the event and pass the new item as the argument.
  }
  
}

Then in your View.

public YourView(){
    InitializeComponent();
    ...
    this.Loaded += (s, a) => { //You should subscribe only when the view is loaded, otherwise you might get a null issue with your DataContext. You may also use DataContextChanged if you want.
        var vm = (ViewModel)DataContext; //(1) Get the ViewModel from your DataContext.
        vm.MyEvent += (item) =>{  //(2) Subscribe to the event from the ViewModel.
            YourDataGrid.ScrollIntoView(item); //(3) Scroll to that item.
        };
    };
}

With this approach, you don't have to make any comparison. You just need to know which item you will be scrolling.

Upvotes: 1

Paolo Iommarini
Paolo Iommarini

Reputation: 259

You can try to check if NotifyCollectionChangedEventArgs.NewStartingIndex is at the end of your collection. You should scroll to the end only if the change has happened at the end.

private void DataGrid_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e){
    if (MyDataGrid.Items.Count > 0 && e.NewStartingIndex == MyDataGrid.Items.Count - 1){
        if (VisualTreeHelper.GetChild(MyDataGrid, 0) is Decorator border){
            if (border.Child is ScrollViewer scroll) scroll.ScrollToEnd();
        }
    }
}

Upvotes: 1

George Kerwood
George Kerwood

Reputation: 1306

You can use the passed NotifyCollectionChangedEventArgs to identify whether a Duplicate or Delete has occurred.

Delete is easy, simply:

if (e.Action == NotifyCollectionChangedAction.Remove)
{
    // An item has been removed from your collection. Do not scroll.
}

Duplicate depends on your definition of what a "duplicate" or "copy" is exactly, but most likely you can use a quick Linq check like:

if (e.Action == NotifyCollectionChangedAction.Add)
{
    // An item has been added, let's see if it's a duplicate.
    CollectionView changedCollection = (CollectionView)sender;
    foreach (myRecordType record in e.NewItems)
    {
        if (changedCollection.Contains(record))
        {
             // This was likely a duplicate added
        }
    }
}

It's worth noting that EventArgs of any type are really there for this very purpose. They'll generally provide you with more information regarding the event for exactly this kind of logical handling.

Upvotes: 0

Related Questions