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

时间:2014-12-24 19:50:15

标签: c# wpf entity-framework ef-code-first

我正在使用Entity Framework Code First

我有这样的电影

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}" />

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

EDIT ---

谢谢Dave的回答。你能详细说明&#34;使它成为一个关联(又名导航属性)&#34;

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上的断点永远不会被击中。我是否必须手动加载关联?

5 个答案:

答案 0 :(得分:18)

我认为你基本上是在询问如何做几件事:

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

快速加载电影

首先,正如@Dave M的答案所述,您需要将缩略图拆分为单独的实体,以便您可以要求实体框架加载电影列表而不加载缩略图。

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不在范围内而获得异常。

获得电影实体列表后,您需要将它们转换为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。我稍后会谈到这一点。

单独加载缩略图,并缓存它们

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

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

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

绑定性能

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

此MSDN页面(Optimizing Performance: Data Binding)有一些有用的提示。

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

仅在视图中加载缩略图

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

我假设您将 MovieViewModel 的集合绑定到某种类型的ItemsControl,例如ListBoxListView 。此外,我假设您已经配置了某种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,即使您将其使用的面板更改为WrapPanelListBox&#39; s ControlTemplate也包含ScrollViewer,它提供滚动条并处理任何滚动。在这种情况下,我们可以说缩略图在ScrollViewer的视口中出现时是可见的。因此,我们需要一个自定义ScrollViewer元素,在滚动时,确定其中的哪个&#34;孩子&#34;在视口中可见,并相应地标记它们。标记它们的最好方法是使用附加的布尔属性:这样,我们可以修改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模板中:

<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的{​​{1}}以包含自定义ListBox,其中包含将布置项目的ScrollViewer。在窗口资源中,我们有WrapPanel,用于显示每个 MovieViewModel 。这类似于之前介绍的DataTemplate,但请注意,我们不再绑定模板正文中的DataTemplate Image属性:相反,我们使用的是触发基于Source属性,并在项目变为“可见”时设置绑定。此绑定将导致调用 MovieViewModel 类的IsInViewport属性获取器,这将从缓存或数据库加载缩略图图像。请注意,绑定是父Thumbnail上的属性,注入了ListBoxItem的标记。

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

答案 1 :(得分:5)

每当您从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属性时,只会从数据库加载缩略图数据。

答案 2 :(得分:1)

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

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

答案的提示是:

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

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

古德勒克

答案 3 :(得分:1)

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

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

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

向项目添加行为:

var result = new List<ValidationResult>();
if (!Validator.TryValidateObject(dto, ctx, result)) {
    // inspect `result` list for specific validation errors
}

然后,您必须使用此答案才能向ListBoxItem样式添加行为:How to add a Blend Behavior in a Style Setter

这导致在项目中添加一个帮助器:

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

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

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

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

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

并引用ListBox中的样式:

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

在UniformGrid的情况下:

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

答案 4 :(得分:0)

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

如此:

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

上面的MSDN链接可以找到here