Improved the behavior of the async virtual collection

This commit is contained in:
Markus Ewald 2025-07-09 22:05:46 +02:00
parent 1d955c1506
commit 059c093ec3
2 changed files with 214 additions and 158 deletions

View File

@ -23,6 +23,8 @@ using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Threading;
#if !NO_SPECIALIZED_COLLECTIONS #if !NO_SPECIALIZED_COLLECTIONS
using System.Collections.Specialized; using System.Collections.Specialized;
@ -63,7 +65,7 @@ namespace Nuclex.Avalonia.Collections {
/// <summary>Immediately releases all resources owned by the instance</summary> /// <summary>Immediately releases all resources owned by the instance</summary>
public void Dispose() { public void Dispose() {
this.virtualList = null!; // Only to make life easier got the GC this.virtualList = null!; // Only to decouple and make the GC's work easier
} }
/// <summary>The item at the enumerator's current position</summary> /// <summary>The item at the enumerator's current position</summary>
@ -73,6 +75,8 @@ namespace Nuclex.Avalonia.Collections {
checkVersion(); checkVersion();
#endif #endif
// If the most recent call to MoveNext() returned false, it means that
// the enumerator has reached the end of the list, so in that
if(this.lastMoveNextResult == false) { if(this.lastMoveNextResult == false) {
throw new InvalidOperationException("Enumerator is not on a valid position"); throw new InvalidOperationException("Enumerator is not on a valid position");
} }
@ -122,8 +126,8 @@ namespace Nuclex.Avalonia.Collections {
} }
/// <summary>The item at the enumerator's current position</summary> /// <summary>The item at the enumerator's current position</summary>
object IEnumerator.Current { object? IEnumerator.Current {
get { return Current!; } // No idea what the compiler's issue is here get { return Current; }
} }
#if DEBUG #if DEBUG
@ -149,9 +153,9 @@ namespace Nuclex.Avalonia.Collections {
#endregion // class Enumerator #endregion // class Enumerator
/// <summary>Raised when an item has been added to the collection</summary> /// <summary>Raised when an item has been added to the collection</summary>
public event EventHandler<ItemEventArgs<TItem>>? ItemAdded; public event EventHandler<ItemEventArgs<TItem>>? ItemAdded { add {} remove{} }
/// <summary>Raised when an item is removed from the collection</summary> /// <summary>Raised when an item is removed from the collection</summary>
public event EventHandler<ItemEventArgs<TItem>>? ItemRemoved; public event EventHandler<ItemEventArgs<TItem>>? ItemRemoved { add {} remove{} }
/// <summary>Raised when an item is replaced in the collection</summary> /// <summary>Raised when an item is replaced in the collection</summary>
public event EventHandler<ItemReplaceEventArgs<TItem>>? ItemReplaced; public event EventHandler<ItemReplaceEventArgs<TItem>>? ItemReplaced;
/// <summary>Raised when the collection is about to be cleared</summary> /// <summary>Raised when the collection is about to be cleared</summary>
@ -160,9 +164,9 @@ namespace Nuclex.Avalonia.Collections {
/// contained in the collection, but it is often simpler and more efficient /// contained in the collection, but it is often simpler and more efficient
/// to process the clearing of the entire collection as a special operation. /// to process the clearing of the entire collection as a special operation.
/// </remarks> /// </remarks>
public event EventHandler? Clearing; public event EventHandler? Clearing { add {} remove{} }
/// <summary>Raised when the collection has been cleared</summary> /// <summary>Raised when the collection has been cleared</summary>
public event EventHandler? Cleared; public event EventHandler? Cleared { add {} remove{} }
#if !NO_SPECIALIZED_COLLECTIONS #if !NO_SPECIALIZED_COLLECTIONS
/// <summary>Called when the collection has changed</summary> /// <summary>Called when the collection has changed</summary>
@ -182,7 +186,7 @@ namespace Nuclex.Avalonia.Collections {
/// performance from requesting multiple items at once. /// performance from requesting multiple items at once.
/// </remarks> /// </remarks>
public AsyncVirtualObservableReadOnlyList(int pageSize = 32) { public AsyncVirtualObservableReadOnlyList(int pageSize = 32) {
this.typedList = new TItem[0]; this.typedList = new List<TItem>();
this.objectList = (IList)this.typedList; this.objectList = (IList)this.typedList;
this.pageSize = pageSize; this.pageSize = pageSize;
this.fetchedPages = new bool[0]; this.fetchedPages = new bool[0];
@ -197,18 +201,51 @@ namespace Nuclex.Avalonia.Collections {
/// will make them available for garbage collection. /// will make them available for garbage collection.
/// </param> /// </param>
public void InvalidateAll(bool purgeItems = false) { public void InvalidateAll(bool purgeItems = false) {
if(this.assumedCount.HasValue) { // If not fetched before, no action needed int itemCount;
int pageCount = this.fetchedPages.Length; lock(this) {
for(int index = 0; index < pageCount; ++index) { if(!this.assumedCount.HasValue) {
this.fetchedPages[index] = false; return; // If not fetched before, no action is needed
} }
if(purgeItems) { itemCount = this.assumedCount.Value;
int itemCount = this.assumedCount.Value; }
for(int index = 0; index < itemCount; ++index) {
this.typedList[index] = default(TItem)!; // not going to be exposed to users // Mark the pages as un-fetched
int pageCount = this.fetchedPages.Length;
if(purgeItems) {
var oldItemList = new List<TItem>(capacity: this.pageSize);
for(int pageIndex = 0; pageIndex < pageCount; ++pageIndex) {
if(this.fetchedPages[pageIndex]) {
this.fetchedPages[pageIndex] = false;
// Figure out the page's item start index and how big the page is
int offset = pageIndex * this.pageSize;
int count = Math.Min(itemCount - offset, this.pageSize);
// Make a backup of the old item values so we can provide them
// in the ItemReplaced change notification we'll send out.
for(int index = 0; index < count; ++index) {
oldItemList[index] = this.typedList[offset + index];
}
// Replace the loaded items with placeholder items
CreatePlaceholderItems(this.typedList, pageIndex * this.pageSize, count);
// Send out change notifications for all items we just replace with placeholders
for(int index = 0; index < count; ++index) {
OnReplaced(oldItemList[index], this.typedList[offset + index], offset + index);
}
// Make sure the old items aren't lingering around
oldItemList.Clear();
} }
} }
} else {
for(int pageIndex = 0; pageIndex < pageCount; ++pageIndex) {
this.fetchedPages[pageIndex] = false;
}
} }
} }
@ -229,39 +266,30 @@ namespace Nuclex.Avalonia.Collections {
/// when any of them are next accessed. /// when any of them are next accessed.
/// </remarks> /// </remarks>
public void Invalidate(int itemIndex, bool purgeItems = false) { public void Invalidate(int itemIndex, bool purgeItems = false) {
if(this.assumedCount.HasValue) { // If not fetched before, no action needed lock(this) {
int pageIndex = itemIndex / this.pageSize; if(!this.assumedCount.HasValue) {
this.fetchedPages[pageIndex] = false; return; // If not fetched before, no action is needed
if(purgeItems) {
int count = Math.Min(
this.assumedCount.Value - (this.pageSize * pageIndex),
this.pageSize
);
for(int index = itemIndex / this.pageSize; index < count; ++index) {
this.typedList[index] = default(TItem)!; // not going to be exposed to users
}
} }
} }
// Mark the entie page as needing re-fetching (we've got no other choice)
int pageIndex = itemIndex / this.pageSize;
this.fetchedPages[pageIndex] = false;
// If we're asked to also purge the item, replace the item with
// a placeholder item and trigger the change notification
if(purgeItems) {
TItem oldItem = this.typedList[itemIndex];
CreatePlaceholderItems(this.typedList, itemIndex, 1);
OnReplaced(oldItem, this.typedList[itemIndex], itemIndex);
}
} }
/// <summary>Determines the index of the specified item in the list</summary> /// <summary>Determines the index of the specified item in the list</summary>
/// <param name="item">Item whose index will be determined</param> /// <param name="item">Item whose index will be determined</param>
/// <returns>The index of the item in the list or -1 if not found</returns> /// <returns>The index of the item in the list or -1 if not found</returns>
public int IndexOf(TItem item) { public int IndexOf(TItem item) {
requireCount(); return this.typedList.IndexOf(item);
requireAllPages();
// TODO: this won't work, it will compare the placeholder items :-/
IComparer<TItem> itemComparer = Comparer<TItem>.Default;
for(int index = 0; index < this.assumedCount.Value; ++index) {
if(itemComparer.Compare(this.typedList[index], item) == 0) {
return index;
}
}
return -1;
} }
/// <summary>Inserts an item into the list at the specified index</summary> /// <summary>Inserts an item into the list at the specified index</summary>
@ -316,7 +344,10 @@ namespace Nuclex.Avalonia.Collections {
/// <param name="item">Item the list will be checked for</param> /// <param name="item">Item the list will be checked for</param>
/// <returns>True if the list contains the specified items</returns> /// <returns>True if the list contains the specified items</returns>
public bool Contains(TItem item) { public bool Contains(TItem item) {
return (IndexOf(item) != -1); requireCount();
requireAllPages();
return this.typedList.Contains(item);
} }
/// <summary>Copies the contents of the list into an array</summary> /// <summary>Copies the contents of the list into an array</summary>
@ -334,14 +365,16 @@ namespace Nuclex.Avalonia.Collections {
/// <summary>Total number of items in the list</summary> /// <summary>Total number of items in the list</summary>
public int Count { public int Count {
get { get {
requireCount(); return requireCount();
return this.assumedCount.Value;
} }
} }
/// <summary>Whether the list is a read-only list</summary> /// <summary>Whether the list is a read-only list</summary>
public bool IsReadOnly { public bool IsReadOnly {
get { return this.typedList.IsReadOnly; } get {
//return this.typedList.IsReadOnly;
return true;
}
} }
/// <summary>Removes the specified item from the list</summary> /// <summary>Removes the specified item from the list</summary>
@ -402,14 +435,14 @@ namespace Nuclex.Avalonia.Collections {
/// <returns> /// <returns>
/// The position at which the item has been inserted or -1 if the item was not inserted /// The position at which the item has been inserted or -1 if the item was not inserted
/// </returns> /// </returns>
int IList.Add(object value) { int IList.Add(object? value) {
throw new NotSupportedException("Cannot add items into a read-only list"); throw new NotSupportedException("Cannot add items into a read-only list");
} }
/// <summary>Checks whether the list contains the specified item</summary> /// <summary>Checks whether the list contains the specified item</summary>
/// <param name="item">Item the list will be checked for</param> /// <param name="item">Item the list will be checked for</param>
/// <returns>True if the list contains the specified items</returns> /// <returns>True if the list contains the specified items</returns>
bool IList.Contains(object item) { bool IList.Contains(object? item) {
requireCount(); requireCount();
requireAllPages(); requireAllPages();
return this.objectList.Contains(item); return this.objectList.Contains(item);
@ -418,7 +451,7 @@ namespace Nuclex.Avalonia.Collections {
/// <summary>Determines the index of the specified item in the list</summary> /// <summary>Determines the index of the specified item in the list</summary>
/// <param name="item">Item whose index will be determined</param> /// <param name="item">Item whose index will be determined</param>
/// <returns>The index of the item in the list or -1 if not found</returns> /// <returns>The index of the item in the list or -1 if not found</returns>
int IList.IndexOf(object item) { int IList.IndexOf(object? item) {
requireCount(); requireCount();
requireAllPages(); requireAllPages();
return this.objectList.IndexOf(item); return this.objectList.IndexOf(item);
@ -427,7 +460,7 @@ namespace Nuclex.Avalonia.Collections {
/// <summary>Inserts an item into the list at the specified index</summary> /// <summary>Inserts an item into the list at the specified index</summary>
/// <param name="index">Index the item will be inserted at</param> /// <param name="index">Index the item will be inserted at</param>
/// <param name="item">Item that will be inserted into the list</param> /// <param name="item">Item that will be inserted into the list</param>
void IList.Insert(int index, object item) { void IList.Insert(int index, object? item) {
throw new NotSupportedException("Cannot insert items into a read-only list"); throw new NotSupportedException("Cannot insert items into a read-only list");
} }
@ -438,14 +471,14 @@ namespace Nuclex.Avalonia.Collections {
/// <summary>Removes the specified item from the list</summary> /// <summary>Removes the specified item from the list</summary>
/// <param name="item">Item that will be removed from the list</param> /// <param name="item">Item that will be removed from the list</param>
void IList.Remove(object item) { void IList.Remove(object? item) {
throw new NotSupportedException("Cannot remove items from a read-only list"); throw new NotSupportedException("Cannot remove items from a read-only list");
} }
/// <summary>Accesses the item at the specified index in the list</summary> /// <summary>Accesses the item at the specified index in the list</summary>
/// <param name="index">Index of the item that will be accessed</param> /// <param name="index">Index of the item that will be accessed</param>
/// <returns>The item at the specified index</returns> /// <returns>The item at the specified index</returns>
object IList.this[int index] { object? IList.this[int index] {
get { get {
requireCount(); requireCount();
requirePage(index / this.pageSize); requirePage(index / this.pageSize);
@ -453,56 +486,12 @@ namespace Nuclex.Avalonia.Collections {
return this.objectList[index]; return this.objectList[index];
} }
set { set {
// Make sure the page is fetched, otherwise, the item would only suddenly throw new NotSupportedException("Cannot assign items into a read-only list");
// revert to its state in the source when the pages around it is fetchd later.
requireCount();
requirePage(index / this.pageSize);
#if DEBUG
++this.version;
#endif
TItem oldItem = this.typedList[index];
this.objectList[index] = value;
TItem newItem = this.typedList[index];
OnReplaced(oldItem, newItem, index);
} }
} }
#endregion // IList implementation #endregion // IList implementation
/// <summary>Fires the 'ItemAdded' event</summary>
/// <param name="item">Item that has been added to the collection</param>
/// <param name="index">Index of the added item</param>
protected virtual void OnAdded(TItem item, int index) {
if(ItemAdded != null) {
ItemAdded(this, new ItemEventArgs<TItem>(item));
}
#if !NO_SPECIALIZED_COLLECTIONS
if(CollectionChanged != null) {
CollectionChanged(
this,
new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, index)
);
}
#endif
}
/// <summary>Fires the 'ItemRemoved' event</summary>
/// <param name="item">Item that has been removed from the collection</param>
/// <param name="index">Index the item has been removed from</param>
protected virtual void OnRemoved(TItem item, int index) {
if(ItemRemoved != null) {
ItemRemoved(this, new ItemEventArgs<TItem>(item));
}
#if !NO_SPECIALIZED_COLLECTIONS
if(CollectionChanged != null) {
CollectionChanged(
this,
new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, index)
);
}
#endif
}
/// <summary>Fires the 'ItemReplaced' event</summary> /// <summary>Fires the 'ItemReplaced' event</summary>
/// <param name="oldItem">Item that has been replaced</param> /// <param name="oldItem">Item that has been replaced</param>
/// <param name="newItem">New item the original item was replaced with</param> /// <param name="newItem">New item the original item was replaced with</param>
@ -523,25 +512,6 @@ namespace Nuclex.Avalonia.Collections {
#endif #endif
} }
/// <summary>Fires the 'Clearing' event</summary>
protected virtual void OnClearing() {
if(Clearing != null) {
Clearing(this, EventArgs.Empty);
}
}
/// <summary>Fires the 'Cleared' event</summary>
protected virtual void OnCleared() {
if(Cleared != null) {
Cleared(this, EventArgs.Empty);
}
#if !NO_SPECIALIZED_COLLECTIONS
if(CollectionChanged != null) {
CollectionChanged(this, Constants.NotifyCollectionResetEventArgs);
}
#endif
}
/// <summary>Counts the total number of items in the virtual collection</summary> /// <summary>Counts the total number of items in the virtual collection</summary>
/// <returns>The total number of items</returns> /// <returns>The total number of items</returns>
protected abstract int CountItems(); protected abstract int CountItems();
@ -583,16 +553,58 @@ namespace Nuclex.Avalonia.Collections {
IList<TItem> target, int startIndex, int count IList<TItem> target, int startIndex, int count
); );
/// <summary>Reports errors when fetching data</summary>
/// <param name="error">Exception that has occured</param>
/// <param name="action">Describes the action during which the error happened</param>
protected abstract void HandleFetchError(Exception error, string action);
/// <summary>Ensures that the total number of items is known</summary> /// <summary>Ensures that the total number of items is known</summary>
/// <returns>The item count</returns>
[MemberNotNull(nameof(assumedCount))] [MemberNotNull(nameof(assumedCount))]
private void requireCount() { private int requireCount() {
if(!this.assumedCount.HasValue) { lock(this) {
int itemCount = CountItems(); if(this.assumedCount.HasValue) {
return this.assumedCount.Value;
}
}
int itemCount;
try {
itemCount = CountItems();
}
catch(Exception error) {
lock(this) {
this.assumedCount = 0; // Act as if the collection has zero items
}
// Do not let the error bubble up into what is likely an unsuspecting
// data grid control or virtualized list widget. Let the user decide what
// to do about the error - i.e. set an error flag in their view model.
HandleFetchError(
error, "Failed to determine the number of items in the collection"
);
return 0;
} // try CountItems() catch
// Items were counted successfully, so adopt the count and initialize our
// internal arrays to avoid having to resize the arrays later on (which
// saves us a few lock statements)
lock(this) {
this.assumedCount = itemCount; this.assumedCount = itemCount;
int pageCount = (itemCount + this.pageSize - 1) / this.pageSize; int pageCount = (itemCount + this.pageSize - 1) / this.pageSize;
this.fetchedPages = new bool[pageCount]; this.fetchedPages = new bool[pageCount];
}
// Resize the list (so we don't have to mutex-lock the list itself),
// but put default items in there. We'll create placeholders when they
// are accessed, not before (ideally, I'd want uninitialized instances
// here, but that would need very thorough verification
while(this.typedList.Count < itemCount) {
this.typedList.Add(default(TItem)!);
}
} // lock this
return itemCount;
} }
/// <summary>Ensures that all items have fetched</summary> /// <summary>Ensures that all items have fetched</summary>
@ -600,55 +612,96 @@ namespace Nuclex.Avalonia.Collections {
/// Avoid if possible. /// Avoid if possible.
/// </remarks> /// </remarks>
private void requireAllPages() { private void requireAllPages() {
Debug.Assert( lock(this) {
this.assumedCount.HasValue, Debug.Assert(
"This method should only be called when item count is already known" this.assumedCount.HasValue,
); "This method should only be called when item count is already known"
);
}
// We may find that the list is shorter than expected while requesting pages. int pageCount = this.fetchedPages.Length;
// But the results will come in asynchronously, so we can't wait for it.
int pageCount = (this.assumedCount!.Value + this.pageSize - 1) / this.pageSize;
for(int index = 0; index < pageCount; ++index) { for(int index = 0; index < pageCount; ++index) {
requirePage(index); requirePage(index);
// CHECK: Should we throttle this by constructing a clever chain of
// ContinueWith() tasks so we don't cause a flood of async requests?
} }
} }
/// <summary>Ensures that the specified page has been fetched</summary> /// <summary>Ensures that the specified page has been fetched</summary>
/// <param name="pageIndex">Index of the page that needs to be fetched</param> /// <param name="pageIndex">Index of the page that needs to be fetched</param>
private async void requirePage(int pageIndex) { private async void requirePage(int pageIndex) {
Debug.Assert( int itemCount;
this.assumedCount.HasValue, lock(this) {
"This method should only be called when item count is already known" if(!this.assumedCount.HasValue) {
); Debug.Assert(
if(this.fetchedPages[pageIndex]) { this.assumedCount.HasValue,
"This method should only be called when item count is already known"
);
return;
}
itemCount = this.assumedCount.Value;
}
// If the page is already fetched (or in flight), do nothing
if((pageIndex >= this.fetchedPages.Length) || this.fetchedPages[pageIndex]) {
return; return;
} }
int startIndex = pageIndex * this.pageSize; // The page was not fetched, so synchronously fill it with place holder items
int count = Math.Min(this.assumedCount!.Value - startIndex, this.pageSize); // as a first step (their creation should be fast and immediate).
CreatePlaceholderItems(this.typedList, pageIndex * this.pageSize, count); int offset = pageIndex * this.pageSize;
int count = Math.Min(itemCount - offset, this.pageSize);
CreatePlaceholderItems(this.typedList, offset, count);
// Ensure the items are present before marking the page as fetched
Interlocked.MemoryBarrier();
this.fetchedPages[pageIndex] = true; // Prevent double-fetch this.fetchedPages[pageIndex] = true; // Prevent double-fetch
// Send out change notifications that the items have been replaced // Remember the placeholder items that are going to be replaced by
// (at this point, they're only placeholder items, of course) // the fetch operation below,allowing us to report these in our change notification
var placeholderItems = new List<TItem>(count); var placeholderItems = new List<TItem>(capacity: count);
for(int index = startIndex; index < count; ++index) { for(int index = offset; index < count; ++index) {
placeholderItems[index - startIndex] = this.typedList[index]; placeholderItems.Add(this.typedList[index + offset]);
//OnReplaced(default(TItem), this.typedList[index], index);
// We act as if the whole list was filled with place holders from the start,
// but only realize these as needed (so no change notification for these).
// If we did it right, the user will not be able to ever see the uninitialized
// items, yet we only create placeholders when they are really needed.
//OnReplaced(default(TItem), this.typedList[index + offset], index);
} }
int fetchedItemCount = await FetchItemsAsync(this.typedList, startIndex, count); // Now request the items on the page. This request will run asynchronously,
// but we'll use an await to get to deliver change notifications once
// the page has been fetched and the items are there.
int fetchedItemCount;
try {
fetchedItemCount = await FetchItemsAsync(this.typedList, offset, count);
}
catch(Exception error) {
// Do not let the error bubble up into what is likely an unsuspecting
// data grid control or virtualized list widget. Let the user decide what
// to do about the error - i.e. set an error flag in their view model.
HandleFetchError(
error, $"Failed to fetch list items {offset} to {offset + count}"
);
this.fetchedPages[pageIndex] = false; // We failed!
return; // Leave the placeholder items in
}
if(fetchedItemCount < this.pageSize) { if(fetchedItemCount < this.pageSize) {
this.assumedCount = startIndex + fetchedItemCount; itemCount = offset + fetchedItemCount;
lock(this) {
this.assumedCount = itemCount;
}
} }
// The count may have been adjusted if this truncated the list, // The count may have been adjusted if this truncated the list,
// so recalculate the actual number of items. Then send out change // so recalculate the actual number of items. Then send out change
// notifications for the items that have now been fetched. // notifications for the items that have now been fetched.
count = Math.Min(this.assumedCount!.Value - startIndex, this.pageSize); count = Math.Min(itemCount - offset, this.pageSize);
for(int index = startIndex; index < count; ++index) { for(int index = offset; index < count; ++index) {
OnReplaced(placeholderItems[index - startIndex], this.typedList[index], index); OnReplaced(placeholderItems[index - offset], this.typedList[index], index);
} }
} }
@ -659,7 +712,7 @@ namespace Nuclex.Avalonia.Collections {
/// <summary>Tracks which pages have been fetched so far</summary> /// <summary>Tracks which pages have been fetched so far</summary>
private bool[] fetchedPages; private bool[] fetchedPages;
/// <summary>The wrapped list under its type-safe interface</summary> /// <summary>The wrapped list under its type-safe interface</summary>
private TItem[] typedList; private IList<TItem> typedList;
/// <summary>The wrapped list under its object interface</summary> /// <summary>The wrapped list under its object interface</summary>
private IList objectList; private IList objectList;
#if DEBUG #if DEBUG

View File

@ -23,7 +23,6 @@ using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
#if !NO_SPECIALIZED_COLLECTIONS #if !NO_SPECIALIZED_COLLECTIONS
using System.Collections.Specialized; using System.Collections.Specialized;
#endif #endif
@ -597,19 +596,23 @@ namespace Nuclex.Avalonia.Collections {
this.assumedCount.HasValue, this.assumedCount.HasValue,
"This method should only be called when item count is already known" "This method should only be called when item count is already known"
); );
if(!this.fetchedPages[pageIndex]) {
int count = Math.Min(
this.assumedCount!.Value - (this.pageSize * pageIndex),
this.pageSize
);
int fetchedItemCount = FetchItems(this.typedList, pageIndex * this.pageSize, count); // If the page is already fetched (or in flight), do nothing
if(fetchedItemCount < this.pageSize) { if((pageIndex >= this.fetchedPages.Length) || this.fetchedPages[pageIndex]) {
this.assumedCount = pageIndex * this.pageSize + fetchedItemCount; return;
}
this.fetchedPages[pageIndex] = true;
} }
int count = Math.Min(
this.assumedCount!.Value - (this.pageSize * pageIndex),
this.pageSize
);
int fetchedItemCount = FetchItems(this.typedList, pageIndex * this.pageSize, count);
if(fetchedItemCount < this.pageSize) {
this.assumedCount = pageIndex * this.pageSize + fetchedItemCount;
}
this.fetchedPages[pageIndex] = true;
} }
/// <summary>Number of items the collection believes it has</summary> /// <summary>Number of items the collection believes it has</summary>