加载要在WrapPanel中显示的大量图像

我正在使用Entity Framework Code First

我有这样的电影

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

电影的集合是这样的:

 public class NorthwindContext : DbContext { public DbSet 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 MovieVms = new ObservableCollection(); foreach (var movie in MyDbContext.Movies) MovieVms.Add(new MovieViewModel(movie)); 

我有4000部电影。 此过程需要25秒 。 有更好/更快的方法吗?

我的主页面使用缩略图,但要清楚这个加载时间发生在任何UI相关之前:

 MyView = new ListCollectionView(MovieVms);  

我的记忆用量也经过了屋顶。 我该如何加载这些图片? 我需要一个完整的视图模型集合,以启用排序,过滤,搜索,但我只需要在我的包装面板中可见的项目的缩略图。

编辑 – –

谢谢戴夫一个很好的答案。 你能详细说明“让它成为一个协会(又名导航财产)”

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

一旦我的UI访问它,它总是有一个空Thumbnail和错误。 关于movie.ThumbnailId的断点永远不会被击中。 我是否必须手动加载关联?

我认为你基本上是在问几个不同的事情:

  • 快速加载整个电影列表,以允许在UI中进行排序和过滤
  • 在UI中显示影片缩略图,但仅在它们滚动到视图中时才显示
  • 将内存使用量降至最低
  • 在应用程序启动后尽快显示UI

快速加载电影

首先,正如@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 Movies { get; set; } public DbSet Thumbnails { get; set; } } 

所以,要加载所有电影:

 public List 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(); } } 

此时,您将拥有一个Movie实体列表,其中填充了Thumbnail属性,但Thumbnail属性将为null因为您没有要求EF加载相关的Thumbnail实体。 此外,如果您稍后尝试访问Thumbnail属性,您将获得一个exception,因为MoviesContext不再在范围内。

获得Movie实体列表后,需要将它们转换为ViewModel。 我假设您的ViewModel实际上是只读的。

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

请注意,我们只是在这里存储缩略图ID,而不是填充Thumbnail 。 我稍后会谈到这一点。

分别加载缩略图,并缓存它们

所以,你已经加载了电影,但目前还没有加载任何缩略图。 您需要的是一种方法,它将从给定ID的数据库中加载单个Thumbnail实体。 我建议将它与某种缓存相结合,这样一旦你加载了一个缩略图,就可以将它保存在内存中一段时间​​。

 public sealed class ThumbnailCache { public ThumbnailCache(string connectionString) { _connectionString = connectionString; } readonly string _connectionString; readonly Dictionary _cache = new Dictionary(); 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; } } 

这显然是一个非常基本的缓存:检索是阻塞的,没有error handling,如果暂时没有检索到缩略图,为了保持内存使用率,缩略图应该从缓存中删除。

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

现在只有在访问Thumbnail属性时才会加载thumnail图像:如果图像已经在缓存中,它将立即返回,否则它将首先从数据库加载,然后存储在缓存中以备将来使用。

绑定性能

MovieViewModel集合绑定到视图中的控件的方式也会对感知的加载时间产生影响。 您希望尽可能延迟绑定,直到您的集合已填充为止。 这比绑定到空集合更快,然后一次一个地添加项目到集合。 你可能已经知道这一点,但我想我会提到它以防万一。

此MSDN页面( 优化性能:数据绑定 )有一些有用的提示。

这个由Ian Griffiths撰写的精彩系列博客文章( Too Much,Too Fast with WPF and Async )展示了各种绑定策略如何影响绑定列表的加载时间。

仅在视图中加载缩略图

现在是最困难的一点! 我们已经停止在应用程序启动时加载缩略图,但我们确实需要在某些时候加载它们。 加载它们的最佳时间是它们在UI中可见。 所以问题就变成了:如何检测缩略图何时在UI中可见? 这在很大程度上取决于您在视图中使用的控件(UI)。

我假设您将MovieViewModel的集合绑定到某种类型的ItemsControl ,例如ListBoxListView 。 此外,我假设你已经配置了某种DataTemplate (作为ListBox / ListView标记的一部分,或者在某个地方的ResourceDictionary ),它被映射到MovieViewModel类型。 DataTemplate一个非常简单的版本可能如下所示:

       

如果您正在使用ListBox ,即使您将其使用的面板更改为类似WrapPanel的面板, ListBoxControlTemplate包含一个ScrollViewer ,它提供滚动条并处理任何滚动。 在这种情况下,我们可以说缩略图在ScrollViewer的视口中出现时是可见的。 因此,我们需要一个自定义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作为ListBox模板的一部分:

                         

这里我们有一个包含单个ListBoxWindow 。 我们已经更改了ListBoxControlTemplate以包含自定义ScrollViewer ,并且内部是将布置项目的WrapPanel 。 在窗口的资源中,我们有DataTemplate ,它将用于显示每个MovieViewModel 。 这类似于前面介绍的DataTemplate ,但请注意我们不再绑定模板正文中的ImageSource属性:相反,我们使用基于IsInViewport属性的触发器,并在项目变为时设置绑定’可见’。 此绑定将导致调用MovieViewModel类的Thumbnail属性getter,这将从缓存或数据库加载缩略图图像。 请注意,绑定是父ListBoxItem上的属性, DataTemplate的标记将注入其中。

这种方法的唯一问题是,由于缩略图加载是在UI线程上完成的,因此滚动会受到影响。 修复此问题的最简单方法是修改MovieViewModel Thumbnail属性getter以返回“虚拟”缩略图,在单独的线程上调度对缓存的调用,然后获取该线程以相应地设置Thumbnail属性并引发PropertyChanged事件,从而确保绑定机制获得变化。 还有其他解决方案,但它们会显着提高复杂性:考虑这里提供的只是一个可能的起点。

每当您从EF请求实体时,它会自动加载所有标量属性(只有延迟加载的关联)。 将缩略图数据移动到它自己的实体,使其成为一个关联(也称为导航属性)并利用延迟加载。

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

现在,只有当UI访问ViewModel的Thumbnail属性时,才会从数据库加载缩略图数据。

您想一次加载所有图像吗? (我不会同时在屏幕上显示所有4000多个电影缩略图)。 我认为你可以实现这一目标的最简单方法是在需要时加载图像(例如,只显示正在显示的图像并在不显示时处理这些图像(以节省内存))。

这应该加快速度,因为你只需要实例化对象(并让ObservableCollection指向对象的内存地址),并且只在需要时再加载图像。

答案的提示是:

以块(页面)划分屏幕
在更改索引加载新图像后(您已经有了一个可观察的集合列表)

如果你仍遇到困难,我会尝试给出一个更明确的答案:)

祝好运

当应用于ItemsSource是动态的ListBox时,我遇到了问题。 在这种情况下,当修改源时,不一定是ScrollViewer,不会触发触发器并且不加载图像。

我的主要问题涉及在UniformGrid(未虚拟化)中延迟加载大量高图像。

为了解决这个问题,我在ListBoxItem上应用了一个Behavior。 我发现它也是一个很好的解决方案,因为你不必子类化ScrollViewer并更改ListBox的模板而只需更改ListBoxItem。

向项目添加行为:

 namespace behaviors { using System.Windows; using System.Windows.Controls; using System.Windows.Interactivity; using System.Windows.Media; public class ListBoxItemIsVisibleBehavior : Behavior { 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(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 { } 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); } } 

然后在控件中某处的资源中添加行为:

      

以及此资源的引用和ListBoxItem样式的触发器:

  

并引用ListBox中的样式:

   

在UniformGrid的情况下:

        

这需要很长时间,因为它会把你装载的所有内容都放到EF的Change Tracker中。 这就是在内存中跟踪的所有4000条记录,这可以理解地导致您的应用程序变慢。 如果您实际上没有在页面上进行任何编辑,我建议您在抓取Movies时使用.AsNoTracking()

如此:

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

可在此处找到MSDN链接。