I have a set of items (~12.000) i want to show in a ListView
. Each of those items is a view model that has an assigned image that is not part of the app package (it's in an 'external' folder on a local disc). And because of UWP's limitations i can't (afaik and tested) assign an Uri
to the ImageSource
and have to use the SetSourceAsync
method instead. Because of this the initial loading time of the app is too high, because all ImageSource
objects have to be initialized at start up even if the image will not be seen by the user (the list is unfiltered at startup) and the resulting memory consumption is ~4GB. Copying the image files to an app data directory would resolve the issue, but is no solution for me, because the images are updated regularly and it would waste disc space.
The items are displayed in a ListView
that uses a grouped ICollectionView
as source.
Now i thought i could implement either IItemsRangeInfo
or ISupportIncrementalLoading
on each group and defer the initialization of the view model so only images are loaded if they are to be displayed. I testet this and it does not seem to work because neither interface's method gets called on the groups at runtime (please correct me here if that is not true and can be achieved). The current (not working) version uses a custom ICollectionView
(for testing purposes) but the DeferredObservableCollection
could as well implement IGrouping<TKey, TElement>
and be used in a CollectionViewSource
.
Is there any way i could achieve the deferred initialization or use an Uri
for the image source or do i have to use a 'plain' collection or custom ICollectionView
as ItemsSource
on the ListView
that implements the desired behaviour?
Current target version of the app: 1803 (Build 17134) Current target version of the app: Fall Creators Update (Build 16299) Both (minimum and target version) can be changed.
Code for creating the image 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;
}
}
}
Code for the resulting groups that are returned by the ICollectionView.CollectionGroups
property:
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));
}
}
Code of the collection that contains the items of each group:
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
}
From the perspective of reducing memory consumption, the method of using BitmapImage.SetSourceAsync
is not recommended, because it is not conducive to memory release. But considering your actual situation, I can provide some suggestions to help you optimize application performance.
1. Don't initialize 12000 images uniformly
Reading 12,000 pictures at a time will inevitably increase the memory usage. But we can create UserControl
as one picture unit, and hand over the work of loading pictures to these units.
-ImageItem.cs
public class ImageItem
{
public string Name { get; set; }
public BitmapImage Image { get; set; } = null;
public ImageItem()
{
}
public async Task Init()
{
// do somethings..
// get image from folder, named imageFile
Image = new BitmapImage();
await Image.SetSourceAsync(await imageFile.OpenReadAsync());
}
}
-ImageItemControl.xaml
<UserControl
...>
<StackPanel>
<Image Width="200" Height="200" x:Name="MyImage"/>
</StackPanel>
</UserControl>
-ImageItemControl.xaml.cs
public sealed partial class ImageItemControl : UserControl
{
public ImageItemControl()
{
this.InitializeComponent();
}
public ImageItem Data
{
get { return (ImageItem)GetValue(DataProperty); }
set { SetValue(DataProperty, value); }
}
public static readonly DependencyProperty DataProperty =
DependencyProperty.Register("Data", typeof(ImageItem), typeof(ImageItemControl), new PropertyMetadata(null,new PropertyChangedCallback(Data_Changed)));
private static async void Data_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if(e.NewValue != null)
{
var image = e.NewValue as ImageItem;
var instance = d as ImageItemControl;
if (image.Image == null)
{
await image.Init();
}
instance.MyImage.Source = image.Image;
}
}
}
-Usage
<Page.Resources>
<DataTemplate x:DataType="local:ImageItem" x:Key="ImageTemplate">
<controls:ImageItemControl Data="{Binding}"/>
</DataTemplate>
</Page.Resources>
<Grid>
<GridView ItemTemplate="{StaticResource ImageTemplate}"
.../>
</Grid>
Please modify this code according to your actual situation
There are some advantages to this. Through the distributed method, on the one hand, the loading speed of pictures is increased (simultaneous loading). On the other hand, with virtualization, some pictures are not actually rendered, which can reduce memory usage.
2. Limiting the resolution of BitmapImage
This is very important, it can greatly reduce the memory consumption when loading a large number of pictures.
For example, you have a picture with 1920x1080 resolution, but only 200x200 resolution is displayed on the application. Then loading the original image will waste system resources.
We can modify the ImageItem.Init
method:
public async Task Init()
{
// do somethings..
// get image from folder, named imageFile
Image = new BitmapImage() { DecodePixelWidth = 200 };
await Image.SetSourceAsync(await imageFile.OpenReadAsync());
}
Hope these two methods can help you reduce memory usage.