Загрузка большого количества изображений для отображения в WrapPanel

сначала я использую код Entity Framework

у меня есть кино вот так:

public class Movie
{
        public byte[] Thumbnail { get; set; }
        public int MovieId { get; set; }
}

и сборник кинокак так:

public class NorthwindContext : DbContext
{
    public DbSet<Movie> Movies { get; set; }
}

у меня есть MovieViewModel вот так:

public class MovieViewModel
{
        private readonly Movie _movie;

        public MovieViewModel(Movie movie)
        {
            _movieModel = movieModel;
        }

        public byte[] Thumbnail { get { return _movie.Thumbnail; } }
}

когда мое приложение запускается:

public ObservableCollection<MovieViewModel> MovieVms = 
                      new ObservableCollection<MovieViewModel>();

foreach (var movie in MyDbContext.Movies)
     MovieVms.Add(new MovieViewModel(movie));

у меня 4000 фильмов. Этот процесс занимает 25 секунд. Есть ли лучший / быстрый способ сделать это?

моя главная страница использует миниатюры так, но, чтобы быть ясным, это время загрузки происходит до того, как что-либо связанное с UI:

MyView = new ListCollectionView(MovieVms);

<ListBox ItemsSource="{Binding MyView}" />

также мое использование памяти проходит через крышу. Как я должен загружать эти изображения? Мне нужна полная коллекция моделей просмотра с места в карьер, чтобы включить сортировку, фильтрацию, поиск, но мне нужны только эскизы элементов, видимых на моей панели обертки.

правка---

спасибо Дэйв за отличный ответ. Можете ли вы подробно остановиться на "make it an association (он же свойство навигации)"

var thumbnail = new Thumbnail();
thumbnail.Data = movie.GetThumbnail();
Globals.DbContext.Thumbnails.Add(thumbnail);
Globals.DbContext.SaveChanges();
movie.ThumbnailId = thumbnail.ThumbnailId;
Globals.DbContext.SaveChanges();

я могу запустить этот код без ошибок, но мое свойство в моем MovieViewModel

public new byte[] Thumbnail { get { return _movie.Thumbnail.Data; } }

всегда имеет значение null Thumbnail и ошибки, как только мой пользовательский интерфейс обращается к нему. Точка останова на movie.ThumbnailId никогда не ударил. Нужно ли загружать ассоциацию вручную?

5 ответов


я думаю, что вы по существу спрашиваете, как сделать несколько разных вещей:

  • загрузите весь список фильмов быстро, чтобы разрешить сортировку и фильтрацию в пользовательском интерфейсе
  • отображать миниатюры фильмов в пользовательском интерфейсе, но только когда они прокручиваются в view
  • сохранить использование памяти к минимуму
  • отображение пользовательского интерфейса как можно быстрее после запуска приложения

загрузить фильмы быстро!--69-->

во-первых, как говорится в ответе @Dave M, вам нужно разделить эскиз на отдельный объект, чтобы вы могли попросить Entity Framework загрузить список фильмов без загрузки эскизов.

public class Movie
{
    public int Id { get; set; }
    public int ThumbnailId { get; set; }
    public virtual Thumbnail Thumbnail { get; set; }  // This property must be declared virtual
    public string Name { get; set; }

    // other properties
}

public class Thumbnail
{
    public int Id { get; set; }
    public byte[] Image { get; set; }
}

public class MoviesContext : DbContext
{
    public MoviesContext(string connectionString)
        : base(connectionString)
    {
    }

    public DbSet<Movie> Movies { get; set; }
    public DbSet<Thumbnail> Thumbnails { get; set; }
}

Итак, чтобы загрузить все фильмы:

public List<Movie> LoadMovies()
{
    // Need to get '_connectionString' from somewhere: probably best to pass it into the class constructor and store in a field member
    using (var db = new MoviesContext(_connectionString))
    {
        return db.Movies.AsNoTracking().ToList();
    }
}

в этот момент у вас будет список кино предприятия, где ThumbnailId свойство заполняется, но Thumbnail собственность будет null как вы не просили EF загрузите связанный эскиз объекты. Кроме того, если вы попытаетесь получить доступ к Thumbnail собственность позже вы получите исключение, как MoviesContext больше не в области.

если у вас есть список кино сущности, вам нужно преобразовать их в ViewModels. Я предполагаю, что ваши ViewModels эффективно доступны только для чтения.

public sealed class MovieViewModel
{
    public MovieViewModel(Movie movie)
    {
        _thumbnailId = movie.ThumbnailId;
        Id = movie.Id;
        Name = movie.Name;
        // copy other property values across
    }

    readonly int _thumbnailId;

    public int Id { get; private set; }
    public string Name { get; private set; }
    // other movie properties, all with public getters and private setters

    public byte[] Thumbnail { get; private set; }  // Will flesh this out later!
}

обратите внимание, что мы просто сохраняем идентификатор эскиза здесь, а не заполняем Thumbnail еще. Я приду к это немного позже.

загрузить миниатюры отдельно и кэшировать их

Итак, вы загрузили фильмы, но на данный момент Вы не загрузили никаких эскизов. Что вам нужно, это метод, который будет загружать один эскиз сущность из базы данных с учетом ее идентификатора. Я бы предложил объединить это с каким-то кэшем, чтобы после загрузки миниатюры вы некоторое время хранили ее в памяти.

public sealed class ThumbnailCache
{
    public ThumbnailCache(string connectionString)
    {
        _connectionString = connectionString;
    }

    readonly string _connectionString;
    readonly Dictionary<int, Thumbnail> _cache = new Dictionary<int, Thumbnail>();

    public Thumbnail GetThumbnail(int id)
    {
        Thumbnail thumbnail;

        if (!_cache.TryGetValue(id, out thumbnail))
        {
            // Not in the cache, so load entity from database
            using (var db = new MoviesContext(_connectionString))
            {
                thumbnail = db.Thumbnails.AsNoTracking().Find(id);
            }

            _cache.Add(id, thumbnail);
        }

        return thumbnail;
    }
}

это, очевидно, очень простой кэш: поиск блокируется, нет обработки ошибок, и миниатюры действительно должны быть удалены из кэша, если они не были получены в течение некоторого времени, чтобы сохранить использование памяти.

возвращаясь к ViewModel, вам нужно изменить конструктор, чтобы взять ссылку на экземпляр кэша, а также изменить Thumbnail свойство getter для извлечения миниатюры из кэша:

public sealed class MovieViewModel
{
    public MovieViewModel(Movie movie, ThumbnailCache thumbnailCache)
    {
        _thumbnailId = movie.ThumbnailId;
        _thumbnailCache = thumbnailCache;
        Id = movie.Id;
        Name = movie.Name;
        // copy other property values across
    }

    readonly int _thumbnailId;
    readonly ThumbnailCache _thumbnailCache;

    public int Id { get; private set; }
    public string Name { get; private set; }
    // other movie properties, all with public getters and private setters

    public BitmapSource Thumbnail
    {
        get
        {
            if (_thumbnail == null)
            {
                byte[] image = _thumbnailCache.GetThumbnail(_thumbnailId).Image;

                // Convert to BitmapImage for binding purposes
                var bitmapImage = new BitmapImage();
                bitmapImage.BeginInit();
                bitmapImage.StreamSource = new MemoryStream(image);
                bitmapImage.CreateOptions = BitmapCreateOptions.None;
                bitmapImage.CacheOption = BitmapCacheOption.Default;
                bitmapImage.EndInit();

                _thumbnail = bitmapImage;
            }

            return _thumbnail;
        }
    }
    BitmapSource _thumbnail;
}

теперь изображения thumnail будут загружены только тогда, когда Thumbnail свойство доступно: если изображение уже было в кэше, оно будет немедленно возвращено, в противном случае оно будет загружено из базы данных, а затем сохранено в кэше для дальнейшего использования.

привязка производительности

то, как вы связываете свою коллекцию MovieViewModels к управлению в вашем представлении будет иметь влияние на воспринимаемое время загрузки, а также. Что вы хотите сделать, когда это возможно, это отложить привязку до тех пор, пока ваша коллекция не будет заполнена. Это будет быстрее, чем привязка к пустой коллекции, а затем добавить элементы в коллекции. Возможно, вы уже знаете это, но я подумал, что упомяну об этом на всякий случай.

эта страница MSDN (Оптимизация Производительности: Привязка Данных) имеет некоторые полезные советы.

эта удивительная серия сообщений в блоге Яна Гриффитса (слишком много, слишком быстро с WPF и Async) показывает, как различные стратегии Привязки могут влиять на время загрузки связанный список.

только загрузка эскизов при просмотре

теперь самое трудное! Мы остановили загрузку эскизов при запуске приложения, но нам нужно загрузить их в какой-то момент. Лучшее время для их загрузки - когда они видны в пользовательском интерфейсе. Таким образом, возникает вопрос: Как определить, когда миниатюра отображается в пользовательском интерфейсе? Это во многом зависит от элементов управления, которые вы используете в своем представлении (UI).

я предполагаю, что вы привязка вашей коллекции MovieViewModels к ItemsControl какого-то типа, например ListBox или ListView. Кроме того, я предполагаю, что у вас есть какой-то DataTemplate настроить (либо как часть ListBox/ListView разметка, или в ResourceDictionary где-то), который сопоставляется с MovieViewModel тип. Очень простая версия этого DataTemplate может выглядеть так:

<DataTemplate DataType="{x:Type ...}">
    <StackPanel>
        <Image Source="{Binding Thumbnail}" Stretch="Fill" Width="100" Height="100" />
        <TextBlock Text="{Binding Name}" />
    </StackPanel>
</DataTemplate>

если вы используете ListBox, даже если вы измените панель, которую она использует что-то вроде WrapPanel на ListBox ' s ControlTemplate содержит ScrollViewer, который предоставляет полосы прокрутки и обрабатывает любую прокрутку. В этом случае мы можем сказать, что миниатюра видна, когда она появляется в ScrollViewer's viewport. Поэтому нам нужен custom ScrollViewer элемент, который при прокрутке определяет, какие из его "дочерних элементов" видны в окне просмотра, и помечает их соответствующим образом. Лучший способ пометить их-использовать вложенное логическое свойство: таким образом, мы можем измените DataTemplate чтобы вызвать изменение значения вложенного свойства и загрузить миниатюру в этот момент.

следующее ScrollViewer потомок (извините за ужасное название!) сделает именно это (обратите внимание, что это, вероятно, может быть сделано с прикрепленным поведением вместо подкласса, но этот ответ достаточно длинный).

public sealed class MyScrollViewer : ScrollViewer
{
    public static readonly DependencyProperty IsInViewportProperty =
        DependencyProperty.RegisterAttached("IsInViewport", typeof(bool), typeof(MyScrollViewer));

    public static bool GetIsInViewport(UIElement element)
    {
        return (bool) element.GetValue(IsInViewportProperty);
    }

    public static void SetIsInViewport(UIElement element, bool value)
    {
        element.SetValue(IsInViewportProperty, value);
    }

    protected override void OnScrollChanged(ScrollChangedEventArgs e)
    {
        base.OnScrollChanged(e);

        var panel = Content as Panel;
        if (panel == null)
        {
            return;
        }

        Rect viewport = new Rect(new Point(0, 0), RenderSize);

        foreach (UIElement child in panel.Children)
        {
            if (!child.IsVisible)
            {
                SetIsInViewport(child, false);
                continue;
            }

            GeneralTransform transform = child.TransformToAncestor(this);
            Rect childBounds = transform.TransformBounds(new Rect(new Point(0, 0), child.RenderSize));
            SetIsInViewport(child, viewport.IntersectsWith(childBounds));
        }
    }
}

в основном это ScrollViewer предполагается, что это Content является панелью и устанавливает прилагаемый IsInViewport свойство true для те дети панели, которые лежат в пределах окна просмотра, т. е. видны пользователю. Все, что осталось теперь, это изменить XAML для представления, чтобы включить этот пользовательский ScrollViewer в рамках С:

<Window x:Class="..."
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:my="clr-namespace:...">

    <Window.Resources>
        <DataTemplate DataType="{x:Type my:MovieViewModel}">
            <StackPanel>
                <Image x:Name="Thumbnail" Stretch="Fill" Width="100" Height="100" />
                <TextBlock Text="{Binding Name}" />
            </StackPanel>
            <DataTemplate.Triggers>
                <DataTrigger Binding="{Binding Path=(my:MyScrollViewer.IsInViewport), RelativeSource={RelativeSource AncestorType={x:Type ListBoxItem}}}"
                             Value="True">
                    <Setter TargetName="Thumbnail" Property="Source" Value="{Binding Thumbnail}" />
                </DataTrigger>
            </DataTemplate.Triggers>
        </DataTemplate>
    </Window.Resources>

    <ListBox ItemsSource="{Binding Movies}">
        <ListBox.Template>
            <ControlTemplate>
                <my:MyScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
                    <WrapPanel IsItemsHost="True" />
                </my:MyScrollViewer>
            </ControlTemplate>
        </ListBox.Template>
    </ListBox>

</Window>
у нас есть Window содержащий один ListBox. Мы изменили ControlTemplate на ListBox включить пользовательский ScrollViewer, а внутри это WrapPanel это будет макет элементов. В ресурсах окна у нас есть DataTemplate что будет используется для отображения каждого MovieViewModel. Это похоже на DataTemplate введено ранее, но обратите внимание, что мы больше не связываем Image ' s Source собственность в теле шаблона: вместо этого, мы используем триггер на основе IsInViewport свойство и установите привязку, когда элемент становится "видимым". Эта привязка вызовет MovieViewModel класса Thumbnail вызывается геттер свойств, который загружает миниатюру изображения либо из кэша, либо из база данных. Обратите внимание, что привязка к свойству родительского ListBoxItem, в котором разметка для DataTemplate вводят.

единственная проблема с этим подходом заключается в том, что, поскольку загрузка миниатюр выполняется в потоке пользовательского интерфейса, прокрутка будет затронута. Самый простой способ исправить это - изменить MovieViewModel Thumbnail свойство getter, чтобы вернуть" фиктивный " эскиз, запланировать вызов кэша в отдельном потоке, а затем получить этот поток для установки Thumbnail собственность соответственно и поднять PropertyChanged событие, таким образом, обеспечивая механизм привязки подхватывает изменение. Есть и другие решения, но они значительно усложнят задачу: рассмотрите то, что здесь представлено, как возможную отправную точку.


всякий раз, когда вы запрашиваете сущность из EF, она автоматически загружает все скалярные свойства (только Ассоциации загружаются лениво). Переместите данные эскиза в собственный объект, сделайте его ассоциацией (aka navigation property) и воспользуйтесь ленивой загрузкой.

public class Movie
{
    public int Id { get; set; }
    public int ThumbnailId { get; set; }
    public virtual Thumbnail Thumbnail { get; set; }

    public string Name { get; set; }
    public double Length { get; set; }
    public DateTime ReleaseDate { get; set; }
    //etc...
}

public class Thumbnail
{
    public int Id { get; set; }
    public byte[] Data { get; set; }
}

public class MovieViewModel
{
    private readonly Movie _movie;

    public MovieViewModel(Movie movie)
    {
        _movieModel = movieModel;
    }

    public byte[] Thumbnail { get { return _movie.Thumbnail.Data; } }
}

теперь данные эскиза будут загружаться только из базы данных, когда свойство эскиза ViewModel будет доступно пользовательскому интерфейсу.


вы хотите загрузить все изображения сразу? (Я recon не все 4000 + миниатюры фильмов будут показаны на экране в то же время). Самый простой способ, я думаю, вы могли бы достичь этого, чтобы загрузить изображения, когда это необходимо (например, только загрузить те, которые показывают и утилизировать их (для экономии памяти), когда не отображается).

Это должно ускорить работу, так как вам нужно только создать экземпляр объектов (и пусть ObservableCollection указывает на адрес памяти объектов) и снова загружайте изображения только при необходимости.

подсказки к ответу:

разделить экран на блоки (страницы)
И после изменения индекса загрузите новые изображения (у вас уже есть список наблюдаемых коллекций)

Если вы все еще сталкиваетесь с трудностями, я постараюсь дать более четкий ответ:)

Goodluck


у меня были проблемы с ответом при применении к списку, чей ItemsSource является динамическим. В этом случае при изменении источника ScrollViewer не обязательно, триггер не запускается и изображения не загружаются.

моя основная проблема касается ленивой загрузки большого количества изображений highres в UniformGrid (который не виртуализирован).

чтобы преодолеть это, я применил поведение на ListBoxItem. Я считаю, что это хорошее решение, потому что вам не нужно подкласс ScrollViewer и изменить шаблон ListBox, но только ListBoxItem.

добавьте поведение в свой проект:

namespace behaviors
{
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Interactivity;
    using System.Windows.Media;

    public class ListBoxItemIsVisibleBehavior : Behavior<ListBoxItem>
    {
        public static readonly DependencyProperty IsInViewportProperty = DependencyProperty.RegisterAttached("IsInViewport", typeof(bool), typeof(ListBoxItemIsVisibleBehavior));

        public static bool GetIsInViewport(UIElement element)
        {
            return (bool)element.GetValue(IsInViewportProperty);
        }

        public static void SetIsInViewport(UIElement element, bool value)
        {
            element.SetValue(IsInViewportProperty, value);
        }

        protected override void OnAttached()
        {
            base.OnAttached();

            try
            {
                this.AssociatedObject.LayoutUpdated += this.AssociatedObject_LayoutUpdated;
            }
            catch { }
        }

        protected override void OnDetaching()
        {
            try
            {
                this.AssociatedObject.LayoutUpdated -= this.AssociatedObject_LayoutUpdated;
            }
            catch { }

            base.OnDetaching();
        }

        private void AssociatedObject_LayoutUpdated(object sender, System.EventArgs e)
        {
            if (this.AssociatedObject.IsVisible == false)
            {
                SetIsInViewport(this.AssociatedObject, false);
                return;
            }

            var container = WpfExtensions.FindParent<ListBox>(this.AssociatedObject);
            if (container == null)
            {
                return;
            }

            var visible = this.IsVisibleToUser(this.AssociatedObject, container) == true;
            SetIsInViewport(this.AssociatedObject, visible);
        }

        private bool IsVisibleToUser(FrameworkElement element, FrameworkElement container)
        {
            if (element.IsVisible == false)
            {
                return false;
            }

            GeneralTransform transform = element.TransformToAncestor(container);
            Rect bounds = transform.TransformBounds(new Rect(0.0, 0.0, element.ActualWidth, element.ActualHeight));
            Rect viewport = new Rect(0.0, 0.0, container.ActualWidth, container.ActualHeight);
            return viewport.IntersectsWith(bounds);
        }
    }
}

тогда вам придется использовать этот ответ, чтобы добавить поведение в свой стиль ListBoxItem:как добавить поведение смешивания в установщик стилей

это приводит к добавлению помощника в ваш проект:

public class Behaviors : List<Behavior>
{
}

public static class SupplementaryInteraction
{
    public static Behaviors GetBehaviors(DependencyObject obj)
    {
        return (Behaviors)obj.GetValue(BehaviorsProperty);
    }

    public static void SetBehaviors(DependencyObject obj, Behaviors value)
    {
        obj.SetValue(BehaviorsProperty, value);
    }

    public static readonly DependencyProperty BehaviorsProperty =
        DependencyProperty.RegisterAttached("Behaviors", typeof(Behaviors), typeof(SupplementaryInteraction), new UIPropertyMetadata(null, OnPropertyBehaviorsChanged));

    private static void OnPropertyBehaviorsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var behaviors = Interaction.GetBehaviors(d);
        foreach (var behavior in e.NewValue as Behaviors) behaviors.Add(behavior);
    }
}

затем добавьте поведение в ресурс где-то в вашем управлении :

<UserControl.Resources>
    <behaviors:Behaviors x:Key="behaviors" x:Shared="False">
        <behaviors:ListBoxItemIsVisibleBehavior />
    </behaviors:Behaviors>
</UserControl.Resources>

и ссылка на этот ресурс и триггер в стиле вашего ListBoxItem:

<Style x:Key="_ListBoxItemStyle" TargetType="ListBoxItem">
    <Setter Property="behaviors:SupplementaryInteraction.Behaviors" Value="{StaticResource behaviors}"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="ListBoxItem">
                <StackPanel d:DataContext="{d:DesignInstance my:MovieViewModel}">
                    <Image x:Name="Thumbnail" Stretch="Fill" Width="100" Height="100" />
                    <TextBlock Text="{Binding Name}" />
                </StackPanel>

                <ControlTemplate.Triggers>
                    <DataTrigger Binding="{Binding Path=(behaviors:ListBoxItemIsVisibleBehavior.IsInViewport), RelativeSource={RelativeSource Self}}"
                                 Value="True">
                        <Setter TargetName="Thumbnail"
                                Property="Source"
                                Value="{Binding Thumbnail}" />
                    </DataTrigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

и ссылаться на стиль в вашем списке:

<ListBox ItemsSource="{Binding Movies}"
         Style="{StaticResource _ListBoxItemStyle}">
</ListBox>  

в случае UniformGrid :

<ListBox ItemsSource="{Binding Movies}"
         Style="{StaticResource _ListBoxItemStyle}">
    <ListBox.ItemsPanel>
        <ItemsPanelTemplate>
            <UniformGrid Columns="5" />
        </ItemsPanelTemplate>
    </ListBox.ItemsPanel>
</ListBox>  

это занимает так много времени, потому что он помещает все, что вы загрузили в трекер изменений EF. Это все 4000 записей отслеживаются в памяти, что по понятным причинам вызывает замедление вашего приложения. Если вы на самом деле не редактируете страницу, Я предлагаю вам использовать .AsNoTracking() когда вы захватите Movies.

а так:

var allMovies = MyDbContext.Movies.AsNoTracking();
foreach (var movie in allMovies)
     MovieVms.Add(new MovieViewModel(movie));

MSDN ссылка на нем можно найти здесь.