dotnet / wpf

WPF is a .NET Core UI framework for building Windows desktop applications.
MIT License
7.1k stars 1.17k forks source link

Regarding the issue of lazy loading:When using itemscontrol, listview, and listbox to implement dynamic data loading, there may be limitations with scrollbars. #9756

Open LiamYuan opened 2 months ago

LiamYuan commented 2 months ago

When using itemscontrol, listview, and listbox to implement dynamic data loading, there may be limitations with scrollbars.

The specific situation is that when the data volume is 1 million, 100 records need to be displayed each time. So when the user browses through 90 records, they need to add 50 records and delete the previous records to keep the loaded records at 100. At this time, the scrollbar will reach the bottom and cannot trigger a new load. May I ask if it is possible to open up the ScrollContentViewer? I would like to customize a scrollbar that allows for infinite scrolling. Thank you for reading.

miloush commented 2 months ago

What do you mean by open up? There is no ScrollContentViewer. There is ScrollViewer and ScrollContentPresenter. You can provide your own template for controls, including changing the scrollbars.

h3xds1nz commented 2 months ago

One of the easy options to do this is to handling ScrollViewer.ScrollChanged, finding the ScrollViewer itself via visual tree from the GridView and just scrolling using the viewer when the event happens. It is dirty but it will work flawlessly when you asynchronously load data meanwhile and your scroll bar will stay in the middle (ScrollToVerticalOffset).

Better way is to obviously supply your own ScrollViewer code that faciliates this for you, so you're not undoing the previous act of scrolling.

LiamYuan commented 2 months ago

What do you mean by open up? There is no ScrollContentViewer. There is ScrollViewer and ScrollContentPresenter. You can provide your own template for controls, including changing the scrollbars.

You are right, it is the ScrollContentPresenter, because it cannot be inherited and is deeply bound to track and thumb, causing the scrollbar to know the total amount of content, but when lazy loading, only a small part is displayed each time, which makes it easy for the scrollbar to reach the bottom or top and cannot continue to trigger loading events.

LiamYuan commented 2 months ago

One of the easy options to do this is to handling ScrollViewer.ScrollChanged, finding the ScrollViewer itself via visual tree from the GridView and just scrolling using the viewer when the event happens. It is dirty but it will work flawlessly when you asynchronously load data meanwhile and your scroll bar will stay in the middle (ScrollToVerticalOffset).

Better way is to obviously supply your own ScrollViewer code that faciliates this for you, so you're not undoing the previous act of scrolling.

You are right, no matter what, I need to rewrite the scrollviewer so that the scrollbar is not deeply bound to the content. I just want it to be as simple as possible, so I want to inherit the ScrollContentPresenter, and I just need to implement a customized scrollbar.

miloush commented 2 months ago

If you retemplate the ScrollViewer to use up/down buttons rather than a scrollbar, it might do. Do you have a sample project?

LiamYuan commented 2 months ago

如果您重新模板化ScrollViewer以使用向上/向下按钮而不是滚动条,则可能会有用。您有示例项目吗?

DynamicScrollViewer.zip This is my testing project,

miloush commented 2 months ago

I think you are making your life a bit too complicated. As I said retemplating ScrollViewer is probably the only thing you need, or if you want your own control, you can derive from it directly.

The ScrollViewer gives you ScrollChangedEventArgs when scrolling occurs. Don't throw that away, use the information to decide whether to load data or not.

You seem to be designing your own scroll bar. How do you expect the thumb to behave when data is loaded/removed? Do you want it to move somewhere?

LiamYuan commented 2 months ago

I think you are making your life a bit too complicated. As I said retemplating ScrollViewer is probably the only thing you need, or if you want your own control, you can derive from it directly.

The ScrollViewer gives you ScrollChangedEventArgs when scrolling occurs. Don't throw that away, use the information to decide whether to load data or not.

You seem to be designing your own scroll bar. How do you expect the thumb to behave when data is loaded/removed? Do you want it to move somewhere?

Because the data volume is in the millions, the ItemsControl can only bind 100 entries, so the length of the scrollbar is also calculated based on these 100 entries, not the millions of entries. At this point, the position of the scrollbar can no longer reflect the overall data volume, so it is sufficient to keep the slider in the center.

LiamYuan commented 2 months ago

I think you are making your life a bit too complicated. As I said retemplating ScrollViewer is probably the only thing you need, or if you want your own control, you can derive from it directly.

The ScrollViewer gives you ScrollChangedEventArgs when scrolling occurs. Don't throw that away, use the information to decide whether to load data or not.

You seem to be designing your own scroll bar. How do you expect the thumb to behave when data is loaded/removed? Do you want it to move somewhere?

I'm not sure if you've used Picasa, which is a product from Google. Its content scrollbar is very suitable for handling large amounts of data. QQ图片20240911214120

miloush commented 2 months ago

I agree the situation is not very intuitive and it would be helpful if there was a sample.

While the Scroll event is not fired when there is nowhere to scroll, this is determined during execution of commands that the scrollbar produces. You can catch preview versions of the commands and load data as needed there.

So you subscribe to CommandManager.PreviewExecuted="OnScrollViewerPreviewExecuted" and in the handler you can do something like

private void OnScrollViewerPreviewExecuted(object sender, ExecutedRoutedEventArgs e)
{
    var scrollViewer = sender as DynamicScrollViewer.DynamicScrollViewer;
    var viewModel = DataContext as VendorViewModel;

    if (e.Command == ScrollBar.ScrollToTopCommand || 
        ((e.Command == ScrollBar.LineUpCommand || e.Command == ScrollBar.PageUpCommand) && IsAtTop(scrollViewer.ScrollViewer)))
        viewModel?.LoadPreviousData();

    else if (e.Command == ScrollBar.ScrollToBottomCommand ||
        ((e.Command == ScrollBar.LineDownCommand || e.Command == ScrollBar.PageDownCommand) && IsAtBottom(scrollViewer.ScrollViewer)))
        viewModel?.LoadMoreData();
}

private bool IsAtTop(ScrollViewer scrollViewer) => scrollViewer.VerticalOffset <= 0;
private bool IsAtBottom(ScrollViewer scrollViewer) => scrollViewer.VerticalOffset >= scrollViewer.ExtentHeight - scrollViewer.ViewportHeight;

I don't think there is a problem or limitation with scrollbars. What you need is buttons, not scrollbars, and you can retemplate ScrollViewer to not contain a scrollbar.

LiamYuan commented 2 months ago

I agree the situation is not very intuitive and it would be helpful if there was a sample.

While the Scroll event is not fired when there is nowhere to scroll, this is determined during execution of commands that the scrollbar produces. You can catch preview versions of the commands and load data as needed there.

So you subscribe to CommandManager.PreviewExecuted="OnScrollViewerPreviewExecuted" and in the handler you can do something like

private void OnScrollViewerPreviewExecuted(object sender, ExecutedRoutedEventArgs e)
{
    var scrollViewer = sender as DynamicScrollViewer.DynamicScrollViewer;
    var viewModel = DataContext as VendorViewModel;

    if (e.Command == ScrollBar.ScrollToTopCommand || 
        ((e.Command == ScrollBar.LineUpCommand || e.Command == ScrollBar.PageUpCommand) && IsAtTop(scrollViewer.ScrollViewer)))
        viewModel?.LoadPreviousData();

    else if (e.Command == ScrollBar.ScrollToBottomCommand ||
        ((e.Command == ScrollBar.LineDownCommand || e.Command == ScrollBar.PageDownCommand) && IsAtBottom(scrollViewer.ScrollViewer)))
        viewModel?.LoadMoreData();
}

private bool IsAtTop(ScrollViewer scrollViewer) => scrollViewer.VerticalOffset <= 0;
private bool IsAtBottom(ScrollViewer scrollViewer) => scrollViewer.VerticalOffset >= scrollViewer.ExtentHeight - scrollViewer.ViewportHeight;

I don't think there is a problem or limitation with scrollbars. What you need is buttons, not scrollbars, and you can retemplate ScrollViewer to not contain a scrollbar.

Hello, thank you for your guidance. I have gained a deeper understanding of CommandManager and ScrollChangedEventArgs. However, as I expected, in the initial stage when the data is loaded, it can scroll normally to the bottom one by one, but there are issues when the scrollbar is at the bottom or top. If you continue to click the down button, it suddenly turns into a page-flipping effect.

The reason for this is that after clicking the down button, data needs to be loaded in the ObservableCollection, and excess old data needs to be deleted to keep the ObservableCollection at 100 readable data points. At this point, the ScrollViewer is not aware that the data has changed; it still calculates the slider's position based on the quantity, so it remains at the bottom and does not scroll line by line.

In practice, after loading up to 150 records, it jumps directly to 200 records, and then every time the down button is clicked, it jumps to the last record. If I adjust the slider's position based on the content, it will make users feel like they are jumping around instead of reading smoothly.