0

I have a data grid, while scroling when there are less than 50 records left, the event of retrieving subsequent records is triggered. I start the download process in Task.Run.

There is a situation when the user scrolls the scroll bar to the end with the mouse, the program begins to download new records forever. When I run the program through Visual Studio, the interface does not crash and I can interrupt this download by moving the scroll bar up.

But when the program is launched via .exe, the interface is frozen and nothing can be done.

In this case, always e.ExtentHeight - e.VerticalOffset == 27.

I try to set the scroll bar somehow after each call download to get e.ExtentHeight - e.VerticalOffset> 50. But I don't know how to do this for DataGrid I only see ScrollIntoView () but when I used it I not seen any changes.

I made a test program where the same problem occurs when .exe starts:

public partial class MainWindow : Window
{

    public List<Event> EventsList { get; set; }
    public MainWindow()
    {
        InitializeComponent();
        EventsList = new List<Event>();
    }

    private void EventDataGrid_ScrollChanged(object sender, ScrollChangedEventArgs e)
    {
        Task.Run(() =>
        {
            if (e.ExtentHeight - 50 < e.VerticalOffset)
                TakeEvents();
        });
    }
    Random r = new Random();
    int lastIndex = 0;
    private void TakeEvents()
    {
        Stopwatch s = new Stopwatch();
        s.Start();

        for (int i = 0; i < 900; i++)
        {
            EventsList.Add(new Event()
            {
                Device = 1,
                Index = lastIndex++
            });
        }
        s.Stop();
        RefreshEventsList();
    }
    private void RefreshEventsList()
    {
        Task.Run(() =>
        {
            Dispatcher.Invoke(() =>
            {
                var filtrList = EventsList.Where(x => x != null && x.Device != 0).ToList();
                EventsDataGrid.ItemsSource = filtrList;
            });
        });
    }

}
public class Event
{
    public int Index { get; set; }
    public int Device { get; set; }
}

XAML:

<Grid>
    <DataGrid x:Name="EventsDataGrid" FontSize="15" Grid.Row="1" AutoGenerateColumns="False" ItemsSource="{Binding}" IsReadOnly="True" ScrollViewer.HorizontalScrollBarVisibility="Disabled" VerticalAlignment="Stretch" 
                  Background="Transparent" Foreground="Black" HeadersVisibility="Column" VerticalScrollBarVisibility="Visible" BorderThickness="0" ScrollViewer.ScrollChanged="EventDataGrid_ScrollChanged" >
        <DataGrid.Columns>
            <DataGridTextColumn Width="*" Binding="{Binding Index}" Header="Index" Foreground="Black"/>
            <DataGridTextColumn Width="*" Binding="{Binding Device}" Header="Device" Foreground="Black"/>
        </DataGrid.Columns>
    </DataGrid>
</Grid>
Silny ToJa
  • 1,815
  • 1
  • 6
  • 20

2 Answers2

2

Try the following, placed some comments if unclear please leave comments

public partial class MainWindow : Window
{

    public List<Event> EventsList { get; set; }
    public MainWindow()
    {
        InitializeComponent();
        EventsList = new List<Event>();
    }

    private void EventDataGrid_ScrollChanged(object sender, ScrollChangedEventArgs e)
    {
        if (_blockRefresh)
            return;

        if (e.ExtentHeight - 50 < e.VerticalOffset)
            TakeEvents();
    }
    private int _lastIndex = 0;
    private bool _blockRefresh;

    private void TakeEvents()
    {
        Stopwatch s = new Stopwatch();
        s.Start();

        for (int i = 0; i < 900; i++)
        {
            EventsList.Add(new Event()
            {
                Device = 1,
                Index = _lastIndex++
            });
        }
        s.Stop();
        RefreshEventsList();
    }
    private void RefreshEventsList()
    {
        _blockRefresh = true;
        _ = Dispatcher.BeginInvoke((Action)(() =>
        {
            var oldList = EventsDataGrid.ItemsSource as IList<Event>;
            var oldIndex = oldList?.Count - 1 ?? -1;
            var filterList = EventsList.Where(x => x != null && x.Device != 0).ToList();

            EventsDataGrid.ItemsSource = filterList;
            
            //scroll to the previous last item, to prevent the condition e.ExtentHeight - 50 < e.VerticalOffset to be true once again prevents endless loop
            //scrolling will jump if scrollbar is moved by holding the left mouse button
            if (oldIndex != -1)
                EventsDataGrid.ScrollIntoView(oldIndex);

            _ = Dispatcher.BeginInvoke((Action)(() =>
            {
                //when UI updated "unlock" the refresh routine
                _blockRefresh = false;
            }), DispatcherPriority.ApplicationIdle);
        }), DispatcherPriority.ApplicationIdle);
    }
}
Rand Random
  • 7,300
  • 10
  • 40
  • 88
  • Cannot convert lambda expression to type 'DispatcherPriority' because it is not a delegate type. I used System.Windows.Threading. – Silny ToJa Jul 02 '21 at 10:35
  • @SilnyToJa - updated code to make it compile to .net framework (used .net 5 for my demo) – Rand Random Jul 02 '21 at 10:37
  • unfortunately I am not able to use net 5.0, max 4.6.1 – Silny ToJa Jul 02 '21 at 10:40
  • @SilnyToJa - the edited code should work, try again – Rand Random Jul 02 '21 at 10:40
  • Thanks, it doesn't make error now, but the disadvantage of this solution is that you can't get new records too quickly now. It may also suggest to the user that there are no more records, when he take scroll to end by mouse and it dont take new record. – Silny ToJa Jul 02 '21 at 11:07
  • You have the issue that the cursor can be in a position where your condition `if (e.ExtentHeight - 50 < e.VerticalOffset)` is always true, and by changing the itemssource the event `EventDataGrid_ScrollChanged` will trigger so you are stuck with an endless loop. You would need to find a way to encourage the user to release the mouse and start the scroll process again. You can have a look at this web page (first google search result) https://infinite-scroll.com/demo/full-page/ to test the behaviour. Chrome cheats by doing an offset of cursor position and scroll bar thumb. – Rand Random Jul 02 '21 at 11:26
  • @SilnyToJa - did you give google search a try: https://www.google.at/search?q=wpf+datagrid+infinite+scroll - first hit: https://stackoverflow.com/questions/7581732/wpf-datagrid-lazy-loading-inifinite-scroll – Rand Random Jul 02 '21 at 11:28
  • 2
    I remove if (_blockRefresh) return; and it works as expected. – Silny ToJa Jul 02 '21 at 11:30
  • In my original program, where fetching new items takes a bit long, threw _blockRefresh causes error. But moving _blockRefresh = false; after EventsDataGrid.ItemsSource = filterList; gave the possibility of fast downloading, and at the same time the program does not freeze. – Silny ToJa Jul 02 '21 at 11:40
  • I think using `Dispatcher.BeginInvoke` is good, because the issue you described sounds like a *deadlock* on the UI thread, caused by `Dispatcher.Invoke`. Now you simply have to get your arguments in line. – lidqy Jul 02 '21 at 11:44
0

Not sure what exactly your code is supposed to do, but you should at least make the following modifications:

Make the ScrollChanged handler async and await the Task. Don't run the Task if not necessary:

private async void EventDataGrid_ScrollChanged(object sender, ScrollChangedEventArgs e)
{
    if (e.ExtentHeight - 50 < e.VerticalOffset)
    {
        await Task.Run(TakeEvents);
    }
}

Create the filtered List in the background thread, and do not run a Task just to call Dispatcher.Invoke:

private void TakeEvents()
{
    ...
    s.Stop();

    var filteredList = EventsList.Where(e => e.Device != 0).ToList();

    Dispatcher.Invoke(() => EventsDataGrid.ItemsSource = filteredList);
}
Clemens
  • 123,504
  • 12
  • 155
  • 268