DEV Community

mohamed Tayel
mohamed Tayel

Posted on

Applying Pagination to a Sorted List c#

Learn how to efficiently implement sorted pagination in C# with a reusable SortedListPaginator class. Discover lazy initialization, encapsulated sorting, and flexible pagination design using interfaces like IPaginatedCollection<T> and IPage<T>. Perfect for handling large datasets with optimized performance!

Designing a system to handle paginated, sorted data efficiently requires balancing flexibility, performance, and usability. In this article, we will create a class named SortedListPaginator that implements the IPaginatedCollection<T> interface. The core idea is to encapsulate the logic of sorting and pagination, ensuring that consumers do not need to worry about these details.


Overview of the Design

  1. Input and Sorting:

    • The class takes an unordered sequence as input.
    • It ensures the sequence is sorted based on a custom criterion provided by the caller.
  2. Lazy Initialization:

    • Sorting is deferred until explicitly required, leveraging the Lazy<T> class to optimize performance.
  3. Read-Only Behavior:

    • Once sorted, the data becomes read-only, ensuring it can be safely shared across collaborating objects.
  4. Pagination:

    • The sorted data is divided into pages, each represented by a concrete implementation of the IPage<T> interface.

The Key Components

IPaginatedCollection Interface

Defines the contract for the collection, allowing access to sorted pages and their metadata.

public interface IPaginatedCollection<T> : IEnumerable<IPage<T>>
{
    int PageCount { get; }
    IPage<T> this[int index] { get; }
}
Enter fullscreen mode Exit fullscreen mode

IPage Interface

Represents a single page, exposing its content and metadata.

public interface IPage<T> : IEnumerable<T>
{
    int Ordinal { get; }
    int Count { get; }
    int PageSize { get; }
}
Enter fullscreen mode Exit fullscreen mode

SortedListPaginator Class

The SortedListPaginator class encapsulates the logic for sorting and pagination.


Implementing the SortedListPaginator

Class Definition

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;

public class SortedListPaginator<T> : IPaginatedCollection<T>
{
    private readonly Lazy<List<T>> _sortedData;
    private readonly int _pageSize;

    public SortedListPaginator(IEnumerable<T> source, int pageSize, Func<T, object> sortKeySelector)
    {
        if (source == null) throw new ArgumentNullException(nameof(source));
        if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize), "Page size must be greater than zero.");
        if (sortKeySelector == null) throw new ArgumentNullException(nameof(sortKeySelector));

        _pageSize = pageSize;

        // Lazy initialization for sorting
        _sortedData = new Lazy<List<T>>(() => source.OrderBy(sortKeySelector).ToList());
    }

    // Total number of pages
    public int PageCount => (int)Math.Ceiling((double)_sortedData.Value.Count / _pageSize);

    // Access a specific page
    public IPage<T> this[int index]
    {
        get
        {
            if (index < 0 || index >= PageCount)
                throw new ArgumentOutOfRangeException(nameof(index), "Page index is out of range.");

            var start = index * _pageSize;
            var pageData = _sortedData.Value.Skip(start).Take(_pageSize);
            return new Page<T>(pageData, index + 1, _pageSize);
        }
    }

    // Enumerator for pages
    public IEnumerator<IPage<T>> GetEnumerator()
    {
        for (int i = 0; i < PageCount; i++)
        {
            yield return this[i];
        }
    }

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
Enter fullscreen mode Exit fullscreen mode

The Page Class

Each page is represented by a Page<T> object that implements the IPage<T> interface.

public class Page<T> : IPage<T>
{
    private readonly List<T> _content;

    public Page(IEnumerable<T> content, int ordinal, int pageSize)
    {
        _content = content.ToList();
        Ordinal = ordinal;
        PageSize = pageSize;
    }

    public int Ordinal { get; }
    public int Count => _content.Count;
    public int PageSize { get; }

    public IEnumerator<T> GetEnumerator() => _content.GetEnumerator();
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
Enter fullscreen mode Exit fullscreen mode

How It Works

  1. Lazy Initialization with Lazy<T>:

    • The Lazy<List<T>> ensures that sorting is deferred until the sorted data is accessed for the first time.
    • This approach avoids unnecessary computation if no pages are requested.
  2. Sorting in the Constructor:

    • The sorting logic is encapsulated in a lambda function passed to the Lazy<T> instance.
    • The lambda executes the sorting (OrderBy) only once, ensuring efficiency.
  3. Pagination Logic:

    • The this[int index] property calculates the appropriate segment of the sorted data and returns it as a Page<T> object.
    • The Page<T> object provides metadata and an enumerator for its content.

Usage Example

class Program
{
    static void Main()
    {
        // Sample data: unsorted numbers
        var data = new List<int> { 5, 3, 1, 4, 2, 10, 9, 8, 7, 6 };

        // Create a SortedListPaginator
        var paginator = new SortedListPaginator<int>(
            data,
            pageSize: 3,
            sortKeySelector: x => x // Sort by value
        );

        Console.WriteLine($"Total Pages: {paginator.PageCount}");

        foreach (var page in paginator)
        {
            Console.WriteLine($"Page {page.Ordinal}:");
            foreach (var item in page)
            {
                Console.WriteLine(item);
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Output

Total Pages: 4
Page 1:
1
2
3
Page 2:
4
5
6
Page 3:
7
8
9
Page 4:
10
Enter fullscreen mode Exit fullscreen mode

Advantages of the Design

  1. Efficiency:

    • Sorting occurs only once, and only when needed.
    • Lazy initialization minimizes overhead.
  2. Encapsulation:

    • Sorting and pagination are fully encapsulated, freeing the consumer from dealing with these complexities.
  3. Read-Only Behavior:

    • The sorted list is immutable, ensuring safe usage across different parts of the application.
  4. Flexibility:

    • The design allows any sorting criterion through the Func<T, object> parameter.

Key Takeaways

  • Lazy Initialization is a powerful pattern for deferring costly operations until needed.
  • Separating the sorting logic from pagination improves maintainability and scalability.
  • Designing with interfaces ensures flexibility and encourages reusable, testable code.

This approach provides an efficient and clean solution for handling sorted, paginated data in any application.

Top comments (0)