coldheart
coldheart

Reputation: 17

Pagination control RAM consumption

I created a paginated panel, which loads list of controls and load certain quantity of them on every page so you can switch it. It looks like so Paginated panel and use the following logic:

PaginatedPanel.cs

public partial class PaginatedPanel : UserControl
{
    public int currentPage { get; set; } = 1;
    public int itemsPerPage { get; } = 10;
    public int pagesQt { get; set; } 

    private List<Control> items;
    public PaginatedPanel()
    {
        InitializeComponent();
    }

    public void Add(List<Control> controls)
    {
        items = controls;

        pagesQt = (int)Math.Ceiling((decimal)items.Count / itemsPerPage);

        LoadPage(currentPage);

        pgNumlbl.Text = $"{currentPage}/{pagesQt}";
    }

    private void LoadPage(int pageNum)
    {
        ClearPanel();

        int startIndex = itemsPerPage * (pageNum - 1);

        for (int itemNum = startIndex, numOnPage = 0; numOnPage < itemsPerPage; itemNum++, numOnPage++)
        {
            if (itemNum > items.Count - 1)
            {
                break;
            }

            flowLayoutPanel1.Controls.Add(items[itemNum]);
        }
    }

    private void ClearPanel()
    {
        //Queue<Control> disposeQueue = new Queue<Control>();

        //foreach (Control control in flowLayoutPanel1.Controls)
        //{
        //    disposeQueue.Enqueue(control);
        //}

        flowLayoutPanel1.Controls.Clear();

        //foreach (Control control in disposeQueue)
        //{
        //    control.Dispose();
        //}
    }

    private void prevBtn_Click(object sender, EventArgs e)
    {
        if (currentPage > 1)
        {
            currentPage--;
            LoadPage(currentPage);
            pgNumlbl.Text = $"{currentPage}/{pagesQt}";
        }
    }

    private void nextBtn_Click(object sender, EventArgs e)
    {
        if (currentPage < pagesQt)
        {
            currentPage++;
            LoadPage(currentPage);
            pgNumlbl.Text = $"{currentPage}/{pagesQt}";
        }
    }
}

I tested it with buttons and it works OK, so even millions of controls being loaded don't make form to freeze or something. But one thing I noticed is that continuous constant switching of pages increases RAM consumption, I have no idea why. It adds and removes the same controls which are being stored in the items list. Any suggestions why this happens?

Upvotes: 0

Views: 99

Answers (1)

IV.
IV.

Reputation: 9438

Having a list of "millions" of controls likely does not set you up to win here. What I'm recommending (and what I understand Olivier Jacot-Descombes to be recommending) is to decouple your data from the UI. You said

I created a paginated panel, which loads list of controls.

It's loading that many controls that gives us pause here. In the case of "10 items per page" then you need 10 instances of a content presenter (that you design) that loads from a data model which is basically a metadata header. Don't put actual images into this, but rather (as suggested in the comments) link to the image location on disk (or in the cloud as is done in the code below). This keeps the size down of course.

As a general rule for a recycling-style view, you don't want to create a lot more controls than you can actually see at one time.


Content Presenter and its View Model

This model includes some info on what controls to show, whether to check the CheckBox and so on.

public partial class ContentPresenter : UserControl
{
    public ContentPresenter()
    {
        InitializeComponent();
        Visible = DataContext != null;
    }
    protected override void OnDataContextChanged(EventArgs e)
    {
        base.OnDataContextChanged(e);

        // Customize the behavior when the model loads.
        if (DataContext is ContentPresenterViewModel model)
        {
            pictureBox.ImageLocation = model.ImageLocation;
            pictureBox.Visible = model.PresentationOptions.HasFlag(PresentationOption.PictureBox);
            checkBox.Visible = model.PresentationOptions.HasFlag(PresentationOption.CheckBox);
            button.Visible = model.PresentationOptions.HasFlag(PresentationOption.Button);
            checkBox.Checked = model.Checked == true;
        }

        Visible = DataContext != null;
    }
}

[Flags]
public enum PresentationOption { PictureBox = 0x1, CheckBox = 0x2, Button = 0x4 }

[JsonObject]
public class ContentPresenterViewModel
{
    static Random _randoPic = new Random(2), _randoOption = new Random(1);
    public ContentPresenterViewModel() { }
    public ContentPresenterViewModel(bool testData) : this()
    {
        if (testData)
        {
            ImageLocation = $"https://picsum.photos/id/{_randoPic.Next(13, 200)}/300/200";
            PresentationOptions = (PresentationOption)_randoOption.Next(1, 8);
            Checked = _randoOption.Next(0, 2) == 1;
            switch (_randoOption.Next(1, 3))
            {
                case 0:
                    PresentationOptions = PresentationOption.PictureBox;
                    break;
                case 1:
                    PresentationOptions = PresentationOption.PictureBox | PresentationOption.CheckBox | PresentationOption.Button;
                    ButtonText = "Edit";
                    break;
                case 2:
                    PresentationOptions = PresentationOption.PictureBox;
                    ButtonText = "Button Only";
                    break;
                default: throw new InvalidOperationException();
            }
        }
    }
    public PresentationOption PresentationOptions { get; set; } = PresentationOption.PictureBox;
    public int? Width { get; set; }
    public int? Height { get; set; }
    public Color? ForeColor { get; set; }
    public Color? BackColorColor { get; set; }
    public string? ImageLocation { get; set; }
    public bool? Checked { get; set; }
    public string ButtonText { get; set; } = string.Empty;
}

Memory Profile

Having a million records into memory probably won't happen once things are optimized with lazy loading and such, but let's look at the memory profile and performance of doing that. The app seems to be behaving pretty decently. And by the way, this takes only a couple of seconds to load them all.

screenshot


Customized Flow Layout Panel

Here's an example of loading in response to property changes for CurrentPage that also compensates for any partial page at the end of the list.


public partial class ContentPresenterFlowLayout : UserControl, INotifyPropertyChanged
{
    const int DEFAULT_PAGE_SIZE = 10;
    private ContentPresenter[] ContentPresenters { get; set; } = [];
    public ObservableCollection<ContentPresenterViewModel> Items
    {
        get
        {
            if (_items is null)
            {
                _items = new ObservableCollection<ContentPresenterViewModel>();
                _items.CollectionChanged += (sender, e) =>
                {
                    if (_items.Any())
                    {
                        _currentPage = Math.Max(1, _currentPage);
                        _currentPage = Math.Min(MaxPage, _currentPage);
                    }
                    else _currentPage = 0;
                    OnCurrentPageChanged();
                };
            }
            return _items;
        }
    }
    ObservableCollection<ContentPresenterViewModel>? _items = default;
    public ContentPresenterFlowLayout()
    {
        InitializeComponent();
        OnPageSizeChanged();
        // Suppress collection event changes during range loads.
        AddingRange.FinalDispose += (sender, e) =>
        {
            if(InvokeRequired) BeginInvoke(() => OnCurrentPageChanged());
            else OnCurrentPageChanged();
        };
        buttonPrev.Click += (sender, e) => CurrentPage--;
        buttonNext.Click += (sender, e) => CurrentPage++;
    }

    private void OnPageSizeChanged()
    {
        flowLayoutPanel.Controls.Clear();
        ContentPresenters = Enumerable.Range(0, PageSize).Select(_=>new ContentPresenter()).ToArray();
        foreach (var cp in ContentPresenters)
        {
            flowLayoutPanel.Controls.Add(cp);
        }
    }

    [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible), Browsable(true)]
    public int PageSize
    {
        get => _pageSize;
        set
        {
            value = Math.Max(value, 1); 
            if (!Equals(_pageSize, value))
            {
                _pageSize = value;
                OnPageSizeChanged();
                OnPropertyChanged();
            }
        }
    }
    int _pageSize = DEFAULT_PAGE_SIZE;
    int CurrentPage
    {
        get => _currentPage;
        set
        {
            value = Math.Max(value, MinPage);
            value = Math.Min(value, MaxPage);
            if (!Equals(_currentPage, value))
            {
                _currentPage = value;
                OnCurrentPageChanged();
                OnPropertyChanged();
                Debug.WriteLine($"CurrentPage={CurrentPage} MinIndex={RangeMinIndex} MaxIndex={RangeMaxIndex}");
            }
        }
    }
    int _currentPage = default;
    int RangeMinIndex => Items.Any()  ? (CurrentPage -1) * PageSize  : 0;
    int RangeMaxIndex => Items.Any() ? Math.Min(Items.Count, RangeMinIndex + PageSize)  : 0;
    int MinPage => Items.Any() ? 1 : 2;
    int MaxPage => Items.Any() ? 1 + (Items.Count / PageSize)  : 0;
    protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)=>
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));

    public event PropertyChangedEventHandler? PropertyChanged;
}

Upvotes: 1

Related Questions