У меня есть набор предметов (~ 12.000), которые я хочу показать в ListView
. Каждый из этих элементов представляет собой модель представления с назначенным изображением, которое не является частью пакета приложения (оно находится во «внешней» папке на локальном диске c). И из-за ограничений UWP я не могу (на самом деле и проверено) назначить Uri
для ImageSource
и вместо этого должен использовать метод SetSourceAsync
. Из-за этого начальное время загрузки приложения слишком велико, потому что все ImageSource
объекты должны быть инициализированы при запуске, даже если изображение не будет видно пользователю (список не фильтруется при запуске) и полученная память потребление ~ 4 ГБ. Копирование файлов изображений в каталог данных приложения решило бы проблему, но для меня это не является решением, поскольку изображения регулярно обновляются и это приводит к потере дискового пространства c.
Элементы отображаются в виде ListView
, который использует сгруппированный ICollectionView
в качестве источника.
Теперь я подумал, что мог бы реализовать IItemsRangeInfo
или ISupportIncrementalLoading
в каждой группе и отложить инициализацию модели представления, чтобы загружались только изображения, если они должны быть отображены. Я проверяю это, и это, похоже, не работает, потому что ни один метод интерфейса не вызывается для групп во время выполнения (пожалуйста, исправьте меня здесь, если это не так и может быть достигнуто). В текущей (не рабочей) версии используется пользовательский ICollectionView
(для целей тестирования), но DeferredObservableCollection
может также реализовать IGrouping<TKey, TElement>
и использоваться в CollectionViewSource
.
Есть ли способ, которым я Можно ли выполнить отложенную инициализацию или использовать Uri
для источника изображения или мне нужно использовать «обычную» коллекцию или пользовательский ICollectionView
как ItemsSource
для ListView
, который реализует желаемое поведение?
Текущая целевая версия приложения: 1803 (сборка 17134) Текущая целевая версия приложения: обновление создателей осени (сборка 16299) Можно изменить как минимальную, так и целевую версию.
Код для создания изображения source:
public class ImageService
{
// ...
private readonly IDictionary<short, ImageSource> imageSources;
public async Task<ImageSource> GetImageSourceAsync(Item item)
{
if (imageSources.ContainsKey(item.Id))
return imageSources[item.Id];
try
{
var imageFolder = await storageService.GetImagesFolderAsync();
var imageFile = await imageFolder.GetFileAsync($"{item.Id}.jpg");
var source = new BitmapImage();
await source.SetSourceAsync(await imageFile.OpenReadAsync());
return imageSources[item.Id] = source;
}
catch (FileNotFoundException)
{
// No image available.
return imageSources[item.Id] = unknownImageSource;
}
}
}
Код для результирующих групп, возвращаемых свойством ICollectionView.CollectionGroups
:
public class CollectionViewGroup : ICollectionViewGroup
{
public object Group { get; }
public IObservableVector<object> GroupItems { get; }
public CollectionViewGroup(object group, IObservableVector<object> items)
{
Group = group ?? throw new ArgumentNullException(nameof(group));
GroupItems = items ?? throw new ArgumentNullException(nameof(items));
}
}
Код коллекции, содержащей элементы каждой группы:
public sealed class DeferredObservableCollection<T, TSource>
: ObservableCollection<T>, IObservableVector<T>, IItemsRangeInfo //, ISupportIncrementalLoading
where T : class
where TSource : class
{
private readonly IList<TSource> source;
private readonly Func<TSource, Task<T>> conversionFunc;
// private int currentIndex; // Used for ISupportIncrementalLoading.
// Used to get the total number of items when using ISupportIncrementalLoading.
public int TotalCount => source.Count;
/// <summary>
/// Initializes a new instance of the <see cref="DeferredObservableCollection{T, TSource}"/> class.
/// </summary>
/// <param name="source">The source collection.</param>
/// <param name="conversionFunc">The function used to convert item from <typeparamref name="TSource"/> to <typeparamref name="T"/>.</param>
/// <exception cref="ArgumentNullException">
/// <paramref name="source"/> is <see langword="null"/> or
/// <paramref name="conversionFunc"/> is <see langword="null"/>.
/// </exception>
public DeferredObservableCollection(IList<TSource> source, Func<TSource, Task<T>> conversionFunc)
{
this.source = source ?? throw new ArgumentNullException(nameof(source));
this.conversionFunc = conversionFunc ?? throw new ArgumentNullException(nameof(conversionFunc));
// Ensure the underlying lists capacity.
// Used for IItemsRangeInfo.
for (var i = 0; i < source.Count; ++i)
Items.Add(default);
}
private class VectorChangedEventArgs : IVectorChangedEventArgs
{
public CollectionChange CollectionChange { get; }
public uint Index { get; }
public VectorChangedEventArgs(CollectionChange collectionChange, uint index)
{
CollectionChange = collectionChange;
Index = index;
}
}
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
base.OnCollectionChanged(e);
// For testing purposes the peformed action is not differentiated.
VectorChanged?.Invoke(this, new VectorChangedEventArgs(CollectionChange.ItemInserted, (uint)e.NewStartingIndex));
}
//#region ISupportIncrementalLoading Support
//public bool HasMoreItems => currentIndex < source.Count;
//public IAsyncOperation<LoadMoreItemsResult> LoadMoreItemsAsync(uint count)
//{
// Won't get called.
// return AsyncInfo.Run(async cancellationToken =>
// {
// if (currentIndex >= source.Count)
// return new LoadMoreItemsResult();
// var addedItems = 0u;
// while (currentIndex < source.Count && addedItems < count)
// {
// Add(await conversionFunc(source[currentIndex]));
// ++currentIndex;
// ++addedItems;
// }
// return new LoadMoreItemsResult { Count = addedItems };
// });
//}
//#endregion
#region IObservableVector<T> Support
public event VectorChangedEventHandler<T> VectorChanged;
#endregion
#region IItemsRangeInfo Support
public void RangesChanged(ItemIndexRange visibleRange, IReadOnlyList<ItemIndexRange> trackedItems)
{
// Won't get called.
ConvertItemsAsync(visibleRange, trackedItems).FireAndForget(null);
}
private async Task ConvertItemsAsync(ItemIndexRange visibleRange, IReadOnlyList<ItemIndexRange> trackedItems)
{
for (var i = visibleRange.FirstIndex; i < source.Count && i < visibleRange.LastIndex; ++i)
{
if (this[i] is null)
{
this[i] = await conversionFunc(source[i]);
}
}
}
public void Dispose()
{ }
#endregion
}