Removed virtual collections from this project, I'll add them to the Avalonia project (WinForms can't use them and they're not generic enough for Nuclex.Support)
This commit is contained in:
parent
ac605bf7c3
commit
81f9a0acb4
@ -1,656 +0,0 @@
|
||||
#region Apache License 2.0
|
||||
/*
|
||||
Nuclex .NET Framework
|
||||
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
#endregion // Apache License 2.0
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
||||
#if !NO_SPECIALIZED_COLLECTIONS
|
||||
using System.Collections.Specialized;
|
||||
using System.Diagnostics;
|
||||
#endif
|
||||
|
||||
namespace Nuclex.Support.Collections {
|
||||
|
||||
/// <summary>
|
||||
/// List which fires events when items are added or removed, whilst also
|
||||
/// lazily fetching items as needed (for example from a socket or database)
|
||||
/// </summary>
|
||||
/// <typeparam name="TItem">Type of items the list manages</typeparam>
|
||||
public abstract class AsyncVirtualObservableReadOnlyList<TItem> :
|
||||
IList<TItem>,
|
||||
IList,
|
||||
ICollection,
|
||||
#if !NO_SPECIALIZED_COLLECTIONS
|
||||
INotifyCollectionChanged,
|
||||
#endif
|
||||
IObservableCollection<TItem> {
|
||||
|
||||
#region class Enumerator
|
||||
|
||||
/// <summary>Enumerates over the items in a virtual list</summary>
|
||||
private class Enumerator : IEnumerator<TItem>, IEnumerator {
|
||||
|
||||
/// <summary>Initializes a new virtual list enumerator</summary>
|
||||
/// <param name="virtualList">List whose items will be enumerated</param>
|
||||
public Enumerator(AsyncVirtualObservableReadOnlyList<TItem> virtualList) {
|
||||
this.virtualList = virtualList;
|
||||
this.currentItemIndex = -1;
|
||||
this.lastMoveNextResult = false;
|
||||
|
||||
Reset();
|
||||
}
|
||||
|
||||
/// <summary>Immediately releases all resources owned by the instance</summary>
|
||||
public void Dispose() {
|
||||
this.virtualList = null;
|
||||
}
|
||||
|
||||
/// <summary>The item at the enumerator's current position</summary>
|
||||
public TItem Current {
|
||||
get {
|
||||
#if DEBUG
|
||||
checkVersion();
|
||||
#endif
|
||||
|
||||
if(this.lastMoveNextResult == false) {
|
||||
throw new InvalidOperationException("Enumerator is not on a valid position");
|
||||
}
|
||||
|
||||
this.virtualList.requireCount();
|
||||
this.virtualList.requirePage(this.currentItemIndex / this.virtualList.pageSize);
|
||||
|
||||
return this.virtualList.typedList[this.currentItemIndex];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Advances the enumerator to the next item</summary>
|
||||
/// <returns>True if there was a next item</returns>
|
||||
public bool MoveNext() {
|
||||
#if DEBUG
|
||||
checkVersion();
|
||||
#endif
|
||||
|
||||
this.virtualList.requireCount();
|
||||
|
||||
// Go forward if there potentially are items remaining. The count may still be
|
||||
// unreliable at this point (due to the uncertain count mechanism that truncates
|
||||
// the list when fetching items finds an earlier end of the list)
|
||||
if(this.currentItemIndex < this.virtualList.assumedCount.Value) {
|
||||
|
||||
// If the enumerator's 'Current' property is never used an the virtual list
|
||||
// uses the dynamic truncation features (for unknown list sizes), then this
|
||||
// enumerator could be moved way past the last element via 'MoveNext()'.
|
||||
this.virtualList.requirePage(this.currentItemIndex / this.virtualList.pageSize);
|
||||
++this.currentItemIndex; // Accept potentially advancing past the end here
|
||||
|
||||
}
|
||||
|
||||
// Are we on a valid item? If so, return true to indicate the list continued,
|
||||
// otherwise we must have hit the end (or are already past it).
|
||||
return this.lastMoveNextResult = (
|
||||
(this.currentItemIndex < this.virtualList.assumedCount.Value)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>Resets the enumerator to its initial position</summary>
|
||||
public void Reset() {
|
||||
this.currentItemIndex = -1;
|
||||
#if DEBUG
|
||||
this.expectedVersion = this.virtualList.version;
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>The item at the enumerator's current position</summary>
|
||||
object IEnumerator.Current {
|
||||
get { return Current; }
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
/// <summary>Ensures that the virtual list has not changed</summary>
|
||||
private void checkVersion() {
|
||||
if(this.expectedVersion != this.virtualList.version)
|
||||
throw new InvalidOperationException("Virtual list has been modified");
|
||||
}
|
||||
#endif
|
||||
|
||||
/// <summary>Virtual list the enumerator belongs to</summary>
|
||||
private AsyncVirtualObservableReadOnlyList<TItem> virtualList;
|
||||
/// <summary>Index of the item the enumerator currently is in</summary>
|
||||
private int currentItemIndex;
|
||||
/// <summary>The most recent result returned from MoveNext()</summary>
|
||||
private bool lastMoveNextResult;
|
||||
#if DEBUG
|
||||
/// <summary>Version the virtual list is expected to have</summary>
|
||||
private int expectedVersion;
|
||||
#endif
|
||||
}
|
||||
|
||||
#endregion // class Enumerator
|
||||
|
||||
/// <summary>Raised when an item has been added to the collection</summary>
|
||||
public event EventHandler<ItemEventArgs<TItem>> ItemAdded;
|
||||
/// <summary>Raised when an item is removed from the collection</summary>
|
||||
public event EventHandler<ItemEventArgs<TItem>> ItemRemoved;
|
||||
/// <summary>Raised when an item is replaced in the collection</summary>
|
||||
public event EventHandler<ItemReplaceEventArgs<TItem>> ItemReplaced;
|
||||
/// <summary>Raised when the collection is about to be cleared</summary>
|
||||
/// <remarks>
|
||||
/// This could be covered by calling ItemRemoved for each item currently
|
||||
/// contained in the collection, but it is often simpler and more efficient
|
||||
/// to process the clearing of the entire collection as a special operation.
|
||||
/// </remarks>
|
||||
public event EventHandler Clearing;
|
||||
/// <summary>Raised when the collection has been cleared</summary>
|
||||
public event EventHandler Cleared;
|
||||
|
||||
#if !NO_SPECIALIZED_COLLECTIONS
|
||||
/// <summary>Called when the collection has changed</summary>
|
||||
public event NotifyCollectionChangedEventHandler CollectionChanged;
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the ObservableList class that is empty.
|
||||
/// </summary>
|
||||
/// <param name="pageSize">
|
||||
/// How many items to download in one batch
|
||||
/// </param>
|
||||
/// <remarks>
|
||||
/// The <paramref name="pageSize" /> can be set to one to request items
|
||||
/// individually or to a larger value in order to improve efficiency when
|
||||
/// the source of the items is a database or similar source that gains
|
||||
/// performance from requesting multiple items at once.
|
||||
/// </remarks>
|
||||
public AsyncVirtualObservableReadOnlyList(int pageSize = 32) {
|
||||
this.typedList = new List<TItem>();
|
||||
this.objectList = this.typedList as IList;
|
||||
this.pageSize = pageSize;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks all items as non-fetched, causing them to be requested again
|
||||
/// </summary>
|
||||
/// <param name="purgeItems">
|
||||
/// Whether to also clear the items that may already be in memory but would
|
||||
/// get overwritten on the next fetch. If items consume a lot of memory, this
|
||||
/// will make them available for garbage collection.
|
||||
/// </param>
|
||||
public void InvalidateAll(bool purgeItems = false) {
|
||||
if(this.assumedCount.HasValue) { // If not fetched before, no action needed
|
||||
int pageCount = this.fetchedPages.Length;
|
||||
for(int index = 0; index < pageCount; ++index) {
|
||||
this.fetchedPages[index] = false;
|
||||
}
|
||||
|
||||
if(purgeItems) {
|
||||
int itemCount = this.assumedCount.Value;
|
||||
for(int index = 0; index < itemCount; ++index) {
|
||||
this.typedList[index] = default(TItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks an items as non-fetched, causing it to be requested again on access
|
||||
/// </summary>
|
||||
/// <param name="itemIndex">
|
||||
/// Index of the item that will be marked as non-fetched
|
||||
/// </param>
|
||||
/// <param name="purgeItems">
|
||||
/// Whether to also clear the items that may already be in memory but would
|
||||
/// get overwritten on the next fetch. If items consume a lot of memory, this
|
||||
/// will make them available for garbage collection.
|
||||
/// </param>
|
||||
/// <remarks>
|
||||
/// Since the list works in pages, this will actually mark the whole page as
|
||||
/// non-fetched, causing all items in the same page to be requested again
|
||||
/// when any of them are next accessed.
|
||||
/// </remarks>
|
||||
public void Invalidate(int itemIndex, bool purgeItems = false) {
|
||||
if(this.assumedCount.HasValue) { // If not fetched before, no action needed
|
||||
int pageIndex = itemIndex / this.pageSize;
|
||||
this.fetchedPages[pageIndex] = false;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Determines the index of the specified item in the list</summary>
|
||||
/// <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>
|
||||
public int IndexOf(TItem item) {
|
||||
return this.typedList.IndexOf(item);
|
||||
}
|
||||
|
||||
/// <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="item">Item that will be inserted into the list</param>
|
||||
public void Insert(int index, TItem item) {
|
||||
throw new NotSupportedException("Cannot insert items into a read-only list");
|
||||
}
|
||||
|
||||
/// <summary>Removes the item at the specified index from the list</summary>
|
||||
/// <param name="index">Index at which the item will be removed</param>
|
||||
public void RemoveAt(int index) {
|
||||
throw new NotSupportedException("Cannot remove items from a read-only list");
|
||||
}
|
||||
|
||||
/// <summary>Accesses the item at the specified index in the list</summary>
|
||||
/// <param name="index">Index of the item that will be accessed</param>
|
||||
/// <returns>The item at the specified index</returns>
|
||||
public TItem this[int index] {
|
||||
get {
|
||||
requireCount();
|
||||
requirePage(index / this.pageSize);
|
||||
|
||||
return this.typedList[index];
|
||||
}
|
||||
set {
|
||||
// Make sure the page is fetched, otherwise, the item would only suddenly
|
||||
// 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.typedList[index] = value;
|
||||
OnReplaced(oldItem, value, index);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Adds an item to the end of the list</summary>
|
||||
/// <param name="item">Item that will be added to the list</param>
|
||||
public void Add(TItem item) {
|
||||
throw new NotSupportedException("Cannot add items to a read-only list");
|
||||
}
|
||||
|
||||
/// <summary>Removes all items from the list</summary>
|
||||
public void Clear() {
|
||||
throw new NotSupportedException("Cannot clear a read-only list");
|
||||
}
|
||||
|
||||
/// <summary>Checks whether the list contains the specified item</summary>
|
||||
/// <param name="item">Item the list will be checked for</param>
|
||||
/// <returns>True if the list contains the specified items</returns>
|
||||
public bool Contains(TItem item) {
|
||||
requireCount();
|
||||
requireAllPages();
|
||||
|
||||
return this.typedList.Contains(item);
|
||||
}
|
||||
|
||||
/// <summary>Copies the contents of the list into an array</summary>
|
||||
/// <param name="array">Array the list will be copied into</param>
|
||||
/// <param name="arrayIndex">
|
||||
/// Index in the target array where the first item will be copied to
|
||||
/// </param>
|
||||
public void CopyTo(TItem[] array, int arrayIndex) {
|
||||
requireCount();
|
||||
requireAllPages();
|
||||
|
||||
this.typedList.CopyTo(array, arrayIndex);
|
||||
}
|
||||
|
||||
/// <summary>Total number of items in the list</summary>
|
||||
public int Count {
|
||||
get {
|
||||
requireCount();
|
||||
return this.assumedCount.Value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Whether the list is a read-only list</summary>
|
||||
public bool IsReadOnly {
|
||||
get { return this.typedList.IsReadOnly; }
|
||||
}
|
||||
|
||||
/// <summary>Removes the specified item from the list</summary>
|
||||
/// <param name="item">Item that will be removed from the list</param>
|
||||
/// <returns>
|
||||
/// True if the item was found and removed from the list, false otherwise
|
||||
/// </returns>
|
||||
public bool Remove(TItem item) {
|
||||
throw new NotSupportedException("Cannot remove items from a read-only list");
|
||||
}
|
||||
|
||||
/// <summary>Returns an enumerator for the items in the list</summary>
|
||||
/// <returns>An enumerator for the list's items</returns>
|
||||
public IEnumerator<TItem> GetEnumerator() {
|
||||
return new Enumerator(this);
|
||||
}
|
||||
|
||||
#region IEnumerable implementation
|
||||
|
||||
/// <summary>Returns an enumerator for the items in the list</summary>
|
||||
/// <returns>An enumerator for the list's items</returns>
|
||||
IEnumerator IEnumerable.GetEnumerator() {
|
||||
return new Enumerator(this);
|
||||
}
|
||||
|
||||
#endregion // IEnumerable implementation
|
||||
|
||||
#region ICollection implementation
|
||||
|
||||
/// <summary>Copies the contents of the list into an array</summary>
|
||||
/// <param name="array">Array the list will be copied into</param>
|
||||
/// <param name="arrayIndex">
|
||||
/// Index in the target array where the first item will be copied to
|
||||
/// </param>
|
||||
void ICollection.CopyTo(Array array, int arrayIndex) {
|
||||
requireCount();
|
||||
requireAllPages();
|
||||
|
||||
this.objectList.CopyTo(array, arrayIndex);
|
||||
}
|
||||
|
||||
/// <summary>Whether this list performs thread synchronization</summary>
|
||||
bool ICollection.IsSynchronized {
|
||||
get { return this.objectList.IsSynchronized; }
|
||||
}
|
||||
|
||||
/// <summary>Synchronization root used by the list to synchronize threads</summary>
|
||||
object ICollection.SyncRoot {
|
||||
get { return this.objectList.SyncRoot; }
|
||||
}
|
||||
|
||||
#endregion // ICollection implementation
|
||||
|
||||
#region IList implementation
|
||||
|
||||
/// <summary>Adds an item to the list</summary>
|
||||
/// <param name="value">Item that will be added to the list</param>
|
||||
/// <returns>
|
||||
/// The position at which the item has been inserted or -1 if the item was not inserted
|
||||
/// </returns>
|
||||
int IList.Add(object value) {
|
||||
throw new NotSupportedException("Cannot add items into a read-only list");
|
||||
}
|
||||
|
||||
/// <summary>Checks whether the list contains the specified item</summary>
|
||||
/// <param name="item">Item the list will be checked for</param>
|
||||
/// <returns>True if the list contains the specified items</returns>
|
||||
bool IList.Contains(object item) {
|
||||
requireCount();
|
||||
requireAllPages();
|
||||
return this.objectList.Contains(item);
|
||||
}
|
||||
|
||||
/// <summary>Determines the index of the specified item in the list</summary>
|
||||
/// <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>
|
||||
int IList.IndexOf(object item) {
|
||||
requireCount();
|
||||
requireAllPages();
|
||||
return this.objectList.IndexOf(item);
|
||||
}
|
||||
|
||||
/// <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="item">Item that will be inserted into the list</param>
|
||||
void IList.Insert(int index, object item) {
|
||||
throw new NotSupportedException("Cannot insert items into a read-only list");
|
||||
}
|
||||
|
||||
/// <summary>Whether the list is of a fixed size</summary>
|
||||
bool IList.IsFixedSize {
|
||||
get { return this.objectList.IsFixedSize; }
|
||||
}
|
||||
|
||||
/// <summary>Removes the specified item from the list</summary>
|
||||
/// <param name="item">Item that will be removed from the list</param>
|
||||
void IList.Remove(object item) {
|
||||
throw new NotSupportedException("Cannot remove items from a read-only list");
|
||||
}
|
||||
|
||||
/// <summary>Accesses the item at the specified index in the list</summary>
|
||||
/// <param name="index">Index of the item that will be accessed</param>
|
||||
/// <returns>The item at the specified index</returns>
|
||||
object IList.this[int index] {
|
||||
get {
|
||||
requireCount();
|
||||
requirePage(index / this.pageSize);
|
||||
|
||||
return this.objectList[index];
|
||||
}
|
||||
set {
|
||||
// Make sure the page is fetched, otherwise, the item would only suddenly
|
||||
// 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
|
||||
|
||||
/// <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>
|
||||
/// <param name="oldItem">Item that has been replaced</param>
|
||||
/// <param name="newItem">New item the original item was replaced with</param>
|
||||
/// <param name="index">Index of the replaced item</param>
|
||||
protected virtual void OnReplaced(TItem oldItem, TItem newItem, int index) {
|
||||
if(ItemReplaced != null) {
|
||||
ItemReplaced(this, new ItemReplaceEventArgs<TItem>(oldItem, newItem));
|
||||
}
|
||||
#if !NO_SPECIALIZED_COLLECTIONS
|
||||
if(CollectionChanged != null) {
|
||||
CollectionChanged(
|
||||
this,
|
||||
new NotifyCollectionChangedEventArgs(
|
||||
NotifyCollectionChangedAction.Replace, newItem, oldItem, index
|
||||
)
|
||||
);
|
||||
}
|
||||
#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>
|
||||
/// <returns>The total number of items</returns>
|
||||
protected abstract int CountItems();
|
||||
|
||||
/// <summary>Fetches a page required by the collection</summary>
|
||||
/// <param name="target">List into which the items should be fetched</param>
|
||||
/// <param name="startIndex">
|
||||
/// Index of the first item to fetch. This is both the start index in
|
||||
/// the actual data and the element index at which to write into the list.
|
||||
/// </param>
|
||||
/// <param name="count">Number of items that should be fetched</param>
|
||||
/// <returns>The number of items that were actually fetched</returns>
|
||||
/// <remarks>
|
||||
/// If you fetch fewer than the requested number of items here, you will immediately
|
||||
/// truncate the entire list (it will assume that the end was reached, a means
|
||||
/// to support cases where the total number is not known). Fetching more than
|
||||
/// the requested number of items will just put items in memory that the list will
|
||||
/// continue to think are empty and fetch again if they are actually accessed.
|
||||
/// </remarks>
|
||||
protected abstract Task<int> FetchItemsAsync(
|
||||
IList<TItem> target, int startIndex, int count
|
||||
);
|
||||
|
||||
/// <summary>Requests placeholder items to be created</summary>
|
||||
/// <param name="target">List into which the items should be created</param>
|
||||
/// <param name="startIndex">
|
||||
/// Index of the first item to create a placeholder for. This is both the start index
|
||||
/// in the actual data and the element index at which to write into the list.
|
||||
/// </param>
|
||||
/// <param name="count">Number of items that should be crated</param>
|
||||
/// <remarks>
|
||||
/// When you requests items that have not been fetched yet, this method will be
|
||||
/// called synchronously to create placeholder items (these could be empty items
|
||||
/// or items with a 'StillLoading' flag set. This allows any UI controls showing
|
||||
/// the collection's contents to immediately display the items while fetching
|
||||
/// happens in the background and will replace the placeholder once complete.
|
||||
/// </remarks>
|
||||
protected abstract void CreatePlaceholderItems(
|
||||
IList<TItem> target, int startIndex, int count
|
||||
);
|
||||
|
||||
/// <summary>Ensures that the total number of items is known</summary>
|
||||
private void requireCount() {
|
||||
if(!this.assumedCount.HasValue) {
|
||||
int itemCount = CountItems();
|
||||
this.assumedCount = itemCount;
|
||||
|
||||
int pageCount = (itemCount + this.pageSize - 1) / this.pageSize;
|
||||
this.fetchedPages = new bool[pageCount];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Ensures that all items have fetched</summary>
|
||||
/// <remarks>
|
||||
/// Avoid if possible.
|
||||
/// </remarks>
|
||||
private void requireAllPages() {
|
||||
Debug.Assert(
|
||||
this.assumedCount.HasValue,
|
||||
"This method should only be called when item count is already known"
|
||||
);
|
||||
int pageCount = this.fetchedPages.Length;
|
||||
for(int index = 0; index < pageCount; ++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>
|
||||
/// <param name="pageIndex">Index of the page that needs to be fetched</param>
|
||||
private async void requirePage(int pageIndex) {
|
||||
Debug.Assert(
|
||||
this.assumedCount.HasValue,
|
||||
"This method should only be called when item count is already known"
|
||||
);
|
||||
if(this.fetchedPages[pageIndex]) {
|
||||
return;
|
||||
}
|
||||
|
||||
int startIndex = pageIndex * this.pageSize;
|
||||
int count = Math.Min(this.assumedCount.Value - startIndex, this.pageSize);
|
||||
CreatePlaceholderItems(this.typedList, pageIndex * this.pageSize, count);
|
||||
|
||||
this.fetchedPages[pageIndex] = true; // Prevent double-fetch
|
||||
|
||||
// Send out change notifications that the items have been replaced
|
||||
// (at this point, they're only placeholder items, of course)
|
||||
var placeholderItems = new List<TItem>(count);
|
||||
for(int index = startIndex; index < count; ++index) {
|
||||
placeholderItems[index - startIndex] = this.typedList[index];
|
||||
OnReplaced(default(TItem), this.typedList[index], index);
|
||||
}
|
||||
|
||||
int fetchedItemCount = await FetchItemsAsync(this.typedList, startIndex, count);
|
||||
if(fetchedItemCount < this.pageSize) {
|
||||
this.assumedCount = startIndex + fetchedItemCount;
|
||||
}
|
||||
|
||||
// The count may have been adjusted if this truncated the list,
|
||||
// so recalculate the actual number of items. Then send out change
|
||||
// notifications for the items that have now been fetched.
|
||||
count = Math.Min(this.assumedCount.Value - startIndex, this.pageSize);
|
||||
for(int index = startIndex; index < count; ++index) {
|
||||
OnReplaced(placeholderItems[index - startIndex], this.typedList[index], index);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Number of items the collection believes it has</summary>
|
||||
private int? assumedCount;
|
||||
/// <summary>Number of items to fetch in a single request</summary>
|
||||
private readonly int pageSize;
|
||||
/// <summary>Tracks which pages have been fetched so far</summary>
|
||||
private bool[] fetchedPages;
|
||||
/// <summary>The wrapped list under its type-safe interface</summary>
|
||||
private IList<TItem> typedList;
|
||||
/// <summary>The wrapped list under its object interface</summary>
|
||||
private IList objectList;
|
||||
#if DEBUG
|
||||
/// <summary>Used to detect when enumerators go out of sync</summary>
|
||||
private int version;
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Support.Collections
|
@ -1,617 +0,0 @@
|
||||
#region Apache License 2.0
|
||||
/*
|
||||
Nuclex .NET Framework
|
||||
Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
#endregion // Apache License 2.0
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
||||
#if !NO_SPECIALIZED_COLLECTIONS
|
||||
using System.Collections.Specialized;
|
||||
using System.Diagnostics;
|
||||
#endif
|
||||
|
||||
namespace Nuclex.Support.Collections {
|
||||
|
||||
/// <summary>
|
||||
/// List which fires events when items are added or removed, whilst also
|
||||
/// lazily fetching items as needed (for example from a socket or database)
|
||||
/// </summary>
|
||||
/// <typeparam name="TItem">Type of items the list manages</typeparam>
|
||||
public abstract class VirtualObservableReadOnlyList<TItem> :
|
||||
IList<TItem>,
|
||||
IList,
|
||||
ICollection,
|
||||
#if !NO_SPECIALIZED_COLLECTIONS
|
||||
INotifyCollectionChanged,
|
||||
#endif
|
||||
IObservableCollection<TItem> {
|
||||
|
||||
#region class Enumerator
|
||||
|
||||
/// <summary>Enumerates over the items in a virtual list</summary>
|
||||
private class Enumerator : IEnumerator<TItem>, IEnumerator {
|
||||
|
||||
/// <summary>Initializes a new virtual list enumerator</summary>
|
||||
/// <param name="virtualList">List whose items will be enumerated</param>
|
||||
public Enumerator(VirtualObservableReadOnlyList<TItem> virtualList) {
|
||||
this.virtualList = virtualList;
|
||||
this.currentItemIndex = -1;
|
||||
this.lastMoveNextResult = false;
|
||||
|
||||
Reset();
|
||||
}
|
||||
|
||||
/// <summary>Immediately releases all resources owned by the instance</summary>
|
||||
public void Dispose() {
|
||||
this.virtualList = null;
|
||||
}
|
||||
|
||||
/// <summary>The item at the enumerator's current position</summary>
|
||||
public TItem Current {
|
||||
get {
|
||||
#if DEBUG
|
||||
checkVersion();
|
||||
#endif
|
||||
|
||||
if(this.lastMoveNextResult == false) {
|
||||
throw new InvalidOperationException("Enumerator is not on a valid position");
|
||||
}
|
||||
|
||||
this.virtualList.requireCount();
|
||||
this.virtualList.requirePage(this.currentItemIndex / this.virtualList.pageSize);
|
||||
|
||||
return this.virtualList.typedList[this.currentItemIndex];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Advances the enumerator to the next item</summary>
|
||||
/// <returns>True if there was a next item</returns>
|
||||
public bool MoveNext() {
|
||||
#if DEBUG
|
||||
checkVersion();
|
||||
#endif
|
||||
|
||||
this.virtualList.requireCount();
|
||||
|
||||
// Go forward if there potentially are items remaining. The count may still be
|
||||
// unreliable at this point (due to the uncertain count mechanism that truncates
|
||||
// the list when fetching items finds an earlier end of the list)
|
||||
if(this.currentItemIndex < this.virtualList.assumedCount.Value) {
|
||||
|
||||
// If the enumerator's 'Current' property is never used an the virtual list
|
||||
// uses the dynamic truncation features (for unknown list sizes), then this
|
||||
// enumerator could be moved way past the last element via 'MoveNext()'.
|
||||
this.virtualList.requirePage(this.currentItemIndex / this.virtualList.pageSize);
|
||||
++this.currentItemIndex; // Accept potentially advancing past the end here
|
||||
|
||||
}
|
||||
|
||||
// Are we on a valid item? If so, return true to indicate the list continued,
|
||||
// otherwise we must have hit the end (or are already past it).
|
||||
return this.lastMoveNextResult = (
|
||||
(this.currentItemIndex < this.virtualList.assumedCount.Value)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>Resets the enumerator to its initial position</summary>
|
||||
public void Reset() {
|
||||
this.currentItemIndex = -1;
|
||||
#if DEBUG
|
||||
this.expectedVersion = this.virtualList.version;
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>The item at the enumerator's current position</summary>
|
||||
object IEnumerator.Current {
|
||||
get { return Current; }
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
/// <summary>Ensures that the virtual list has not changed</summary>
|
||||
private void checkVersion() {
|
||||
if(this.expectedVersion != this.virtualList.version)
|
||||
throw new InvalidOperationException("Virtual list has been modified");
|
||||
}
|
||||
#endif
|
||||
|
||||
/// <summary>Virtual list the enumerator belongs to</summary>
|
||||
private VirtualObservableReadOnlyList<TItem> virtualList;
|
||||
/// <summary>Index of the item the enumerator currently is in</summary>
|
||||
private int currentItemIndex;
|
||||
/// <summary>The most recent result returned from MoveNext()</summary>
|
||||
private bool lastMoveNextResult;
|
||||
#if DEBUG
|
||||
/// <summary>Version the virtual list is expected to have</summary>
|
||||
private int expectedVersion;
|
||||
#endif
|
||||
}
|
||||
|
||||
#endregion // class Enumerator
|
||||
|
||||
/// <summary>Raised when an item has been added to the collection</summary>
|
||||
public event EventHandler<ItemEventArgs<TItem>> ItemAdded;
|
||||
/// <summary>Raised when an item is removed from the collection</summary>
|
||||
public event EventHandler<ItemEventArgs<TItem>> ItemRemoved;
|
||||
/// <summary>Raised when an item is replaced in the collection</summary>
|
||||
public event EventHandler<ItemReplaceEventArgs<TItem>> ItemReplaced;
|
||||
/// <summary>Raised when the collection is about to be cleared</summary>
|
||||
/// <remarks>
|
||||
/// This could be covered by calling ItemRemoved for each item currently
|
||||
/// contained in the collection, but it is often simpler and more efficient
|
||||
/// to process the clearing of the entire collection as a special operation.
|
||||
/// </remarks>
|
||||
public event EventHandler Clearing;
|
||||
/// <summary>Raised when the collection has been cleared</summary>
|
||||
public event EventHandler Cleared;
|
||||
|
||||
#if !NO_SPECIALIZED_COLLECTIONS
|
||||
/// <summary>Called when the collection has changed</summary>
|
||||
public event NotifyCollectionChangedEventHandler CollectionChanged;
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the ObservableList class that is empty.
|
||||
/// </summary>
|
||||
/// <param name="pageSize">
|
||||
/// How many items to download in one batch
|
||||
/// </param>
|
||||
/// <remarks>
|
||||
/// The <paramref name="pageSize" /> can be set to one to request items
|
||||
/// individually or to a larger value in order to improve efficiency when
|
||||
/// the source of the items is a database or similar source that gains
|
||||
/// performance from requesting multiple items at once.
|
||||
/// </remarks>
|
||||
public VirtualObservableReadOnlyList(int pageSize = 32) {
|
||||
this.typedList = new List<TItem>();
|
||||
this.objectList = this.typedList as IList;
|
||||
this.pageSize = pageSize;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks all items as non-fetched, causing them to be requested again
|
||||
/// </summary>
|
||||
/// <param name="purgeItems">
|
||||
/// Whether to also clear the items that may already be in memory but would
|
||||
/// get overwritten on the next fetch. If items consume a lot of memory, this
|
||||
/// will make them available for garbage collection.
|
||||
/// </param>
|
||||
public void InvalidateAll(bool purgeItems = false) {
|
||||
if(this.assumedCount.HasValue) { // If not fetched before, no action needed
|
||||
int pageCount = this.fetchedPages.Length;
|
||||
for(int index = 0; index < pageCount; ++index) {
|
||||
this.fetchedPages[index] = false;
|
||||
}
|
||||
|
||||
if(purgeItems) {
|
||||
int itemCount = this.assumedCount.Value;
|
||||
for(int index = 0; index < itemCount; ++index) {
|
||||
this.typedList[index] = default(TItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks an items as non-fetched, causing it to be requested again on access
|
||||
/// </summary>
|
||||
/// <param name="itemIndex">
|
||||
/// Index of the item that will be marked as non-fetched
|
||||
/// </param>
|
||||
/// <param name="purgeItems">
|
||||
/// Whether to also clear the items that may already be in memory but would
|
||||
/// get overwritten on the next fetch. If items consume a lot of memory, this
|
||||
/// will make them available for garbage collection.
|
||||
/// </param>
|
||||
/// <remarks>
|
||||
/// Since the list works in pages, this will actually mark the whole page as
|
||||
/// non-fetched, causing all items in the same page to be requested again
|
||||
/// when any of them are next accessed.
|
||||
/// </remarks>
|
||||
public void Invalidate(int itemIndex, bool purgeItems = false) {
|
||||
if(this.assumedCount.HasValue) { // If not fetched before, no action needed
|
||||
int pageIndex = itemIndex / this.pageSize;
|
||||
this.fetchedPages[pageIndex] = false;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Determines the index of the specified item in the list</summary>
|
||||
/// <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>
|
||||
public int IndexOf(TItem item) {
|
||||
return this.typedList.IndexOf(item);
|
||||
}
|
||||
|
||||
/// <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="item">Item that will be inserted into the list</param>
|
||||
public void Insert(int index, TItem item) {
|
||||
throw new NotSupportedException("Cannot insert items into a read-only list");
|
||||
}
|
||||
|
||||
/// <summary>Removes the item at the specified index from the list</summary>
|
||||
/// <param name="index">Index at which the item will be removed</param>
|
||||
public void RemoveAt(int index) {
|
||||
throw new NotSupportedException("Cannot remove items from a read-only list");
|
||||
}
|
||||
|
||||
/// <summary>Accesses the item at the specified index in the list</summary>
|
||||
/// <param name="index">Index of the item that will be accessed</param>
|
||||
/// <returns>The item at the specified index</returns>
|
||||
public TItem this[int index] {
|
||||
get {
|
||||
requireCount();
|
||||
requirePage(index / this.pageSize);
|
||||
|
||||
return this.typedList[index];
|
||||
}
|
||||
set {
|
||||
// Make sure the page is fetched, otherwise, the item would only suddenly
|
||||
// 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.typedList[index] = value;
|
||||
OnReplaced(oldItem, value, index);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Adds an item to the end of the list</summary>
|
||||
/// <param name="item">Item that will be added to the list</param>
|
||||
public void Add(TItem item) {
|
||||
throw new NotSupportedException("Cannot add items to a read-only list");
|
||||
}
|
||||
|
||||
/// <summary>Removes all items from the list</summary>
|
||||
public void Clear() {
|
||||
throw new NotSupportedException("Cannot clear a read-only list");
|
||||
}
|
||||
|
||||
/// <summary>Checks whether the list contains the specified item</summary>
|
||||
/// <param name="item">Item the list will be checked for</param>
|
||||
/// <returns>True if the list contains the specified items</returns>
|
||||
public bool Contains(TItem item) {
|
||||
requireCount();
|
||||
requireAllPages();
|
||||
|
||||
return this.typedList.Contains(item);
|
||||
}
|
||||
|
||||
/// <summary>Copies the contents of the list into an array</summary>
|
||||
/// <param name="array">Array the list will be copied into</param>
|
||||
/// <param name="arrayIndex">
|
||||
/// Index in the target array where the first item will be copied to
|
||||
/// </param>
|
||||
public void CopyTo(TItem[] array, int arrayIndex) {
|
||||
requireCount();
|
||||
requireAllPages();
|
||||
|
||||
this.typedList.CopyTo(array, arrayIndex);
|
||||
}
|
||||
|
||||
/// <summary>Total number of items in the list</summary>
|
||||
public int Count {
|
||||
get {
|
||||
requireCount();
|
||||
return this.assumedCount.Value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Whether the list is a read-only list</summary>
|
||||
public bool IsReadOnly {
|
||||
get { return this.typedList.IsReadOnly; }
|
||||
}
|
||||
|
||||
/// <summary>Removes the specified item from the list</summary>
|
||||
/// <param name="item">Item that will be removed from the list</param>
|
||||
/// <returns>
|
||||
/// True if the item was found and removed from the list, false otherwise
|
||||
/// </returns>
|
||||
public bool Remove(TItem item) {
|
||||
throw new NotSupportedException("Cannot remove items from a read-only list");
|
||||
}
|
||||
|
||||
/// <summary>Returns an enumerator for the items in the list</summary>
|
||||
/// <returns>An enumerator for the list's items</returns>
|
||||
public IEnumerator<TItem> GetEnumerator() {
|
||||
return this.typedList.GetEnumerator(); // TODO
|
||||
}
|
||||
|
||||
#region IEnumerable implementation
|
||||
|
||||
/// <summary>Returns an enumerator for the items in the list</summary>
|
||||
/// <returns>An enumerator for the list's items</returns>
|
||||
IEnumerator IEnumerable.GetEnumerator() {
|
||||
return this.objectList.GetEnumerator(); // TODO
|
||||
}
|
||||
|
||||
#endregion // IEnumerable implementation
|
||||
|
||||
#region ICollection implementation
|
||||
|
||||
/// <summary>Copies the contents of the list into an array</summary>
|
||||
/// <param name="array">Array the list will be copied into</param>
|
||||
/// <param name="arrayIndex">
|
||||
/// Index in the target array where the first item will be copied to
|
||||
/// </param>
|
||||
void ICollection.CopyTo(Array array, int arrayIndex) {
|
||||
requireCount();
|
||||
requireAllPages();
|
||||
|
||||
this.objectList.CopyTo(array, arrayIndex);
|
||||
}
|
||||
|
||||
/// <summary>Whether this list performs thread synchronization</summary>
|
||||
bool ICollection.IsSynchronized {
|
||||
get { return this.objectList.IsSynchronized; }
|
||||
}
|
||||
|
||||
/// <summary>Synchronization root used by the list to synchronize threads</summary>
|
||||
object ICollection.SyncRoot {
|
||||
get { return this.objectList.SyncRoot; }
|
||||
}
|
||||
|
||||
#endregion // ICollection implementation
|
||||
|
||||
#region IList implementation
|
||||
|
||||
/// <summary>Adds an item to the list</summary>
|
||||
/// <param name="value">Item that will be added to the list</param>
|
||||
/// <returns>
|
||||
/// The position at which the item has been inserted or -1 if the item was not inserted
|
||||
/// </returns>
|
||||
int IList.Add(object value) {
|
||||
throw new NotSupportedException("Cannot add items into a read-only list");
|
||||
}
|
||||
|
||||
/// <summary>Checks whether the list contains the specified item</summary>
|
||||
/// <param name="item">Item the list will be checked for</param>
|
||||
/// <returns>True if the list contains the specified items</returns>
|
||||
bool IList.Contains(object item) {
|
||||
requireCount();
|
||||
requireAllPages();
|
||||
return this.objectList.Contains(item);
|
||||
}
|
||||
|
||||
/// <summary>Determines the index of the specified item in the list</summary>
|
||||
/// <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>
|
||||
int IList.IndexOf(object item) {
|
||||
requireCount();
|
||||
requireAllPages();
|
||||
return this.objectList.IndexOf(item);
|
||||
}
|
||||
|
||||
/// <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="item">Item that will be inserted into the list</param>
|
||||
void IList.Insert(int index, object item) {
|
||||
throw new NotSupportedException("Cannot insert items into a read-only list");
|
||||
}
|
||||
|
||||
/// <summary>Whether the list is of a fixed size</summary>
|
||||
bool IList.IsFixedSize {
|
||||
get { return this.objectList.IsFixedSize; }
|
||||
}
|
||||
|
||||
/// <summary>Removes the specified item from the list</summary>
|
||||
/// <param name="item">Item that will be removed from the list</param>
|
||||
void IList.Remove(object item) {
|
||||
throw new NotSupportedException("Cannot remove items from a read-only list");
|
||||
}
|
||||
|
||||
/// <summary>Accesses the item at the specified index in the list</summary>
|
||||
/// <param name="index">Index of the item that will be accessed</param>
|
||||
/// <returns>The item at the specified index</returns>
|
||||
object IList.this[int index] {
|
||||
get {
|
||||
requireCount();
|
||||
requirePage(index / this.pageSize);
|
||||
|
||||
return this.objectList[index];
|
||||
}
|
||||
set {
|
||||
// Make sure the page is fetched, otherwise, the item would only suddenly
|
||||
// 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
|
||||
|
||||
/// <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>
|
||||
/// <param name="oldItem">Item that has been replaced</param>
|
||||
/// <param name="newItem">New item the original item was replaced with</param>
|
||||
/// <param name="index">Index of the replaced item</param>
|
||||
protected virtual void OnReplaced(TItem oldItem, TItem newItem, int index) {
|
||||
if(ItemReplaced != null) {
|
||||
ItemReplaced(this, new ItemReplaceEventArgs<TItem>(oldItem, newItem));
|
||||
}
|
||||
#if !NO_SPECIALIZED_COLLECTIONS
|
||||
if(CollectionChanged != null) {
|
||||
CollectionChanged(
|
||||
this,
|
||||
new NotifyCollectionChangedEventArgs(
|
||||
NotifyCollectionChangedAction.Replace, newItem, oldItem, index
|
||||
)
|
||||
);
|
||||
}
|
||||
#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>
|
||||
/// <returns>The total number of items</returns>
|
||||
protected abstract int CountItems();
|
||||
|
||||
/// <summary>Fetches a page required by the collection</summary>
|
||||
/// <param name="target">List into which the items should be fetched</param>
|
||||
/// <param name="startIndex">
|
||||
/// Index of the first item to fetch. This is both the start index in
|
||||
/// the actual data and the element index at which to write into the list.
|
||||
/// </param>
|
||||
/// <param name="count">Number of items that should be fetched</param>
|
||||
/// <returns>The number of items that were actually fetched</returns>
|
||||
/// <remarks>
|
||||
/// If you fetch fewer than the requested number of items here, you will immediately
|
||||
/// truncate the entire list (it will assume that the end was reached, a means
|
||||
/// to support cases where the total number is not known). Fetching more than
|
||||
/// the requested number of items will just put items in memory that the list will
|
||||
/// continue to think are empty and fetch again if they are actually accessed.
|
||||
/// </remarks>
|
||||
protected abstract int FetchItems(IList<TItem> target, int startIndex, int count);
|
||||
|
||||
/// <summary>Ensures that the total number of items is known</summary>
|
||||
private void requireCount() {
|
||||
if(!this.assumedCount.HasValue) {
|
||||
int itemCount = CountItems();
|
||||
this.assumedCount = itemCount;
|
||||
|
||||
int pageCount = (itemCount + this.pageSize - 1) / this.pageSize;
|
||||
this.fetchedPages = new bool[pageCount];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Ensures that all items have fetched</summary>
|
||||
/// <remarks>
|
||||
/// Avoid if possible.
|
||||
/// </remarks>
|
||||
private void requireAllPages() {
|
||||
Debug.Assert(
|
||||
this.assumedCount.HasValue,
|
||||
"This method should only be called when item count is already known"
|
||||
);
|
||||
int pageCount = this.fetchedPages.Length;
|
||||
for(int index = 0; index < pageCount; ++index) {
|
||||
requirePage(index);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Ensures that the specified page has been fetched</summary>
|
||||
/// <param name="pageIndex">Index of the page that needs to be fetched</param>
|
||||
private void requirePage(int pageIndex) {
|
||||
Debug.Assert(
|
||||
this.assumedCount.HasValue,
|
||||
"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(fetchedItemCount < this.pageSize) {
|
||||
this.assumedCount = pageIndex * this.pageSize + fetchedItemCount;
|
||||
}
|
||||
|
||||
this.fetchedPages[pageIndex] = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Number of items the collection believes it has</summary>
|
||||
private int? assumedCount;
|
||||
/// <summary>Number of items to fetch in a single request</summary>
|
||||
private readonly int pageSize;
|
||||
/// <summary>Tracks which pages have been fetched so far</summary>
|
||||
private bool[] fetchedPages;
|
||||
/// <summary>The wrapped list under its type-safe interface</summary>
|
||||
private IList<TItem> typedList;
|
||||
/// <summary>The wrapped list under its object interface</summary>
|
||||
private IList objectList;
|
||||
#if DEBUG
|
||||
/// <summary>Used to detect when enumerators go out of sync</summary>
|
||||
private int version;
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
} // namespace Nuclex.Support.Collections
|
Loading…
Reference in New Issue
Block a user