Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Dec 3, 2025

Infinite scroll pagination wasn't triggering because ScrollViewer detection failed—ListBox with WrapPanel doesn't create a standard internal ScrollViewer.

Changes

ScrollViewer Detection (AlbumsListView.axaml.cs)

  • Search descendants first, then ancestors, then walk up visual tree
  • Proper subscribe/unsubscribe lifecycle to prevent memory leaks
// First try descendants of ListBox
_scrollViewer = listBox?.GetVisualDescendants().OfType<ScrollViewer>().FirstOrDefault();

// If not found, try ancestors
if (_scrollViewer == null)
    _scrollViewer = this.GetVisualAncestors().OfType<ScrollViewer>().FirstOrDefault();

// Last resort: walk up visual tree
if (_scrollViewer == null && listBox != null)
{
    var current = listBox.GetVisualParent();
    while (current != null)
    {
        if (current is ScrollViewer sv) { _scrollViewer = sv; break; }
        current = current.GetVisualParent();
    }
}

Pagination Infrastructure

  • API: Added GetAlbumsPagedAsync(libraryId, startIndex, limit, sortBy, sortOrder) with server-side sorting
  • ViewModel: Added HasMoreAlbums, IsLoadingMoreAlbums, LoadMoreAlbumsCommand; LoadAlbumsAsync resets pagination state
  • XAML: Added x:Name="AlbumsListBox", loading indicator, "X OF Y ALBUMS" count format

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • av-build-tel-api-v1.avaloniaui.net
    • Triggering command: /usr/share/dotnet/dotnet dotnet exec --runtimeconfig /home/REDACTED/.nuget/packages/avalonia.buildservices/0.0.31/tools/netstandard2.0/runtimeconfig.json /home/REDACTED/.nuget/packages/avalonia.buildservices/0.0.31/tools/netstandard2.0/Avalonia.BuildServices.Collector.dll (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Original prompt

Summary

Fix the infinite scroll pagination for albums - the scroll detection is not working because the ScrollViewer is not being found correctly.

Current Problem

In PR #12, infinite scroll pagination was implemented but it's not working. The user never sees the "Loading more albums..." text, and only 50 albums load even though they have 1500+ albums.

The issue is in AlbumsListView.axaml.cs. The current code tries to find a ScrollViewer inside the ListBox:

var listBox = this.FindControl<ListBox>("AlbumsListBox");
var scrollViewer = listBox?.GetVisualDescendants().OfType<ScrollViewer>().FirstOrDefault();

However, the ListBox uses a WrapPanel as its ItemsPanel:

<ListBox.ItemsPanel>
    <ItemsPanelTemplate>
        <WrapPanel Orientation="Horizontal" />
    </ItemsPanelTemplate>
</ListBox.ItemsPanel>

WrapPanel is NOT a virtualizing panel, which means the ListBox may not create its internal ScrollViewer in the typical way, or the ScrollViewer found doesn't actually handle the scrolling.

Solution

Update AlbumsListView.axaml.cs to search for the ScrollViewer more thoroughly:

using Avalonia.Controls;
using Avalonia.VisualTree;
using JamBox.Core.ViewModels;
using System.Reactive.Linq;

namespace JamBox.Core.Views.UserControls;

public partial class AlbumsListView : UserControl
{
    private const double ScrollThresholdPixels = 200;
    private ScrollViewer? _scrollViewer;

    public AlbumsListView()
    {
        InitializeComponent();

        this.AttachedToVisualTree += OnAttachedToVisualTree;
        this.DetachedFromVisualTree += OnDetachedFromVisualTree;
    }

    private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
    {
        // The ListBox with WrapPanel may not have a standard internal ScrollViewer,
        // so we need to search more thoroughly
        var listBox = this.FindControl<ListBox>("AlbumsListBox");
        
        // First try to find ScrollViewer in descendants of the ListBox
        _scrollViewer = listBox?.GetVisualDescendants().OfType<ScrollViewer>().FirstOrDefault();
        
        // If not found in descendants, try ancestors (in case the control is inside a parent ScrollViewer)
        if (_scrollViewer == null)
        {
            _scrollViewer = this.GetVisualAncestors().OfType<ScrollViewer>().FirstOrDefault();
        }
        
        // As a last resort, search the entire visual tree from the ListBox upward
        if (_scrollViewer == null && listBox != null)
        {
            // Walk up to find any ScrollViewer that might contain us
            var current = listBox.GetVisualParent();
            while (current != null)
            {
                if (current is ScrollViewer sv)
                {
                    _scrollViewer = sv;
                    break;
                }
                current = current.GetVisualParent();
            }
        }

        if (_scrollViewer != null)
        {
            _scrollViewer.ScrollChanged += OnScrollChanged;
            System.Diagnostics.Debug.WriteLine($"AlbumsListView: Found ScrollViewer: {_scrollViewer.GetType().Name}");
        }
        else
        {
            System.Diagnostics.Debug.WriteLine("AlbumsListView: WARNING - No ScrollViewer found!");
        }
    }

    private void OnDetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
    {
        if (_scrollViewer != null)
        {
            _scrollViewer.ScrollChanged -= OnScrollChanged;
            _scrollViewer = null;
        }
    }

    private void OnScrollChanged(object? sender, ScrollChangedEventArgs e)
    {
        if (_scrollViewer == null) return;

        // Check if we're near the bottom
        var distanceFromBottom = _scrollViewer.Extent.Height - _scrollViewer.Offset.Y - _scrollViewer.Viewport.Height;

        System.Diagnostics.Debug.WriteLine($"Scroll: Extent={_scrollViewer.Extent.Height}, Offset={_scrollViewer.Offset.Y}, Viewport={_scrollViewer.Viewport.Height}, DistanceFromBottom={distanceFromBottom}");

        if (distanceFromBottom < ScrollThresholdPixels)
        {
            if (DataContext is LibraryViewModel vm && vm.HasMoreAlbums && !vm.IsLoadingMoreAlbums)
            {
                System.Diagnostics.Debug.WriteLine("Triggering LoadMoreAlbumsCommand");
                vm.LoadMoreAlbumsCommand?.Execute().Subscribe(_ => { }, _ => { });
            }
        }
    }
}

Additional Debugging

The debug output will help identify:

  1. Whether a ScrollViewer is being found at all
  2. The scroll position values when scrolling
  3. Whether the LoadMoreAlbumsCommand is being triggered

Alternative Approach (if the above doesn't work)

If the ScrollViewer is still not found or not working, wrap the ListBox in an explicit ScrollViewer in AlbumsListView.axaml:

<Grid Grid.Row="1">
    <ScrollViewer x:Name="AlbumsScrollViewer">
        <ListBox
            x:Name="AlbumsList...

</details>

*This pull request was created as a result of the following prompt from Copilot chat.*
> ## Summary
> Fix the infinite scroll pagination for albums - the scroll detection is not working because the `ScrollViewer` is not being found correctly.
> 
> ## Current Problem
> In PR #12, infinite scroll pagination was implemented but it's not working. The user never sees the "Loading more albums..." text, and only 50 albums load even though they have 1500+ albums.
> 
> The issue is in `AlbumsListView.axaml.cs`. The current code tries to find a `ScrollViewer` inside the `ListBox`:
> 
> ```csharp
> var listBox = this.FindControl<ListBox>("AlbumsListBox");
> var scrollViewer = listBox?.GetVisualDescendants().OfType<ScrollViewer>().FirstOrDefault();
> ```
> 
> However, the `ListBox` uses a `WrapPanel` as its `ItemsPanel`:
> ```xml
> <ListBox.ItemsPanel>
>     <ItemsPanelTemplate>
>         <WrapPanel Orientation="Horizontal" />
>     </ItemsPanelTemplate>
> </ListBox.ItemsPanel>
> ```
> 
> `WrapPanel` is NOT a virtualizing panel, which means the `ListBox` may not create its internal `ScrollViewer` in the typical way, or the `ScrollViewer` found doesn't actually handle the scrolling.
> 
> ## Solution
> 
> Update `AlbumsListView.axaml.cs` to search for the `ScrollViewer` more thoroughly:
> 
> ```csharp
> using Avalonia.Controls;
> using Avalonia.VisualTree;
> using JamBox.Core.ViewModels;
> using System.Reactive.Linq;
> 
> namespace JamBox.Core.Views.UserControls;
> 
> public partial class AlbumsListView : UserControl
> {
>     private const double ScrollThresholdPixels = 200;
>     private ScrollViewer? _scrollViewer;
> 
>     public AlbumsListView()
>     {
>         InitializeComponent();
> 
>         this.AttachedToVisualTree += OnAttachedToVisualTree;
>         this.DetachedFromVisualTree += OnDetachedFromVisualTree;
>     }
> 
>     private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
>     {
>         // The ListBox with WrapPanel may not have a standard internal ScrollViewer,
>         // so we need to search more thoroughly
>         var listBox = this.FindControl<ListBox>("AlbumsListBox");
>         
>         // First try to find ScrollViewer in descendants of the ListBox
>         _scrollViewer = listBox?.GetVisualDescendants().OfType<ScrollViewer>().FirstOrDefault();
>         
>         // If not found in descendants, try ancestors (in case the control is inside a parent ScrollViewer)
>         if (_scrollViewer == null)
>         {
>             _scrollViewer = this.GetVisualAncestors().OfType<ScrollViewer>().FirstOrDefault();
>         }
>         
>         // As a last resort, search the entire visual tree from the ListBox upward
>         if (_scrollViewer == null && listBox != null)
>         {
>             // Walk up to find any ScrollViewer that might contain us
>             var current = listBox.GetVisualParent();
>             while (current != null)
>             {
>                 if (current is ScrollViewer sv)
>                 {
>                     _scrollViewer = sv;
>                     break;
>                 }
>                 current = current.GetVisualParent();
>             }
>         }
> 
>         if (_scrollViewer != null)
>         {
>             _scrollViewer.ScrollChanged += OnScrollChanged;
>             System.Diagnostics.Debug.WriteLine($"AlbumsListView: Found ScrollViewer: {_scrollViewer.GetType().Name}");
>         }
>         else
>         {
>             System.Diagnostics.Debug.WriteLine("AlbumsListView: WARNING - No ScrollViewer found!");
>         }
>     }
> 
>     private void OnDetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
>     {
>         if (_scrollViewer != null)
>         {
>             _scrollViewer.ScrollChanged -= OnScrollChanged;
>             _scrollViewer = null;
>         }
>     }
> 
>     private void OnScrollChanged(object? sender, ScrollChangedEventArgs e)
>     {
>         if (_scrollViewer == null) return;
> 
>         // Check if we're near the bottom
>         var distanceFromBottom = _scrollViewer.Extent.Height - _scrollViewer.Offset.Y - _scrollViewer.Viewport.Height;
> 
>         System.Diagnostics.Debug.WriteLine($"Scroll: Extent={_scrollViewer.Extent.Height}, Offset={_scrollViewer.Offset.Y}, Viewport={_scrollViewer.Viewport.Height}, DistanceFromBottom={distanceFromBottom}");
> 
>         if (distanceFromBottom < ScrollThresholdPixels)
>         {
>             if (DataContext is LibraryViewModel vm && vm.HasMoreAlbums && !vm.IsLoadingMoreAlbums)
>             {
>                 System.Diagnostics.Debug.WriteLine("Triggering LoadMoreAlbumsCommand");
>                 vm.LoadMoreAlbumsCommand?.Execute().Subscribe(_ => { }, _ => { });
>             }
>         }
>     }
> }
> ```
> 
> ## Additional Debugging
> 
> The debug output will help identify:
> 1. Whether a `ScrollViewer` is being found at all
> 2. The scroll position values when scrolling
> 3. Whether the `LoadMoreAlbumsCommand` is being triggered
> 
> ## Alternative Approach (if the above doesn't work)
> 
> If the `ScrollViewer` is still not found or not working, wrap the `ListBox` in an explicit `ScrollViewer` in `AlbumsListView.axaml`:
> 
> ```xml
> <Grid Grid.Row="1">
>     <ScrollViewer x:Name="AlbumsScrollViewer">
>         <ListBox
>             x:Name="AlbumsListBox"
>             ItemsSource="{Binding Albums}"
>             ...
>             ScrollViewer.VerticalScrollBarVisibility="Disabled">
>             <!-- Disable ListBox's internal scrolling since we're using external ScrollViewer -->
>             ...
>         </ListBox>
>     </ScrollViewer>
>     
>     <!-- Loading indicator -->
>     ...
> </Grid>
> ```
> 
> Then in code-behind, simply use:
> ```csharp
> var scrollViewer = this.FindControl<ScrollViewer>("AlbumsScrollViewer");
> ```
> 
> ## Testing
> After the fix:
> 1. Run the app and connect to a Jellyfin server with 1500+ albums
> 2. Scroll down in the albums view
> 3. Verify "Loading more albums..." appears when nearing the bottom
> 4. Verify additional albums are loaded and displayed
> 5. Verify the album count updates (e.g., "100 OF 1500 ALBUMS")

<!-- START COPILOT CODING AGENT TIPS -->
---

💬 We'd love your input! Share your thoughts on Copilot coding agent in our [2 minute survey](https://gh.io/copilot-coding-agent-survey).

Copilot AI self-assigned this Dec 3, 2025
Copilot AI and others added 2 commits December 3, 2025 05:34
- Add paginated API method GetAlbumsPagedAsync to IJellyfinApiService and JellyFinApiService
- Add pagination state and LoadMoreAlbumsCommand to LibraryViewModel
- Update AlbumsListView.axaml with ListBox name and loading indicator
- Improve ScrollViewer detection in AlbumsListView.axaml.cs to search more thoroughly (descendants, ancestors, and visual tree traversal)

Co-authored-by: adrianstevens <5965865+adrianstevens@users.noreply.github.com>
…r handling

Co-authored-by: adrianstevens <5965865+adrianstevens@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix infinite scroll pagination for albums Fix infinite scroll pagination for albums Dec 3, 2025
Copilot AI requested a review from adrianstevens December 3, 2025 05:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants