There’s a number of photo viewing applications for Windows 8, including the built-in ‘Photos’ app and other apps on the Windows 8 Store like ‘Gallery HD’ and a few others.
These are all good apps but, for my own use, none of them quite do what I want in terms of providing a simple, fast-and-fluid way to navigate around my photos and so I thought I’d experiment with making a simple app which;
- Allowed access to my photos library.
- Allowed access to other folders that I as a user nominate.
- Presents a simple folder navigation view and thumbnails of the photos in the current folder.
- Presents a simple picture-by-picture view to allow me to to slide easily through all the photos in a folder.
and I want all that to be reasonably performant both on my Core i5 based Samsung slate device and also on my Surface RT ARM based device.
It seems simple but I suspected that there’d be a few interesting bits and pieces along the way and so I thought I’d try it out and write it up here.
Step 1 – Displaying Some Folders and Navigating Around Them
I started simple. I bashed out a little bit of XAML as below;
<Page x:Class="App4.MainPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:App4" xmlns:cmn="using:App4.Common" xmlns:ctrl="using:App4.Controls" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"> <Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}"> <Grid.ColumnDefinitions> <ColumnDefinition Width="*" /> <ColumnDefinition Width="3*" /> </Grid.ColumnDefinitions> <ListView HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" ItemsSource="{Binding Folders}" SelectionMode="None"> <ListView.ItemTemplate> <DataTemplate> <Button Command="{Binding Command}" CommandParameter="{Binding}" Template="{x:Null}"> <TextBlock Text="{Binding Name}" Style="{StaticResource SubheaderTextStyle}" Margin="0,0,0,3" /> </Button> </DataTemplate> </ListView.ItemTemplate> </ListView> </Grid> </Page>
and so this is dependent on a property called Folders which is bound as the ItemsSource of that ListView and each item is displayed using a Name property and that item can be acted upon because there’s a button which is bound to a Command property. I made up a couple of classes;
namespace App4 { using System.Windows.Input; class CommandableItem { public CommandableItem(string name, ICommand command) { this.Name = name; this.Command = command; } public virtual string Name { get; private set; } public ICommand Command { get; private set; } } }
and so a CommandableItem is simply an item that has both a name that can be displayed and a command that can be invoked and then a;
namespace App4 { using System.Windows.Input; using Windows.Storage; class FolderItem : CommandableItem { public FolderItem(StorageFolder folder, ICommand command) : base(null, command) { this.StorageFolder = folder; } public override string Name { get { return (this.StorageFolder.Name); } } public StorageFolder StorageFolder { get; private set; } } }
FolderItem is a CommandableItem which has an underlying StorageFolder that it pulls the name from. With that in play, I put together a simple ViewModel class;
namespace App4 { using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Threading.Tasks; using System.Windows.Input; using App4.Common; using Windows.Storage; using Windows.Storage.Search; class ViewModel : BindableBase { public ViewModel() { this._folderInvokedCommand = new SimpleCommand( new Action<object>(this.OnFolderInvoked)); this._upItemEntry = new CommandableItem[] { new CommandableItem("Go Up", new SimpleCommand(this.OnUp)) }; this._topLevelFolders = new ObservableCollection<CommandableItem>(); this._parentFolders = new Stack<StorageFolder>(); this.PopulateDetailsAsync(); } public ObservableCollection<CommandableItem> Folders { get { return (this._folders); } private set { base.SetProperty(ref this._folders, (ObservableCollection<CommandableItem>)value); } } public bool IsTop { get { return (this._currentFolder == null); } } public void AddTopLevelFolder(StorageFolder folder) { this._topLevelFolders.Add(new FolderItem(folder, this._folderInvokedCommand)); } private ObservableCollection<CommandableItem> TopLevelFolders { get { return (this._topLevelFolders); } } async void PopulateDetailsAsync() { if (this.IsTop) { this.Folders = this.TopLevelFolders; } else { this.QueryCurrentFolders(); } } async Task QueryCurrentFolders() { this.Folders = null; var queryOptions = new QueryOptions(CommonFolderQuery.DefaultQuery); queryOptions.FolderDepth = FolderDepth.Shallow; queryOptions.IndexerOption = IndexerOption.UseIndexerWhenAvailable; var folderQuery = this._currentFolder.CreateFolderQueryWithOptions(queryOptions); var folders = await folderQuery.GetFoldersAsync(); var folderEntries = folders.Select(f => (CommandableItem)(new FolderItem(f, this._folderInvokedCommand))); this.Folders = new ObservableCollection<CommandableItem>( this._upItemEntry.Union(folderEntries) ); } void OnFolderInvoked(object param) { if (!this.IsTop) { this._parentFolders.Push(this._currentFolder); } this._currentFolder = ((FolderItem)param).StorageFolder; this.PopulateDetailsAsync(); } void OnUp() { this._currentFolder = (this._parentFolders.Count != 0) ? this._parentFolders.Pop() : null; this.PopulateDetailsAsync(); } ICommand _folderInvokedCommand; Stack<StorageFolder> _parentFolders; ObservableCollection<CommandableItem> _folders; StorageFolder _currentFolder; ObservableCollection<CommandableItem> _topLevelFolders; CommandableItem[] _upItemEntry; } }
and so the idea of this is;
- Create one of these ViewModel classes.
- Add in top level folders such as the PicturesLibrary via the AddTopLevelFolder method.
- Set it as the DataContext for the UI so that the Folders property is picked up by the ListView.
and then from there the buttons in the ListView should ‘do the right thing’ to allow the user to move around the folders that are in the list.
As an aside, SimpleCommand is just a simple implementation of ICommand – it’s nothing more than a class which implements ICommand by executing a delegate passed to the class.
There would need to be additional UI to allow the user to select a new folder, add it to the TopLevelFolders collection and there would need to be functionality to persist that set of folders and also ensure access to those folders across instances of the app – this will come later but, for now, this gives me a navigable list that works reasonably well.
Step 2 – Displaying Some Thumbnails
To get some thumbnails displayed, I added a GridView over on the right hand side of the screen to display thumbnails of images;
<Page x:Class="App4.MainPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:App4" xmlns:cmn="using:App4.Common" xmlns:ctrl="using:App4.Controls" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"> <Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}"> <Grid.ColumnDefinitions> <ColumnDefinition Width="*" /> <ColumnDefinition Width="3*" /> </Grid.ColumnDefinitions> <ListView HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" ItemsSource="{Binding Folders}" SelectionMode="None"> <ListView.ItemTemplate> <DataTemplate> <Button Command="{Binding Command}" CommandParameter="{Binding}" Template="{x:Null}"> <TextBlock Text="{Binding Name}" Style="{StaticResource SubheaderTextStyle}" Margin="0,0,0,3" /> </Button> </DataTemplate> </ListView.ItemTemplate> </ListView> <GridView Grid.Column="1" ItemsSource="{Binding Files}" SelectionMode="None"> <GridView.ItemTemplate> <DataTemplate> <Image Width="192" Height="132" Source="{Binding Thumbnail,Converter={StaticResource converter}}" Stretch="UniformToFill" HorizontalAlignment="Center" VerticalAlignment="Center" /> </DataTemplate> </GridView.ItemTemplate> </GridView> </Grid> </Page>
where the converter that is being used around line 39 above is a simple converter that goes from a StorageItemThumbnail to an ImageSource as below;
namespace App4.Common { using System; using Windows.Storage.FileProperties; using Windows.UI.Xaml.Data; using Windows.UI.Xaml.Media; using Windows.UI.Xaml.Media.Imaging; class ThumbnailToImageConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, string language) { BitmapImage image = null; if (value != null) { if (value.GetType() != typeof(StorageItemThumbnail)) { throw new ArgumentException("Expected a thumbnail"); } if (targetType != typeof(ImageSource)) { throw new ArgumentException("What are you trying to convert to here?"); } StorageItemThumbnail thumbnail = (StorageItemThumbnail)value; image = new BitmapImage(); image.SetSource(thumbnail); } return (image); } public object ConvertBack(object value, Type targetType, object parameter, string language) { throw new NotImplementedException(); } } }
and that previous XAML is largely dependent on a list of Files that is data-bound into the GridView’s ItemsSource and so I added some extra bits into my ViewModel to support that;
namespace App4 { using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Threading.Tasks; using System.Windows.Input; using App4.Common; using Windows.Storage; using Windows.Storage.BulkAccess; using Windows.Storage.FileProperties; using Windows.Storage.Search; class ViewModel : BindableBase { public ViewModel() { this._folderInvokedCommand = new SimpleCommand( new Action<object>(this.OnFolderInvoked)); this._upItemEntry = new CommandableItem[] { new CommandableItem("Go Up", new SimpleCommand(this.OnUp)) }; this._topLevelFolders = new ObservableCollection<CommandableItem>(); this._parentFolders = new Stack<StorageFolder>(); this.PopulateDetailsAsync(); } public ObservableCollection<CommandableItem> Folders { get { return (this._folders); } private set { base.SetProperty(ref this._folders, (ObservableCollection<CommandableItem>)value); } } public object Files { get { return (this._files); } private set { base.SetProperty(ref this._files, value); } } public bool IsTop { get { return (this._currentFolder == null); } } public void AddTopLevelFolder(StorageFolder folder) { this._topLevelFolders.Add(new FolderItem(folder, this._folderInvokedCommand)); } private ObservableCollection<CommandableItem> TopLevelFolders { get { return (this._topLevelFolders); } } async void PopulateDetailsAsync() { if (this.IsTop) { this.Folders = this.TopLevelFolders; } else { // TODO: the hope of this code is that by awaiting the folder population // we then go back to the UI and update our folder list while // concurrently kicking off a query for the files in the current folder. // I'm not sure the reality quite matches that though. await this.QueryCurrentFolders(); this.QueryCurrentFiles(); } } async Task QueryCurrentFolders() { this.Folders = null; var queryOptions = new QueryOptions(CommonFolderQuery.DefaultQuery); queryOptions.FolderDepth = FolderDepth.Shallow; queryOptions.IndexerOption = IndexerOption.UseIndexerWhenAvailable; var folderQuery = this._currentFolder.CreateFolderQueryWithOptions(queryOptions); var folders = await folderQuery.GetFoldersAsync(); var folderEntries = folders.Select(f => (CommandableItem)(new FolderItem(f, this._folderInvokedCommand))); this.Folders = new ObservableCollection<CommandableItem>( this._upItemEntry.Union(folderEntries) ); } async Task QueryCurrentFiles() { var queryOptions = new QueryOptions(CommonFileQuery.DefaultQuery, new string[] { ".jpg" }); queryOptions.FolderDepth = FolderDepth.Shallow; queryOptions.SetThumbnailPrefetch(ThumbnailMode.PicturesView, 192, ThumbnailOptions.ResizeThumbnail); var fileQuery = this._currentFolder.CreateFileQueryWithOptions(queryOptions); FileInformationFactory f = new FileInformationFactory( fileQuery, ThumbnailMode.PicturesView, 192, ThumbnailOptions.ResizeThumbnail); this.Files = f.GetVirtualizedFilesVector(); } void OnFolderInvoked(object param) { this.ResetFilesFolders(); if (!this.IsTop) { this._parentFolders.Push(this._currentFolder); } this._currentFolder = ((FolderItem)param).StorageFolder; this.PopulateDetailsAsync(); } void OnUp() { this.ResetFilesFolders(); this._currentFolder = (this._parentFolders.Count != 0) ? this._parentFolders.Pop() : null; this.PopulateDetailsAsync(); } void ResetFilesFolders() { this.Files = this.Folders = null; } object _files; ICommand _folderInvokedCommand; Stack<StorageFolder> _parentFolders; ObservableCollection<CommandableItem> _folders; StorageFolder _currentFolder; ObservableCollection<CommandableItem> _topLevelFolders; CommandableItem[] _upItemEntry; } }
and this all works quite nicely and displays folders and images within the current folder;
What I’d then want to be able to do is tap on an image and display it in a FlipView which displays it full-screen and allows navigation to previous/next images. That’s where things get ‘interesting’.
Step 3 – Adding a FlipView
If you look at the previous code, I took different approaches to building my list of folders versus building my list of files.
- For the list of folders, I built up a query and then executed in the function QueryCurrentFolders and I took each StorageFolder and put it into an ObservableCollection having wrapped one of my own objects (FolderItem) around it so that I could add an implementation of ICommand for ‘what to do’ when the user clicks on a folder.
- For the list of files, I built up a query in the QueryCurrentFiles function and then I asked WinRT to give me a virtualised data source via the call FileInformationFactory.GetVirtualizedFilesVector()
My hope/expectation is that there will be more files than there are folders so it’s perhaps better to try and use ‘system mechanisms’ to build that list of files with this call to GetVirtualizedFilesVector() rather than build up my own list as I do for folders in QueryCurrentFolders which builds the whole list up front.
I don’t really know how that mechanism works. All I know is that GetVirtualizedFilesVector() returns me an object which can work as an ItemsSource on a control like a ListView, GridView, FlipView and so I use it here. If I poke around in the debugger a little I can see the object that I am returned implements IObservableVector<object>.
I also know that each individual item delivered by that virtualized list will be a FileInformation object and that has a Thumbnail property so I can pick that up via binding and convert it into something that the XAML Image control can handle which is an ImageSource.
That’s great if I want to display a thumbnail, but what if I want to display the actual image itself? A full-sized image? How can I get that displayed using this scheme?
I can see that others have struggled with this question and haven’t really seemed to get an answer.
My first inclination was to think that this just involved writing a different converter.
I have a converter that goes from FileInformation.Thumbnail to an ImageSource so why not go from FileInformation itself to ImageSource? What I find is;
- You can try and write that converter so that it uses FileInformation.Path and then tries to construct a BitmapImage from that path used as a Uri. I don’t find that to work.
- You can try and write that converter so that it tries to open the file stream and construct a BitmapImage that way but the problem here is that a converter needs to work synchronously whereas on WinRT you have to open and read files asynchronously so I don’t think that can work without a hack that I wasn’t prepared to contemplate.
- You can try and cheat by asking for ‘very large thumbnails’ from Windows so that you can continue to use the FileInformation.Thumbnail property and forget all about trying to really read the file yourself but WinRT throws exceptions if you ask for very large thumbnails so that doesn’t work either.
So, it didn’t seem possible to use GetVirtualizedFilesVector() in a pure data-bound way but I did come up with a solution and that solution involved adding an attached property to an Image. Given that I have an Image and a FileInformation object and that I can’t bind one straight to the other, perhaps I can add a property that does know how to make that work as in;
class ImageProperties : DependencyObject { public static DependencyProperty FileSourceProperty = DependencyProperty.RegisterAttached("FileInformationSource", typeof(FileInformation), typeof(ImageProperties), new PropertyMetadata(null, OnFileSourceChanged)); public static void SetFileInformationSource(Image image, FileInformation value) { image.SetValue(FileSourceProperty, value); } public static FileInformation GetFileInformationSource(Image image) { return ((FileInformation)image.GetValue(FileSourceProperty)); } static async void OnFileSourceChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args) { Image image = (Image)sender; FileInformation newValue = (FileInformation)args.NewValue; if (newValue == null) { image.Source = null; } else { using (var stream = await newValue.OpenReadAsync()) { BitmapImage bitmapImage = new BitmapImage(); bitmapImage.SetSource(stream); image.Source = bitmapImage; } } } }
and then using that exact same Files property to bind as the ItemsSource of my FlipView I can define that FlipView in XAML to do the right thing;
<FlipView Grid.Column="1" ItemsSource="{Binding Files}"> <FlipView.ItemTemplate> <DataTemplate> <Grid> <Image Stretch="Uniform" local:ImageProperties.FileInformationSource="{Binding}"> </Image> <StackPanel HorizontalAlignment="Right" VerticalAlignment="Bottom"> <TextBlock Text="{Binding Name}" /> <TextBlock Text="{Binding ImageProperties.CameraModel}" /> <TextBlock Text="{Binding ImageProperties.CameraManufacturer}" /> <TextBlock Text="{Binding ImageProperties.Height}" /> <TextBlock Text="{Binding ImageProperties.Width}" /> <TextBlock Text="{Binding ImageProperties.DateTaken}" /> </StackPanel> </Grid> </DataTemplate> </FlipView.ItemTemplate> </FlipView>
but there’s a problem.
Step 4 – Trying to Stop my FlipView Solution Being Horrible
I was feeling fairly good about my FlipView solution until I happened to look in TaskManager and notice that after browsing just a few photos my app was using more than 500MB of memory.
The images that I was browsing were pretty big JPG images running at about 5MB each in the file system but the way I figure it it’s hard to see how loading 3 or so of those in memory could cause a bloat of 500MB.
I spent quite a long time thinking about this and ran a few experiments.
- The first thing I did was to embed 3 of these images into my project and then make a new FlipView which simply flipped through those 3 images at full size. I didn’t see any major memory bloat from doing that.
- The next thing I did was to leave the code that loaded the image files but removed lines 30 to 32 above so that the image didn’t go on the screen. I didn’t see any major memory bloat from that either.
- I did a bit of debugging to see how many images the FlipView seemed to be demanding concurrently from the data source and it looked to be 3 which kind of made sense to me based on a current/next/previous model.
- I wrote a separate little test app which simply used a file picker to open one of these images and then used similar code to what I have above in order to read the file and make it the source of an Image. This simple code did seem to grow memory usage by about 100MB at the point where I loaded up an image.
So, it seems like I do see a major memory growth if I simply read the image from the file and hand the stream over to the Image control and, presumably, this is something to do with the size of the image uncompressed versus the size of the image compressed.
So, this harmless looking bit of code;
FileOpenPicker picker = new FileOpenPicker(); picker.FileTypeFilter.Add(".jpg"); var file = await picker.PickSingleFileAsync(); var stream = await file.OpenReadAsync(); BitmapImage img = new BitmapImage(); img.SetSource(stream); myImage.Source = img;
with a 5MB image selected from the file dialog can cause my memory usage to ramp up by about 100MB which seems a little ‘unfair’ but I’m not sure what I can do about it. One option might be to accept that I won’t allow zoom in/out and try to scale the image based on the screen size;
FileOpenPicker picker = new FileOpenPicker(); picker.FileTypeFilter.Add(".jpg"); var file = await picker.PickSingleFileAsync(); var stream = await file.OpenReadAsync(); BitmapDecoder decoder = await BitmapDecoder.CreateAsync(stream); double width = decoder.PixelWidth; double height = decoder.PixelHeight; if (width > Window.Current.Bounds.Width) { width = Window.Current.Bounds.Width; height = height * (width / decoder.PixelWidth); } if (height > Window.Current.Bounds.Height) { width = width * (Window.Current.Bounds.Height / height); height = Window.Current.Bounds.Height; } InMemoryRandomAccessStream outStream = new InMemoryRandomAccessStream(); BitmapEncoder encoder = await BitmapEncoder.CreateForTranscodingAsync( outStream, decoder); encoder.BitmapTransform.ScaledHeight = (uint)height; encoder.BitmapTransform.ScaledWidth = (uint)width; await encoder.FlushAsync(); outStream.Seek(0); BitmapImage img = new BitmapImage(); img.SetSource(outStream); myImage.Source = img;
And if I run this code on my 1366×768 screen I find that my images are bloating my memory size by about 30MB rather than 100MB so that’s a step forward but that transcoding seems to take a bit of time which means my hope of being ‘fast and fluid’ looks like it’s heading out of the window.
Ultimately, this is the solution I’m going for so far though.
Step 5 – Trying to Stop My FlipView Solution Being Horrible (Again)
There’s another problem with the way that I’m using the FlipView. It runs something like this;
- My user displays a list of folders and a GridView which shows them a bunch of thumbnails. As it happens, my FlipView is binding its collection of items to that same list of files that is driving those thumbnails but my FlipView isn’t on the screen yet.
- When the user clicks on a thumbnail, they expect to jump into the FlipView with that particular item selected so I data-bind the SelectedItem on the FlipView to the FileInformation that the user selects.
- What then happens is something like this;
- The FlipView is made visible on the screen and it more than likely has a new ItemsSource so it seems to have some logic that causes it to load items 0,1,2 ready for display.
- This results in my imaging code above starting to do work to resize images 0, 1, 2.
- My other code then comes along and tells the FlipView to change its SelectedItem to (e.g.) item 22 so now the FlipView switches to start loading items 21,22,23 (or possibly 22,23,24) which causes my image code to start loading those images too.
- Now my image code has asynchronous operations in flight for maybe 6 images when I only really need 3.
- Depending on the order of completion of the async operations, I can now get into a situation where I either have a flicker as the correct image replaces an incorrect one or, even worse, if the ordering goes against me I can display the wrong image.
I tried a few solutions here with most of them revolving around trying to get the FlipView to avoid loading items 0,1,2 when it first gets a new value for its ItemsSource property but I didn’t seem to have much success with that so I ended up having to try and put that logic elsewhere.
What I did was to modify my ImageProperties class such that if it has an out-standing asynchronous load for an image to put into one specific Image control and then it receives another request for the same control it will attempt to cancel the first async operation and only process the subsequent one for that Image.
That is, I changed my code for that attached property to look like;
namespace App4.Common { using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Windows.Graphics.Imaging; using Windows.Storage.BulkAccess; using Windows.Storage.Streams; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Media.Imaging; class ImageProperties : DependencyObject { static Dictionary<Image, CancellationTokenSource> _imageLoads; public static DependencyProperty FileSourceProperty = DependencyProperty.RegisterAttached("FileInformationSource", typeof(FileInformation), typeof(ImageProperties), new PropertyMetadata(null, OnFileSourceChanged)); static ImageProperties() { _imageLoads = new Dictionary<Image, CancellationTokenSource>(); } public static void SetFileInformationSource(Image image, FileInformation value) { image.SetValue(FileSourceProperty, value); } public static FileInformation GetFileInformationSource(Image image) { return ((FileInformation)image.GetValue(FileSourceProperty)); } static async void OnFileSourceChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args) { Image image = (Image)sender; FileInformation newValue = (FileInformation)args.NewValue; image.Source = null; if (_imageLoads.ContainsKey(image)) { // We're already loading this thing. CancellationTokenSource source = _imageLoads[image]; // TODO: this throws on me? source.Cancel(); } if (newValue != null) { CancellationTokenSource source = new CancellationTokenSource(); _imageLoads[image] = source; try { await ResizeAsync(image, newValue, source); } catch (TaskCanceledException) { } } } static async Task ResizeAsync(Image image, FileInformation fileInformation, CancellationTokenSource source) { using (var stream = await fileInformation.OpenReadAsync().AsTask(source.Token)) { BitmapDecoder decoder = await BitmapDecoder.CreateAsync(stream).AsTask(source.Token); double width = decoder.PixelWidth; double height = decoder.PixelHeight; if (width > Window.Current.Bounds.Width) { width = Window.Current.Bounds.Width; height = height * (width / decoder.PixelWidth); } if (height > Window.Current.Bounds.Height) { width = width * (Window.Current.Bounds.Height / height); height = Window.Current.Bounds.Height; } InMemoryRandomAccessStream outStream = new InMemoryRandomAccessStream(); BitmapEncoder encoder = await BitmapEncoder.CreateForTranscodingAsync( outStream, decoder).AsTask(source.Token); encoder.BitmapTransform.ScaledHeight = (uint)height; encoder.BitmapTransform.ScaledWidth = (uint)width; await encoder.FlushAsync().AsTask(source.Token); outStream.Seek(0); BitmapImage bitmapImage = new BitmapImage(); bitmapImage.SetSource(outStream); image.Source = bitmapImage; _imageLoads.Remove(image); } } } }
Step 7 – Moving Away From That Attached Property
The solution that I came up with in the last few steps certainly ‘works’ for me but one of its problems is that it means that the knowledge of when an image is loading and when it has finished loading is hidden away in the ImageProperties class. What that means is that if I have some other UI (e.g. the image details like width, height and so on) then it’s hard for me to know when it’s appropriate to display that UI because it’s hard to know when the image has actually finished loading.
So, I took the same code but built it into a slightly different approach. I implemented my own dependency object that I can place into the item template for each data item displayed by my FlipView and then I can use that object to load the images and resize them and then bind other controls to that object.
With that in mind, I created this little class and moved most of the code from my ImageProperties class into it;
using System; using System.ComponentModel; using System.Threading; using System.Threading.Tasks; using Windows.Graphics.Imaging; using Windows.Storage.BulkAccess; using Windows.Storage.Streams; using Windows.UI.Xaml; using Windows.UI.Xaml.Media.Imaging; namespace App4.Common { public class ImageLoader : DependencyObject, INotifyPropertyChanged { public static DependencyProperty FileInformationSourceProperty = DependencyProperty.Register("FileInformationSource", typeof(FileInformation), typeof(ImageLoader), new PropertyMetadata(null, OnFileInformationSourceChanged)); public ImageLoader() { } public bool HasLoaded { get { return (this._hasLoaded); } private set { this._hasLoaded = value; this.FirePropertyChanged("HasLoaded"); } } public BitmapImage Image { get { return (_image); } private set { this._image = value; this.FirePropertyChanged("Image"); } } void FirePropertyChanged(string property) { if (this.PropertyChanged != null) { this.PropertyChanged(this, new PropertyChangedEventArgs(property)); } } public FileInformation FileInformationSource { get { return ((FileInformation)base.GetValue(FileInformationSourceProperty)); } set { base.SetValue(FileInformationSourceProperty, value); } } static void OnFileInformationSourceChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args) { ImageLoader loader = (ImageLoader)sender; loader.Reload(); } void Reload() { this.Image = null; this.HasLoaded = false; if (this._tokenSource != null) { this._tokenSource.Cancel(); this._tokenSource = null; } if (this.FileInformationSource == null) { this.HasLoaded = true; } else { this.ResizeAsync(); } } async void ResizeAsync() { this._tokenSource = new CancellationTokenSource(); try { using (var stream = await this.FileInformationSource.OpenReadAsync().AsTask( this._tokenSource.Token)) { BitmapDecoder decoder = await BitmapDecoder.CreateAsync(stream).AsTask( this._tokenSource.Token); double width = decoder.PixelWidth; double height = decoder.PixelHeight; if (width > Window.Current.Bounds.Width) { width = Window.Current.Bounds.Width; height = height * (width / decoder.PixelWidth); } if (height > Window.Current.Bounds.Height) { width = width * (Window.Current.Bounds.Height / height); height = Window.Current.Bounds.Height; } InMemoryRandomAccessStream outStream = new InMemoryRandomAccessStream(); BitmapEncoder encoder = await BitmapEncoder.CreateForTranscodingAsync( outStream, decoder).AsTask(this._tokenSource.Token); encoder.BitmapTransform.ScaledHeight = (uint)height; encoder.BitmapTransform.ScaledWidth = (uint)width; await encoder.FlushAsync().AsTask(this._tokenSource.Token); outStream.Seek(0); BitmapImage bitmapImage = new BitmapImage(); bitmapImage.SetSource(outStream); this.Image = bitmapImage; this.HasLoaded = true; this._tokenSource = null; } } catch (TaskCanceledException) { } } CancellationTokenSource _tokenSource; bool _hasLoaded; BitmapImage _image; public event PropertyChangedEventHandler PropertyChanged; } }
with that in place, I can change my FlipView’s template such that it includes one of these ImageLoader objects and that object will bind to the current FileInformation object for the image that’s being displayed and my Image can then bind to the ImageLoader.Image property as below;
<FlipView Grid.Column="1" ItemsSource="{Binding Files}" SelectedItem="{Binding SelectedFile,Mode=TwoWay}"> <FlipView.ItemTemplate> <DataTemplate> <Grid> <Grid.Resources> <cmn:ImageLoader x:Key="imageLoader" FileInformationSource="{Binding}" /> </Grid.Resources> <StackPanel VerticalAlignment="Center" HorizontalAlignment="Center" Orientation="Horizontal"> <TextBlock Style="{StaticResource SubheaderTextStyle}" Text="resizing" VerticalAlignment="Center" /> <ProgressRing Margin="12,0,0,0" Width="24" Height="24" Foreground="White" IsActive="True" VerticalAlignment="Center" /> </StackPanel> <Grid HorizontalAlignment="Center" VerticalAlignment="Center" DataContext="{StaticResource imageLoader}"> <Image Stretch="None" Source="{Binding Image}"> </Image> <StackPanel HorizontalAlignment="Right" VerticalAlignment="Bottom" Visibility="{Binding HasLoaded, Converter={StaticResource boolToVisibilityConverter}}"> <TextBlock Text="{Binding FileInformationSource.Name}" /> <TextBlock Text="{Binding FileInformationSource.ImageProperties.CameraModel}" /> <TextBlock Text="{Binding FileInformationSource.ImageProperties.CameraManufacturer}" /> <TextBlock Text="{Binding FileInformationSource.ImageProperties.Height}" /> <TextBlock Text="{Binding FileInformationSource.ImageProperties.Width}" /> <TextBlock Text="{Binding FileInformationSource.ImageProperties.DateTaken}" /> </StackPanel> </Grid> </Grid> </DataTemplate> </FlipView.ItemTemplate> </FlipView>
and the advantage over the previous solution is that I can now have other UI (in this case the StackPanel) which is aware of when the image has or hasn’t loaded by binding to the HasLoaded property of my ImageLoader.
Step 8 – Invoking a Command from a GridView’s ItemClicked
When someone taps on a photo in my GridView display, I want to replace the GridView with a FlipView. This means tapping in to the ItemClick event on a GridView and also switching on the flag IsItemClickEnabled on the control.
I’d prefer to work with bindings and ICommand implementations but the GridView doesn’t really do anything around ICommand and so I derive my own GridView and add a little bit of awareness of how to invoke a command. I could go and grab an MVVM helper library at this point but, for this code, I felt it was overkill;
public class GridViewWithClickCommand : GridView { public static DependencyProperty ClickCommandProperty = DependencyProperty.Register("ClickCommand", typeof(ICommand), typeof(GridViewWithClickCommand), null); public ICommand ClickCommand { get { return ((ICommand)base.GetValue(ClickCommandProperty)); } set { base.SetValue(ClickCommandProperty, value); } } public GridViewWithClickCommand() { this.ItemClick += OnItemClick; this.IsItemClickEnabled = true; } void OnItemClick(object sender, ItemClickEventArgs e) { if ((this.ClickCommand != null) && (this.ClickCommand.CanExecute(e.ClickedItem))) { this.ClickCommand.Execute(e.ClickedItem); } } }
and now I have the basics of a GridView that supports a simple command for when a user clicks on an item.
Step 9 – Access to Additional Photo Folders
I added a button to my ‘UI’ such that it’s possible for the user to be able to add a folder beyond their pictures library.
and I added an AppBar to show that button;
<Page.BottomAppBar> <AppBar> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition /> <ColumnDefinition /> </Grid.ColumnDefinitions> <StackPanel Orientation="Horizontal"> <Button Command="{Binding AddFolderCommand}" Style="{StaticResource AddAppBarButtonStyle}"/> </StackPanel> <StackPanel Grid.Column="1" HorizontalAlignment="Right" Orientation="Horizontal" /> </Grid> </AppBar> </Page.BottomAppBar>
and that’s bound to an ICommand implementation on my ViewModel which raises a file selection dialog (which probably needs to be moved from the ViewModel and perhaps into a service that can be replaced with different implementations) but it’s ok for the moment;
async void OnAddInvoked() { FolderPicker picker = new FolderPicker(); picker.FileTypeFilter.Add("*"); var folder = await picker.PickSingleFolderAsync(); this.AddTopLevelFolder(folder); }
and this works fine but it will suffer from the problem that if my application shuts down then this access won’t be remembered and that shutdown could be something as simple as an application suspend/terminate that Windows might force on me.
I need to make sure that if the user gives the application a folder then that choice is persisted and that the app will be able to get back to any such folders and access them in the future. Enter the future access list which I can add my folder to when the user chooses it;
FolderPicker picker = new FolderPicker(); picker.FileTypeFilter.Add("*"); var folder = await picker.PickSingleFolderAsync(); this.AddTopLevelFolder(folder); StorageApplicationPermissions.FutureAccessList.AddOrReplace( Guid.NewGuid().ToString(), folder);
and I can use the same list to ‘rehydrate’ that set of folders when the app starts up into my ViewModel;
ViewModel vm = new ViewModel(); vm.AddTopLevelFolder(KnownFolders.PicturesLibrary); foreach (var entry in StorageApplicationPermissions.FutureAccessList.Entries) { var folder = await StorageApplicationPermissions.FutureAccessList.GetFolderAsync( entry.Token); vm.AddTopLevelFolder(folder); }
Step 10 – Putting It Together
I thought I’d draw together the pieces above into a simple, app with just 2 controls, one of which displays the folder list and the grid view of thumbnails and the other to display the photos themselves in a FlipView. I haven’t done too much around the UI but I’ve tidied up the code a little and I might actually take this just a few steps further and put the app into the Store if I take it so far as to think about snapped views, device orientation and so on.
But Was It Simple?
I’d argue that there’s a few things that I had to think about here which should be simpler on the platform. A lot of apps want to display an image and, many times, that image is going to be big so large images need to be thought about. Also, a lot of apps are going to want to flip through an image set and take advantage of virtualisation so I think that should be a bit easier too.
Of course, it might be that there’s easier ways of doing what I did here so feel free to add comments and I’ll update the post if I’ve over-complicated things that didn’t need to be so complicated
And Is It ‘Fast and Fluid’?
Partially I’m not unhappy with the performance of navigating folders and of building thumbnails but, right now, the performance of loading photos that are large (i.e. larger than the screen) isn’t so great because it involves a resize operation.
I think if I really wanted to tackle that then I’d need to look to move away from binding items into my FlipView and start to take closer control of when images are loaded and resized – it’s definitely achievable but requires more effort than I’ve put so far into surfacing those images where I was trying to be as declarative as I could and use as much binding as I could.