【问题标题】:Implement deferred item loading in a grouped ICollectionView在分组的 ICollectionView 中实现延迟项目加载
【发布时间】:2020-02-20 10:22:26
【问题描述】:

我有一组项目 (~12.000) 我想在 ListView 中显示。这些项目中的每一个都是一个视图模型,它具有一个不属于应用程序包的分配图像(它位于本地磁盘上的“外部”文件夹中)。而且由于 UWP 的限制,我不能(afaik 和测试)将Uri 分配给ImageSource,而必须使用SetSourceAsync 方法。因此,应用程序的初始加载时间太长,因为所有ImageSource 对象都必须在启动时初始化,即使用户看不到图像(列表在启动时未过滤)和结果内存消耗约为 4GB。将图像文件复制到应用程序数据目录可以解决问题,但对我来说不是解决方案,因为图像会定期更新,会浪费磁盘空间。

项目显示在ListView 中,该ICollectionView 使用分组的ICollectionView 作为源。

现在我想我可以在每个组上实现IItemsRangeInfoISupportIncrementalLoading 并推迟视图模型的初始化,以便仅加载要显示的图像。我对此进行了测试,但它似乎不起作用,因为在运行时组上都没有调用接口的方法(如果这不是真的并且可以实现,请在此处纠正我)。当前(不工作)版本使用自定义ICollectionView(用于测试目的),但DeferredObservableCollection 也可以实现IGrouping<TKey, TElement> 并用于CollectionViewSource

有什么方法可以实现延迟初始化或使用Uri 作为图像源,或者我必须在实现的ListView 上使用“普通”集合或自定义ICollectionView 作为ItemsSource期望的行为?

应用的当前目标版本:1803(内部版本 17134) 应用程序的当前目标版本:Fall Creators Update (Build 16299) 两者(最低版本和目标版本)都可以更改。

创建图片源代码:

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
}

【问题讨论】:

    标签: c# uwp collectionviewsource icollectionview


    【解决方案1】:

    从减少内存消耗的角度,不推荐使用BitmapImage.SetSourceAsync的方法,因为不利于内存释放。但考虑到您的实际情况,我可以提供一些建议来帮助您优化应用程序性能。

    1.不要统一初始化12000张图片

    一次读取12000张图片必然会增加内存占用。但是我们可以将UserControl创建为一个图片单元,将加载图片的工作交给这些单元。

    -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;
            }
        }
    }
    

    -用法

    <Page.Resources>
        <DataTemplate x:DataType="local:ImageItem" x:Key="ImageTemplate">
            <controls:ImageItemControl Data="{Binding}"/>
        </DataTemplate>
    </Page.Resources>
    <Grid>
        <GridView ItemTemplate="{StaticResource ImageTemplate}"
                  .../>
    </Grid>
    

    请根据您的实际情况修改此代码

    这样做有一些好处。通过分布式的方式,一方面提高了图片的加载速度(同时加载)。另一方面,通过虚拟化,一些图片实际上并没有被渲染,这可以减少内存使用。

    2。限制BitmapImage的分辨率

    这个很重要,在加载大量图片时可以大大减少内存消耗。

    例如,您有一张分辨率为 1920x1080 的图片,但应用程序上仅显示 200x200 分辨率。那么加载原图会浪费系统资源。

    我们可以修改ImageItem.Init方法:

    public async Task Init()
    {
        // do somethings..
        // get image from folder, named imageFile
        Image = new BitmapImage() { DecodePixelWidth = 200 };
        await Image.SetSourceAsync(await imageFile.OpenReadAsync());
    }
    

    希望这两种方法可以帮助你减少内存使用。

    【讨论】:

    • 我喜欢将与图像相关的东西放在用户控件中的想法,我一定会尝试的。我当前使用自定义ICollectionView 的解决方案确实可以以某种方式工作,但会被更好的东西所取代。关于图片尺寸:所有图片都只有 177x254,所以你的第二点在这方面对我没有帮助。
    猜你喜欢
    • 1970-01-01
    • 2021-05-25
    • 2016-09-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多