Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ItemsRepeater eagerly enumerates the IList<T> it's bound to #6904

Closed
uxmal opened this issue Nov 10, 2021 · 5 comments
Closed

ItemsRepeater eagerly enumerates the IList<T> it's bound to #6904

uxmal opened this issue Nov 10, 2021 · 5 comments
Labels
by-design The behavior reported in the issue is actually correct.

Comments

@uxmal
Copy link

uxmal commented Nov 10, 2021

Describe the bug
It seems that an ItemsRepeater nested inside a ScrollViewer "eagerly" enumerates all the items of the IList<T> that is is bound to. The advertised behaviour is that the list of items is "virtualized", which to me implies that the ItemsRepeater should only iterate enough times to get the items it needs to create its visual state. E.g. if I have a list of 100,000 items, but the visible area of the containing ScrollViewer is only large enough to display 2 items, I expect ItemsRepeater to only fetch the first two items (or at most a handful).

To Reproduce
I've created a VS2019 solution that exhibits the problem (https://github.com/uxmal/avalonia-6904). Just start the program and you'll notice that it takes a few seconds to start up, as 20 instances of class ExpensiveObject are being created, even though the IList<T> container they live in takes steps to defer their construction until IEnumerable<T>.MoveNext() gets called.

In the class MainWindowViewModel you will see the collection being bound to the Items property of ItemsRepeater, called ExpensiveObjects. There are three initializers (two of which are commented out). MakeVirtualList creates an instance of a collection class implementing IList<T> that creates expensive objects on the fly. It takes 500 msec to create each object. You can see from the startup time how the caller is greedily enumerating all the items in the collection, rather than stopping after the first fiew.

Expected behavior
My expectation is that the IEnumerable<T>.MoveNext() method should only be called 2 or 3 times (in the code sample provided), rather than the full 20.

Desktop (please complete the following information):

  • OS: Windows 10
  • Version [e.g. 0.10.10
@uxmal uxmal added the bug label Nov 10, 2021
uxmal added a commit to uxmal/avalonia-6904 that referenced this issue Nov 10, 2021
@maxkatz6
Copy link
Member

maxkatz6 commented Nov 10, 2021

My expectation is that the IEnumerable.MoveNext() method should only be called 2 or 3 times

Virtualization of ItemsRepeater means that only 2-3 children controls will be rendered instead of 20 at a time. It does not guarantees "only be called 2 or 3 times". Moreover ItemsRepeater can't work with IEnumerable efficiently, because it needs materialized list of objects (so generic IList<T> doesn't work). Related source file is here if you are interested https://github.com/AvaloniaUI/Avalonia/blob/master/src/Avalonia.Controls/ItemsSourceView.cs

From source code you can see that your collection must implement IList (and not IList<T>), otherwise it will needs to be boxed into List by items repeater iterating the whole list. And GetEnumerator should never be called. I cloned your project, implemented "IList" interface (without generic) and it works now as you initially expected.

It takes 500 msec to create each object

IList is definitely not expected to work in this way, when each indexer call or enumeration creates new item without any caching. Usually all the data is predefined.
And I don't know what's your task to solve in this strange way, but even if everything worked as you expected, 500msec per item is too much. Sync operations on the UI thread shouldn't take more than ~20-30ms, otherwise there will be noticeable UI lag.

If you want to achieve lazy loading of items, you need to use handle scrolling events, do your async operation (which can take 500ms or more but without UI block) and update source list with new items. Note that source list much be of ObservableCollection type of any other that implements INotifyPropertyChanged (and IList of course).

@maxkatz6 maxkatz6 added by-design The behavior reported in the issue is actually correct. and removed bug labels Nov 10, 2021
@timunie
Copy link
Contributor

timunie commented Nov 10, 2021

Hi @uxmal

I have found the it may be useful to use async Task<MyContent> and bind to it via ^. (see: https://docs.avaloniaui.net/docs/data-binding/binding-to-tasks-and-observables) example:

// In your ViewModel: 

// The property to bind to: 
public Task<int> AsyncContent => GetContentAsync();

// the async operation 
private async Task<int> GetContentAsync()
{
    await Task.Delay(500); // Simulate long running operation
    return MyIndex;
}

and in your xaml:

<TextBlock Text={Binding AsyncContent^} />

for an example please see:
uxmal/avalonia-6904#1

Happy coding
Tim


@maxkatz6 ► Should I provide a PR to the docs with an sample how to bind to tasks?

@maxkatz6
Copy link
Member

@timunie we should have docs for "^" bindinds operator, don't we? If not, or if there is no any example of usage, if would be useful, thanks.

@FoggyFinder
Copy link
Contributor

@uxmal Ui virtualization and data virtualization are totally different things

@timunie
Copy link
Contributor

timunie commented Nov 11, 2021

Send a PR to the docs: https://github.com/AvaloniaUI/Documentation/pull/157

@maxkatz6 maxkatz6 closed this as not planned Won't fix, can't repro, duplicate, stale May 28, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
by-design The behavior reported in the issue is actually correct.
Projects
None yet
Development

No branches or pull requests

4 participants