What I wanted to detail in these upcoming posts was how I implemented a paginated view for the “card deck” on each platform. To sum it up, Android and iOS
provided better better controls out of the box to implement this functionality. Windows Phone 7 was kind of a pain so we will start with that first. So what was the end goal? The
end goal was a fluid image-gallery type experience that allowed users to swipe through the cards with touch rather than buttons. This
type of functionality is extremely common on mobile apps so I imagined there would be built in controls in the frameworks. Well I was half right..
WP7 Pivot
Windows Phone 7 has no obvious choice for this control, however with some template modification, I ended up using the Pivot.
The pivot control is a common element in metro style apps. The great thing about the pivot control is it inherits from ItemsControl,
which gave me all the properties and events that I need to get the functionality, most importantly the SelectedItem property and LoadedPivotItem and UnloadedPivotItem events.
So the layout code is fairly simple, a pivot control whose ItemsSource is databound to your viewmodel, and implements a custom style to get the layout the way you want it and binds the items to the viewmodel.
So in your code you can subscribe to the LoadedPivotItem and UnloadedPivotItem events to access your Items every time the user changes the cards
publicCardPage(){//apply datacontext to the page to bind to my viewmodel.this.DataContext=App.ViewModel;}privatevoidMainPivotLoadedItem(objectsender,PivotItemEventArgse){//get reference to controlvarpivot=senderasPivot();if(pivot==null)return;//get reference to SelectedItem as cast as my bound objectvarcard=pivot.SelectedItemasCardModel;//do some stuff if required. In my case, play a sound effect and narration fileif(card!=null)card.PlaySound();}privatevoidMainPivotUnLoadedItem(objectsender,PivotItemEventArgse){//get reference to controlvarpivot=senderasPivot();if(pivot==null)return;//get reference to SelectedItem as cast as my bound objectvarcard=pivot.SelectedItemasCardModel;//do some stuff if required. In my case, stop any playing sound as card is was unloadedif(card!=null)card.KillSounds();}
Pivot problems
In the end this solution is nice for its simplicity. First, the pivot control doesn’t provide the experience you would expect from
this type of application. Each item that is not in view is hidden, so without a header each item just kind of appears and animates into view. Secondly, the first problem
is exaggerated since the Pivot control does not support dragging, which I found out is highly annoying to some users. See the video below for an example of
what I am talking about.
Panorama
So after some a few hours of racking my brain trying to create my own control by sub-classing ListBox, or adding to ScrollViewer with a
StackPanel, I realized that another Microsoft.Phone.Controls class did exactly what I needed; the Panorama. Again, the Panorama required some templating
to get the look layout I was after.
So with this template, I changed the style of the panorama to minimize the massive header section to make it look more like a card. For the panorama,
everything is loaded at runtime, so there are no LoadedItem or UnloadedItem events to subscribe to. This makes getting the currently selected item
a little different than the Pivot control. The relevant event to handle for the Panorama is the SelectionChanged event. You can utilize it in your
code-behind, or preferably your viewmodel.
publicMainPage(){//apply datacontext to the page to bind to my viewmodel.this.DataContext=App.ViewModel;//subscribe to the selection changed eventMainPanorama.SelectionChanged+=newEventHandler(ListSelectionChanged);}privatevoidListSelectionChanged(objectsender,SelectionChangedEventArgse){CardnewItem;CardoldItem;//get the currently selected panorama item if(e.AddedItems.Count>0){currentItem=e.AddedItems[0]asCard;}//get previous item that was unselected if(e.RemovedItems.Count>0){oldItem=e.RemovedItems[0]asCard;}//do some on change some stuff. In my case, play some sounds.if(oldItem!=null)oldItem.KillSounds();if(newItem!=null)newItem.PlaySounds();}
So anyways that is pretty straightforward and is a much nicer experience for the user. I have seen many blog posts detailing the use of the Pivot control for
an items reel of sorts. IMHO, the panorama is nicer. See the difference in the video below…
Lazy loading
So the downside to the panorama control is that it utilizes a non-virtualized panel for its ItemsControl. What does this mean for us? It means that every Image in the view
is going to load and display on startup. That absolutely kills your memory. Microsoft recommends that you keep your peak memory usage below 90MB, I could easily surpass that with
around 30 images loading in the panorama without lazy-loading. So to get acceptable performance I had to implement some lazy-loading technique for the images in bound collection.
To implement this, I started with my datamodel.
[DataContractAttribute]publicclassCard:INotifyPropertyChanged{//this is the relative path to the card image. [DataMember]publicstringImageUri{get;set;} [DataMember]publicstringName{get;set;} [DataMember]publicstringPhoenetic{get;set;}//this is the ImageSource class that I will bind to in my view.privateImageSource_imgSource; [IgnoreDataMember]publicImageSourceImageSource{get{return_imgSource;}set{if(_imgSource==null||!_imgSource.Equals(value)){_imgSource=value;OnPropertyChanged("ImageSource");}}}publiceventPropertChangedEventHandlerPropertyChanged;protectedvoidOnPropertyChanged(stringpropertyName){if(PropertyChanged!=null)PropertyChanged(this,newPropertyChangedEventArgs(propertyName));}}
So now I have this nice data model that holds both the ImageURI and an ImageSource property that I can bind to. Let’s look at the data template for the Panorama one more time-
Now here lets take a look at my Viewmodel. If you remember from earlier, the important event handler for the Panorama control for my use was the SelectionChanged event.
On the SelectionChanged event, I get the index of the currently selected item, and essentially start a background worker to assign the ImageSource
property from the ImageUri property for the next two images. At the same time, I null the references to ImageSource for the previous two images. This allows the GC to free up their memory on the
next cycle.
publicvoidListSelectionChanged(objectsender,SelectionChangedEventArgse){// get current selected cardvarcurrentItem=e.AddedItems[0]asCard;// add two more card in worker thread LoadImageViewCardsAsync(currentItem);}/// <summary>/// Loads the next two card images in the deck and unloads the previous two/// </summary>publicvoidLoadImageViewCardsAsync(CardcurrentCard){varbgWorker=newBackgroundWorker();bgWorker.DoWork+=BgWorkerDoWork;varindex=Dinosaurs.IndexOf(currentCard);bgWorker.RunWorkerAsync(index);}privatevoidBgWorkerDoWork(objectsender,DoWorkEventArgse){varindex=(int)e.Argument;for(vari=index;i<(index+2);i++){if(i>Dinosaurs.Count-1)continue;vari1=i;_dispatcher.BeginInvoke(delegate{varbmp=newBitmapImage(newUri(Dinosaurs[i1].ImageUri,UriKind.Relative));Dinosaurs[i1].ImageSource=bmp;});}//remove old images from UI by nulling the referencefor(vari=index-2;i>=(index-3);i--){if(i<0)continue;vari1=i;_dispatcher.BeginInvoke(delegate{Dinosaurs[i1].ImageSource=null;});}}
So what was the result of this? For about 57 cards being loaded at a time, I was able to reduce my memory usage from 100MB to ~50MB while
viewing a flash card deck. If you run the Memory Analyzer you will see that it is the image loads on the UI that kill the memory, so this method
removes that barrier. To go even further, you could implement a buffer collection that is bound to the Panorama and dynamically load and unload full
items, however without the image my objects are very small so that was overkill for me.
Drawbacks
The main drawback with using a panorama control is the initial load time for a page utilizing a panorama. Check out this telerik post about
the subject. This drawback was acceptable for me for the vastly better user experience vs. a pivot control.
FlipView to the rescue
I wanted to add this in- Windows 8 fixes this problem and introduces a control called FlipView. Awesome control that can be virtualized and databound with built-in touch and button navigation.
Next post I will talk about my Android implementation!