I am building my first WPF application and I ran into some performance issues that I am not sure how to deal with. I have a list of around 500 items that I bind to UserControls using an ItemsControl in a Page. The data gets loaded into memory during the program startup and is displayed when the user opens the page. This already takes a few seconds but can be dealt with, however the data can also be reordered (e.g. ascending, descending) and filtered which takes around 0.6-1.3 seconds every time (depending on reordering method used). It seems to be caused by the UI parsing and layout and I was hoping this could be improved somehow. Here is the performance graph in case that helps.
And here is the code of the Item class and associated UserControl xaml (simplified):
public class Item : INotifyPropertyChanged
{
public String name { get; set; }
public String content { get; set; }
public BitmapImage image { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
private Visibility visibility = Visibility.Visible;
protected void OnPropertyChanged(string name)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
public void LoadImage()
{
var bitmap = new BitmapImage();
bitmap.BeginInit();
bitmap.DecodePixelWidth = 48;
bitmap.DecodePixelHeight = 48;
bitmap.UriSource = new Uri(somepath);
bitmap.EndInit();
bitmap.Freeze();
image = bitmap;
}
public Item()
{
LoadImage();
}
}
<UserControl
...
Visibility="{Binding Path=Visibility}">
<Grid Background="White">
<Image x:Name="ItemImage" Source="{Binding Path=image}" Width="64" Height="64"></Image>
<TextBlock x:Name="NameTextBlock" Text="{Binding Path=name}" FontSize="18px"></TextBlock>
<TextBlock x:Name="ContentTextBlock" Text="{Binding Path=content}" FontSize="14px" ></TextBlock>
</Grid>
</UserControl>
And here is the xaml and code of the Page which displays the Usercontrols:
<Page>
<StackPanel x:Name="RootStackPanel">
<Some static content>
<ScrollViewer x:Name="ItemScrollViewer" CanContentScroll="True" VerticalScrollBarVisibility="Auto" Width="auto" VerticalAlignment="Top" HorizontalAlignment="Stretch">
<ItemsControl x:Name="CustomItemsControl">
</ItemsControl>
</ScrollViewer>
</StackPanel>
</Page>
public partial class ItemsPage : Page
{
public static ItemsPage Instance = new ItemsPage();
public ObservableCollection<Item> collection { get; set; }
// called by a button on the page
private void ReorderItems()
{
// Simplified: Usually more ordering options e.g. OrderByDescending
List<Item> temp = new List<Item>(collection.OrderBy(item => item.name));
collection.Clear();
foreach (Item item in temp)
{
collection.Add(item);
}
}
public ItemsPage()
{
InitializeComponent();
collection = new ObservableCollection<Item>(ItemCollection.GetItems());
// getItems returns the list of items as List<>
FrameworkElementFactory factoryPanel = new FrameworkElementFactory(typeof(VirtualizingStackPanel));
factoryPanel.SetValue(VirtualizingStackPanel.IsItemsHostProperty, true);
factoryPanel.SetValue(VirtualizingStackPanel.IsVirtualizingProperty, true);
factoryPanel.SetValue(VirtualizingStackPanel.VirtualizationModeProperty, VirtualizationMode.Recycling);
ItemsPanelTemplate template = new ItemsPanelTemplate();
template.VisualTree = factoryPanel;
CustomItemsControl.ItemsPanel = template;
FrameworkElementFactory dataTemplateFactory = new FrameworkElementFactory(typeof(ItemUserControl));
DataTemplate dataTemplate = new DataTemplate();
dataTemplate.VisualTree = dataTemplateFactory;
CustomItemsControl.ItemTemplate = dataTemplate;
CustomItemsControl.ItemsSource = collection;
}
}
As can be seen I tried using Virtualization but I am not sure if I am using it correctly since I saw no measurable performance gain. The pictured reordering method is obviously not ideal since I alternatively tried reordering the members of the items instead and calling the PropertyChangedEventHandler on the bound members. This did improve performance by getting rid of the parsing step but still takes 600ms on average per reorder. Are there better methods to do that or should I be trying to simplify my UserControls instead?
When you use a ScrollViewer around ItemsControl Virtualization will not work.
extract ItemsControl from ScrollViewer.
Related
I created extension to IOrderedIEnumerable
which looks like this:
internal static ObservableCollection<T> ToObservableCollection<T>(this IOrderedEnumerable<T> list)
{
var observableCollection = new ObservableCollection<T>();
foreach (var p in list)
observableCollection.Add(p);
return observableCollection;
}
Also I have a page which looks like:
private ObservableCollection<BusStopName> _ListOfBusStopNames;
public BusStopsListPage()
{
this.InitializeComponent();
this.SetIsBackFromPageAllowed(true);
_ListOfBusStopNames = Timetable.Instance.BusStopsNames
.OrderBy(p => p.Name)
.ToObservableCollection<BusStopName>();
}
And in this page i have a listview which has binding to _ListOfBusStopNames
Timetable.Instance.BusStopsNames has 2812 entries.
When I'm navigating to this page, application memory is growing up to infinity numbers. It looks like (red line is moment when I navigated to this page):
In memory snapshot we can see that this thing is making this problem:
I don't have any idea what it can be. Do anyone have any idea?
When I change instance constructor to:
public BusStopsListPage()
{
this.InitializeComponent();
this.SetIsBackFromPageAllowed(true);
_ListOfBusStopNames = new ObservableCollection<BusStopName>();
}
everything works nice. Also, when I changed in this constructor manually List ( Timetable.Instance.BusStopsNames ) to ObservableCollection, it works great too.
EDIT
Hm.. i tried to make an example how to reproduce it, and i get to a solution for it. It is very confusing, so if someone can explain why this is happening it will be nice :)
Create UWP APP
Add in MainPage.xaml only this: <Frame Name="MainFrame" />
In code-behind put:this.Loaded += (s, e) => MainFrame.Navigate(typeof(BlankPage1));
Create new blank page with name BlankPage1
In View of BlankPage add this:
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<StackPanel>
<ListView ItemsSource="{x:Bind _ListOfTestClasses}">
<ListView.ItemTemplate>
<DataTemplate x:DataType="local:TestClass">
<Grid>
<TextBlock Text="{x:Bind Name}" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackPanel>
</Grid>
In code-behind put:
public sealed partial class BlankPage1 : Page
{
private List<TestClass> _List;
private ObservableCollection<TestClass> _ListOfTestClasses;
public BlankPage1() {
this.InitializeComponent();
GenerateTestData();
_ListOfTestClasses = _List.OrderBy(p => p.Name).ToObservableCollection<TestClass>();
}
private void GenerateTestData() {
_List = new List<TestClass>();
for(int i = 0; i < 2800; i++)
_List.Add(new TestClass() { Id = i, Name = Guid.NewGuid().ToString() });
}
}
public static class Extension {
public static ObservableCollection<T> ToObservableCollection<T>(this IOrderedEnumerable<T> list) {
var observableCollection = new ObservableCollection<T>();
foreach (var p in list)
observableCollection.Add(p);
return observableCollection;
}
}
public class TestClass {
public int Id { get; set; }
public string Name { get; set; }
}
Run the app
So.. you can see that app is lagging and memory is going taking more and more.. And now.. try to delete in BlankPage1 view StackPanel. And now run the app.
This is a common problem. The issue is the misuse of panels in XAML. A Grid will stretch to fit its container's boundaries and cascade this to its children. A StackPanel will only cascade the size of the opposing orientation.
For example, when you have <StackPanel Orientation="Vertical"/>, all children will receive the width of the parent Grid, but not the height. Because the orientation is vertical, the height will be infinite to allow the panel to stack its children based on their requested heights.
When you have <StackPanel Orientation="Horizontal"/>, all children will receive the height of the parent Grid, but not the width. Same reason as above. It needs to stack horizontally.
So..., when you have a <ListView/> inside a <StackPanel/>:
<StackPanel>
<ListView>...</ListView>
</StackPanel>
The StackPanel is telling the ListView to render its size with height = infinity. And therefore, all 2,800 items in your ListView are actually being rendered, not virtualized.
You have to avoid putting ListViews in StackPanels when the orientations are the same.
This should work:
<Grid>
<StackPanel Orientation="Horizontal">
<ListView>...</ListView>
</StackPanel>
</Grid>
... but it's silly.
I have what I believe to be a potentially unique situation.
My ListBox items consist of the following:
StackPanel
Image
ListItem
The ListItem and Image are inserted into the StackPanel, then the StackPanel is the inserted into the ListBox for each item in the array.
Now the challenging part comes in sorting the content by the ListItem's Content (text) as it's a child of the StackPanel. Naturally, the StackPanel does not contain a Content member, so using the below code fails.
this.Items.SortDescriptions.Add(new System.ComponentModel.SortDescription("Content",
System.ComponentModel.ListSortDirection.Ascending));
So I figured, what if I set my StackPanel's data context to my ListItem, then surely it will find it.
stackPanel.DataContext = this.Items;
However, that also fails.
I'm creating my ListItems programatically in the code behind, via data that is loaded in via Json.Net.
My goal here is to sort the items from A-Z, based on the Items Content. I would prefer to keep my current implementation (creating the data programatically) as it gives me more control over the visuals. Plus, it's only about 20 lines of code.
Is it possible to use SortDescriptions when the ListItem's content is a StackPanel ?
Thank you
PS: Only started with WPF today, but have been developing WinForms apps for nearly 2 months.
The WPF way to do it would be to bind your ListBox ItemsSource to an ObservableCollection containing your items.
You would then be able to sort your observableCollection liks so :
CollectionViewSource.GetDefaultView(YourObservableCollection).SortDescriptions.Add(new SortDescription("PropertyToSort", ListSortDirection.Ascending));
Here is a small project that highlights this :
XAML :
<Window x:Class="stackPanelTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:stackPanelTest"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid>
<ListBox ItemsSource="{Binding Items}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<Label Content="{Binding Image}" />
<TextBlock Text="{Binding Item.Content}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Window>
Code Behind :
public partial class MainWindow : Window
{
public ViewModel Items { get; set; } = new ViewModel();
public MainWindow()
{
InitializeComponent();
DataContext = this;
}
}
ViewModel :
public class ViewModel : ObservableCollection<ListItem>
{
public ViewModel()
{
populateItems();
CollectionViewSource.GetDefaultView(this).SortDescriptions.Add(new SortDescription("Item.Content", ListSortDirection.Ascending));
}
private void populateItems()
{
addOneItem(0, "zero");
addOneItem(1, "one");
addOneItem(2, "two");
addOneItem(3, "three");
addOneItem(4, "four");
}
private void addOneItem(int img, string content)
{
ListItem item = new ListItem();
item.Image = img;
item.Item = new SomeItem { Content = content };
Add(item);
}
}
public class ListItem
{
public int Image { get; set; }
public SomeItem Item { get; set; }
}
public class SomeItem
{
public string Content { get; set; }
}
I took the liberty of renaming your "ListItem" into a "SomeItem" class because I didn't know what it was.
Then I made a "ListItem" class which is used to contain a Image/SomeItem pair (which is what your ListBox is composed of).
Also I used an int instead of an actual image but that should be easily changable.
Here's a screenshot of what I get when executing this code :
Hope this helps, good luck.
PS : if your items values are susceptible to change, don't forget to implement INotifyPropertyChanged in "SomeItem" and "ListItem", otherwise the change won't be updated in your view.
Is it possible to use SortDescriptions when the ListItem's content is a StackPanel ?
No. You will have to implement the sorting logic yourself.
There is no easy way to apply custom sorting to the ItemCollection that is returned from the Items property of the ListBox so instead of adding items to this one you could add the items to a List<StackPanel> and sort this one.
You could still create the data programatically just as before.
Here is an example for you:
Code:
public partial class MainWindow : Window
{
private List<StackPanel> _theItems = new List<StackPanel>();
public MainWindow()
{
InitializeComponent();
//create the items:
StackPanel sp1 = new StackPanel();
ListBoxItem lbi1 = new ListBoxItem() { Content = "b" };
Image img1 = new Image();
sp1.Children.Add(lbi1);
sp1.Children.Add(img1);
_theItems.Add(sp1);
StackPanel sp2 = new StackPanel();
ListBoxItem lbi2 = new ListBoxItem() { Content = "a" };
Image img2 = new Image();
sp2.Children.Add(lbi2);
sp2.Children.Add(img2);
_theItems.Add(sp2);
StackPanel sp3 = new StackPanel();
ListBoxItem lbi3 = new ListBoxItem() { Content = "c" };
Image img3 = new Image();
sp3.Children.Add(lbi3);
sp3.Children.Add(img3);
_theItems.Add(sp3);
//sort the items by the Content property of the ListBoxItem
lb.ItemsSource = _theItems.OrderBy(x => x.Children.OfType<ListBoxItem>().FirstOrDefault().Content.ToString()).ToList();
}
}
XAML:
<ListBox x:Name="lb" />
I am using Entity Framework Code First
I have a Movie like so:
public class Movie
{
public byte[] Thumbnail { get; set; }
public int MovieId { get; set; }
}
And a Collection of Movies like so:
public class NorthwindContext : DbContext
{
public DbSet<Movie> Movies { get; set; }
}
I have a MovieViewModel like so:
public class MovieViewModel
{
private readonly Movie _movie;
public MovieViewModel(Movie movie)
{
_movieModel = movieModel;
}
public byte[] Thumbnail { get { return _movie.Thumbnail; } }
}
When my App starts:
public ObservableCollection<MovieViewModel> MovieVms =
new ObservableCollection<MovieViewModel>();
foreach (var movie in MyDbContext.Movies)
MovieVms.Add(new MovieViewModel(movie));
I have 4000 movies. This process takes 25 seconds. Is there a better/faster way to do this?
My main page uses the thumbnails like so, but to be clear this loading time happens before anything UI related:
MyView = new ListCollectionView(MovieVms);
<ListBox ItemsSource="{Binding MyView}" />
Also my memory usage goes through the roof. How should I be loading these images? I need a full collection of view models off the bat to enable sorting, filtering, searching, but I only need the thumbnails of the items visible in my wrap panel.
EDIT---
Thanks Dave for a great answer. Can you elaborate on "make it an association (aka navigation property)"
var thumbnail = new Thumbnail();
thumbnail.Data = movie.GetThumbnail();
Globals.DbContext.Thumbnails.Add(thumbnail);
Globals.DbContext.SaveChanges();
movie.ThumbnailId = thumbnail.ThumbnailId;
Globals.DbContext.SaveChanges();
I can run that code with no errors, but my property in my MovieViewModel
public new byte[] Thumbnail { get { return _movie.Thumbnail.Data; } }
always has a null Thumbnail and errors as soon as my UI accesses it. A breakpoint on movie.ThumbnailId is never hit. Do I have to load the association manually?
I think you are essentially asking how to do several different things:
Load the entire list of movies quickly, to allow for sorting and filtering in the UI
Display the movie thumbnails in the UI but only when they are scrolled into view
Keep memory usage to a minimum
Display the UI as quickly as possible after the application starts
Load the movies quickly
First off, as #Dave M's answer states, you need to split the thumbnail into a separate entity so that you can ask Entity Framework to load the list of movies without also loading the thumbnails.
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; }
}
So, to load all of the movies:
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();
}
}
At this point you will have a list of Movie entities where the ThumbnailId property is populated but the Thumbnail property will be null as you have not asked EF to load the related Thumbnail entities. Also, should you try to access the Thumbnail property later you will get an exception as the MoviesContext is no longer in scope.
Once you have a list of Movie entities, you need to convert them into ViewModels. I'm assuming here that your ViewModels are effectively read-only.
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!
}
Note that we're just storing the thumbnail ID here, and not populating the Thumbnail yet. I'll come to that in a bit.
Load thumbnails separately, and cache them
So, you've loaded the movies, but at the moment you haven't loaded any thumbnails. What you need is a method that will load a single Thumbnail entity from the database given its ID. I would suggest combining this with a cache of some sort, so that once you've loaded a thumbnail image you keep it in memory for a while.
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;
}
}
This is obviously a very basic cache: the retrieval is blocking, there is no error handling, and the thumbnails should really be removed from the cache if they haven't been retrieved for a while in order to keep memory usage down.
Going back to the ViewModel, you need to modify the constructor to take a reference to a cache instance, and also modify the Thumbnail property getter to retrieve the thumbnail from the cache:
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;
}
Now the thumnail images will only be loaded when the Thumbnail property is accessed: if the image was already in the cache, it will be returned immediately, otherwise it will be loaded from the database first and then stored in the cache for future use.
Binding performance
The way that you bind your collection of MovieViewModels to the control in your view will have an impact on perceived loading time as well. What you want to do whenever possible is to delay the binding until your collection has been populated. This will be quicker than binding to an empty collection and then adding items to the collection one at a time. You may already know this but I thought I'd mention it just in case.
This MSDN page (Optimizing Performance: Data Binding) has some useful tips.
This awesome series of blog posts by Ian Griffiths (Too Much, Too Fast with WPF and Async) shows how various binding strategies can affect the load times of a bound list.
Only loading thumbnails when in view
Now for the most difficult bit! We've stopped the thumbnails from loading when the application starts, but we do need to load them at some point. The best time to load them is when they are visible in the UI. So the question becomes: how do I detect when the thumbnail is visible in the UI? This largely depends on the controls you are using in your view (the UI).
I'll assume that you are binding your collection of MovieViewModels to an ItemsControl of some type, such as a ListBox or ListView. Furthermore, I'll assume that you have some kind of DataTemplate configured (either as part of the ListBox/ListView markup, or in a ResourceDictionary somewhere) that is mapped to the MovieViewModel type. A very simple version of that DataTemplate might look like this:
<DataTemplate DataType="{x:Type ...}">
<StackPanel>
<Image Source="{Binding Thumbnail}" Stretch="Fill" Width="100" Height="100" />
<TextBlock Text="{Binding Name}" />
</StackPanel>
</DataTemplate>
If you are using a ListBox, even if you change the panel it uses to something like a WrapPanel, the ListBox's ControlTemplate contains a ScrollViewer, which provides the scroll bars and handles any scrolling. In this case then, we can say that a thumbnail is visible when it appears within the ScrollViewer's viewport. Therefore, we need a custom ScrollViewer element that, when scrolled, determines which of its "children" are visible in the viewport, and flags them accordingly. The best way of flagging them is to use an attached Boolean property: in this way, we can modify the DataTemplate to trigger on the attached property value changing and load the thumbnail at that point.
The following ScrollViewer descendant (sorry for the terrible name!) will do just that (note that this could probably be done with an attached behaviour instead of having to subclass, but this answer is long enough as it is).
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));
}
}
}
Basically this ScrollViewer assumes that it's Content is a panel, and sets the attached IsInViewport property to true for those children of the panel that lie within the viewport, ie. are visible to the user. All that remains now is to modify the XAML for the view to include this custom ScrollViewer as part of the ListBox's template:
<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>
Here we have a Window containing a single ListBox. We've changed the ControlTemplate of the ListBox to include the custom ScrollViewer, and inside that is the WrapPanel that will layout the items. In the window's resources we have the DataTemplate that will be used to display each MovieViewModel. This is similar to the DataTemplate introduced earlier, but note that we are no longer binding the Image's Source property in the body of the template: instead, we use a trigger based on the IsInViewport property, and set the binding when the item becomes 'visible'. This binding will cause the MovieViewModel class's Thumbnail property getter to be called, which will load the thumbnail image either from the cache or the database. Note that the binding is to the property on the parent ListBoxItem, into which the markup for the DataTemplate is injected.
The only problem with this approach is that, as the thumbnail loading is done on the UI thread, scrolling will be affected. The easiest way to fix this would be to modify the MovieViewModel Thumbnail property getter to return a "dummy" thumbnail, schedule the call to the cache on a separate thread, then get that thread to set the Thumbnail property accordingly and raise a PropertyChanged event, thus ensuring the binding mechanism picks-up the change. There are other solutions but they would raise the complexity significantly: consider what is presented here as just a possible starting point.
Whenever you request an Entity from EF, it automatically loads all scalar properties (only associations are lazy loaded). Move the Thumbnail data to it's own Entity, make it an association (aka navigation property) and take advantage of the Lazy loading.
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; } }
}
Now the thumbnail data will only be loaded from the database when Thumbnail property of the ViewModel is accessed by the UI.
I was having issues with the answer when applied to a ListBox whose ItemsSource is dynamic. In that case, when the source is modified, the ScrollViewer is not necessarily, the trigger is not fired and the images are not loaded.
My main issue concerns the lazy loading lots of highres images in a UniformGrid (which is not virtualized).
To overcome this, I applied a Behavior on ListBoxItem. I find it is a good solution too because you do not have to subclass the ScrollViewer and change the ListBox's Template but only ListBoxItem's.
Add a behavior to your project :
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);
}
}
}
Then you would have to use this answer in order to add a behavior to your ListBoxItem style : How to add a Blend Behavior in a Style Setter
This leads to add a helper in your project :
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);
}
}
Then add the behavior in a resource somewhere in your control :
<UserControl.Resources>
<behaviors:Behaviors x:Key="behaviors" x:Shared="False">
<behaviors:ListBoxItemIsVisibleBehavior />
</behaviors:Behaviors>
</UserControl.Resources>
And a reference of this resource and a trigger to the style of your 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>
And reference the style in your ListBox :
<ListBox ItemsSource="{Binding Movies}"
Style="{StaticResource _ListBoxItemStyle}">
</ListBox>
In the case of a UniformGrid :
<ListBox ItemsSource="{Binding Movies}"
Style="{StaticResource _ListBoxItemStyle}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="5" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
Do you want to load all the images at once? (I recon not all 4000+ movie thumbnails will be shown on screen at the same time). The easiest way I think you could achieve this is to load the images when needed (e.g. only load the ones that are showing and dispose of these (to conserve memory) when not showing).
This should speed up things since you only have to instantiate the objects (and let the ObservableCollection points to the memory address of the objects) and again only load images when needed.
hints to an answer are:
Divide the screen in blocks (pages)
And upon changing the index load the new images (you've already got an observable collection list)
If you still run into difficulties I'll try to give a more clear answer :)
Goodluck
It's taking that long because it's putting everything you've loaded into EF's Change Tracker. That's all 4000 records being tracked in memory, which is understandably causing your app to slow down. If you're not actually doing any editing on the page, I suggest that you use .AsNoTracking() when you grab the Movies.
As so:
var allMovies = MyDbContext.Movies.AsNoTracking();
foreach (var movie in allMovies)
MovieVms.Add(new MovieViewModel(movie));
MSDN link on it can be found here.
I want to crate a simple flip-book animation in WPF. A flip-book animation just flips between multiple images (eg. classical animation or an animated gif). Note: I am not trying to create a Page Curl animation.
The set of images comes from a List collection, and is not know at compile time.
As such, I was thinking it would be good to start with an ItemsControl bound to my list of images, and then somehow cycle through them with a storyboard animation. However, I am running into a lot of issues with this, and feeling like there might be a better way.
Has anyone done this before, or have ideas for solutions? Thanks!
It might be easier to use a DispatcherTimer. Storyboards are good for animating properties, but they do not work so well with lists and ItemsControls.
I have tested the following approach, and it seems to work well. First put each image into a view-model wrapper with a Visibility property.
public class ImageViewModel : INotifyPropertyChanged
{
public string ImagePath { get; set; }
public Visibility Visibility
{
get { return _vis; }
set
{
_vis = value;
RaisePropertyChanged("Visibility");
}
}
private Visibility _vis = Visibility.Collapsed;
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged(string prop)
{
var handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(prop));
}
}
So your main view model would have a list of the image view models:
public class FlipBookViewModel
{
public List<ImageViewModel> FlipBookImages { get; private set; }
public FlipBookViewModel(string[] imagePaths)
{
FlipBookImages = imagePaths.Select(imagePath => new ImageViewModel
{ ImagePath = imagePath }
).ToList();
}
}
Then, in the page, simply place them on top of each other by using a Grid as the ItemsPanel:
<ItemsControl ItemsSource="{Binding FlipBookImages}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Image Source="{Binding ImagePath}" Visibility="{Binding Visibility}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Grid />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
To start the flip book, fire up the DispatcherTimer. Something like this:
var dispatcherTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(100) };
int i=0;
dispatcherTimer.Tick += (sender, args) => {
if (i>0)
FlipBookImages[i-1].Visibility = Visibility.Collapsed;
if (i>=FlipBookImages.Count)
dispatcherTimer.Stop();
else
FlipBookImages[i].Visibility = Visibility.Visible;
i++;
};
dispatcherTimer.Start();
I am having an absolute headache figuring this out. I badly need some help with this.
I have a listbox populated with items called with a public static void RSS feed class. Once the listbox populates with the databound items, I click on an item and it passes it through to my pivot page. However, when I flick left or right, all I get is the same image. That is my problem, and what I would like to have happen is if the user flicks left, it loads the previous RSS image. I would like it to also go to the next picture if the If the user scrolls right.
The community has been helpful in providing links to some things, or saying to not use the listbox, etc. However while I am new to all of this, I would just like concrete help with the code i have to achieve what I have in mind. It's nothing personal -- I just need to take babysteps with this before I get worked up with other things I have no clue about.
Here is all my relevant code.
Page 1 Xaml:
<ListBox x:Name="listbox" HorizontalContentAlignment="Stretch" ItemsSource="{Binding items}" SelectionChanged="listbox_SelectionChanged">
<ListBox.ItemTemplate>
<DataTemplate>
<Image Stretch="Fill" Height="60" Width="85" Source="{Binding Url}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Page1 C# Code Behind:
namespace Imaged
{
public partial class UserSubmitted : PhoneApplicationPage
{
private const string Myrssfeed = "http://feeds.bbci.co.uk/news/rss.xml";
public UserSubmitted()
{
InitializeComponent();
//This next function calls the RSS service, and returns the (items) and binds it to
//{listbox.ItemsSource = items;}. I am unable to reference the count of the items, or
//the array of it for some reason? The images load once the page loads.
RssService.GetRssItems(Myrssfeed, (items) => { listbox.ItemsSource = items; }, (exception) => { MessageBox.Show(exception.Message); }, null);
}
}
}
Once the listbox fills I am now trying to pass the selection by the user to a pivot page. I want that same image to show up in the pivot, and when the user pivots left or right, it shows the previous image or next image in the collection.
The Pivot Page I am trying to pass this to, XAML:
<Grid x:Name="LayoutRoot" Background="Transparent">
<!--Pivot Control-->
<controls:Pivot Title="{Binding Title}">
<!--Pivot item one-->
<controls:PivotItem x:Name="item1">
<Image Source="{Binding Url}"/> <!--I take it this is causing the pics to be the same?-->
</controls:PivotItem>
<!--Pivot item two-->
<controls:PivotItem x:Name="item2">
<Image Source="{Binding Url}"/>
</controls:PivotItem>
<!--Pivot item three-->
<controls:PivotItem x:Name="item3">
<Image Source="{Binding Url}"/>
</controls:PivotItem>
</controls:Pivot>
</Grid>
The RSS Service Class being called:
namespace WindowsPhone.Helpers
{
public class RssService
{
public static void GetRssItems(string rssFeed, Action<IList<RssItem>> onGetRssItemsCompleted = null, Action<Exception> onError = null, Action onFinally = null)
{
WebClient webClient = new WebClient();
// register on download complete event
webClient.OpenReadCompleted += delegate(object sender, OpenReadCompletedEventArgs e)
{
try
{
// convert rss result to model
IList<RssItem> rssItems = new List<RssItem>();
Stream stream = e.Result;
XmlReader response = XmlReader.Create(stream);
{
SyndicationFeed feeds = SyndicationFeed.Load(response);
foreach (SyndicationItem f in feeds.Items)
{
RssItem rssItem = new RssItem(f.Title.Text, f.Summary.Text, f.PublishDate.ToString(), f.Links[0].Uri.AbsoluteUri);
rssItems.Add(rssItem);
}
}
// notify completed callback
if (onGetRssItemsCompleted != null)
{
onGetRssItemsCompleted(rssItems);
}
}
finally
{
// notify finally callback
if (onFinally != null)
{
onFinally();
}
}
};
webClient.OpenReadAsync(new Uri(rssFeed));
}
}
}
and finally the RSSItem Class:
namespace WindowsPhone.Helpers
{
public class RssItem
{
public RssItem(string title, string summary, string publishedDate, string url)
{
Title = title;
Summary = summary;
PublishedDate = publishedDate;
Url = url;
// Get plain text from html
PlainSummary = HttpUtility.HtmlDecode(Regex.Replace(summary, "<[^>]+?>", ""));
}
public string Title { get; set; }
public string Summary { get; set; }
public string PublishedDate { get; set; }
public string Url { get; set; }
public string PlainSummary { get; set; }
}
}
Disclaimer: I don't think that binding this many items to a Pivot control is necessarily the right thing to do. Your mileage may vary, but I think a more virtualized solution would be more efficient. For my tests, it seemed to perform OK, but my little voice tells me that there be dragons here...
I recreated your project to the best of my ability and made some enhancements to get it to do what you wanted. Basically, the trick was using a ViewModel that was shared between both the main list page (UserSubmitted.xaml) and the page with the Pivot items on it (PivotPage1.xaml). By setting both page's DataContext property to the same object, we were able to bind both lists to the same source, thus eliminating the need to pass anything around.
In App.xaml.cs:
public static ViewData ViewModel { get; private set; }
private void Application_Launching(object sender, LaunchingEventArgs e)
{
// note: you should properly Tombstone this data to prevent unnecessary network access
ViewModel = new ViewData();
}
Here is how ViewData is defined:
public class ViewData : INotifyPropertyChanged
{
private string _FeedTitle;
private RssItem _SelectedItem = null;
private ObservableCollection<RssItem> _feedItems = new ObservableCollection<RssItem>();
private const string MyRssfeed = "http://feeds.bbci.co.uk/news/rss.xml";
public ViewData()
{
RssService.GetRssItems(
MyRssfeed,
(title, items) =>
{
App.Current.RootVisual.Dispatcher.BeginInvoke(() =>
{
FeedTitle = title;
FeedItems = new ObservableCollection<RssItem>(items);
});
},
(exception) =>
{
MessageBox.Show(exception.Message);
},
null);
}
public ObservableCollection<RssItem> FeedItems
{
get { return _feedItems; }
set
{
if (_feedItems == value)
return;
_feedItems = value;
NotifyPropertyChanged(this, new PropertyChangedEventArgs("FeedItems"));
}
}
public string FeedTitle
{
get { return _FeedTitle; }
set
{
if (_FeedTitle == value)
return;
_FeedTitle = value;
NotifyPropertyChanged(this, new PropertyChangedEventArgs("FeedTitle"));
}
}
public RssItem SelectedItem
{
get { return _SelectedItem; }
set
{
if (_SelectedItem == value)
return;
_SelectedItem = value;
NotifyPropertyChanged(this, new PropertyChangedEventArgs("SelectedItem"));
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(object sender, PropertyChangedEventArgs args)
{
if (PropertyChanged != null)
PropertyChanged(sender, args);
}
}
Once this is established, it's relatively easy to wire up both page's data context properties to App.ViewModel.
Last item was the scrolling and positioning of the selected item when navigating. When you select an item from the list page, the SelectedItem property of the shared ViewModel is bound to the SelectedItem property on the ListBox. After navigation to the details page, we have to find the selected item in the pivot and make it visible:
public PivotPage1()
{
InitializeComponent();
Loaded += (sender, e) =>
{
this.DataContext = App.ViewModel;
var selectedItem = App.ViewModel.SelectedItem;
var pi = ItemPivot.Items.First(p => p == selectedItem);
ItemPivot.SelectedItem = pi;
};
}
Setting the SelectedItem property of the Pivot control scrolls the pivot to the proper item and makes it visible.
The full sample is posted at http://chriskoenig.net/upload/imaged.zip if you want to see it in action.
If I got you correctly, you need to bind listbox in following way:
<ListBox ItemsSource="{Binding items}" SelectedItem="{Binding SelectedFeed, Mode=TwoWay}" />
And then bind Pivot in same way:
<Pivot ItemsSource="{Binding items}" SelectedItem="{Binding SelectedFeed, Mode=TwoWay}" />
Try the following for the pivot (based on Alex's code)
<Pivot ItemsSource="{Binding items}" SelectedItem="{Binding SelectedFeed, Mode=TwoWay}">
<Pivot.ItemTemplate>
<DataTemplate>
<Image Source="{Binding Url}"/>
</DataTemplate>
</Pivot.ItemTemplate>
</Pivot>
It assumes on the pivot page DataContext there is the same object "items" providing access to all the feeditems, and a property SelectedFeed which (as Alex mentioned) supports INotifyPropertyChanged