Cocktails (Quick Jump Grid)

0
204

At a recent party, I walked up to the bar and asked for a Roy Rogers (Coke with grenadine and a maraschino cherry). The bartender said “Sure,” had a quick conversation with the other bartender, hesitated for a moment, whipped out his iPhone, and then swiped around for a bit before finally asking me, “What’s a Roy Rogers?”

The Cocktails app contains an alphabetized list of over 1,100 cocktails (including nonalcoholic drinks, such as Roy Rodgers). Each one links to a recipe (and other relevant information) from About.com. A list this long requires something more than a simple list box. Therefore, Cocktails uses a quick jump grid, the alphabetized list with tiles that jump to each letter that is featured in the People and Music + Videos hubs.

QuickJumpGrid Versus LongListSelector

The Silverlight for Windows Phone Toolkit includes a control called LongListSelector that can be used as a quick jump grid. At its core, it’s a list box with performance optimizations for large lists of items, complete with smoother scrolling, UI virtualization, and data virtualization. In addition, it supports arbitrary grouping of its items with headers that can be tapped to bring up the list of groups. The groups can be anything, as demonstrated in Figure 18.1.

FIGURE 18.1 A hypothetical version of Cocktails that uses LongListSelector to group drinks by descriptive categories.
FIGURE 18.1 A hypothetical version of Cocktails that uses LongListSelector to group drinks by descriptive categories.

The Cocktails app, however, does not use LongListSelector. Instead, it uses a simpler but more limited user control created in this chapter called QuickJumpGrid. QuickJumpGrid isn’t nearly as flexible as LongListSelector, and it only supports alphabetic categorization. If the alphabetic categorization is what you want, however, QuickJumpGrid is simpler to use because you only need to give it a flat list of key/value pairs. (LongListSelector is much more complicated to fill with data, although the Silverlight for Windows Phone Toolkit includes a good sample.) QuickJumpGrid also mimics the behavior of the quick jump grid used by the built-in apps more faithfully with appropriate animations and a grid that doesn’t needlessly scroll.

A large portion of this chapter is dedicated to showing how to the QuickJumpGrid control is built, as it helps highlight some of this chapter’s lessons.

The Main Page

The Cocktails app’s main page, whose XAML is shown in Listing 18.1, contains just the status bar, the app name, and a quick jump grid filled with the list of cocktails. The quick jump grid starts out looking like an alphabetized list box with a header tile for each unique starting letter (and a # for all digits), as seen in Figure 18.2.

Tapping any of the letter tiles (or # tile) animates in the grid shown at the beginning of this chapter. Tapping any of the tiles on this grid jumps to that part of the list. Figure 18.3 shows the main page after the user brings up the grid and taps on the letter v.

FIGURE 18.2 The main page showcases the quick jump grid.
FIGURE 18.2 The main page showcases the quick jump grid.
FIGURE 18.3 Jumping to the letter v with the quick jump grid avoids scrolling through over 1,000 previous cocktails!
FIGURE 18.3 Jumping to the letter v with the quick jump grid avoids scrolling through over 1,000 previous cocktails!

The User Interface

The XAML for the main page is shown in Listing 18.1.

LISTING 18.1 MainPage.xaml—The User Interface for the Cocktails App’s Main Page

[code]

<phone:PhoneApplicationPage
x:Class=”WindowsPhoneApp.MainPage”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:phone=”clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone”
xmlns:shell=”clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone”
xmlns:local=”clr-namespace:WindowsPhoneApp”
x:Name=”Page” Loaded=”MainPage_Loaded” SupportedOrientations=”Portrait”
shell:SystemTray.IsVisible=”True”>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”*”/>
</Grid.RowDefinitions>
<!– Mini-header –>
<TextBlock Text=”COCKTAILS” Margin=”24,16,0,12”
Style=”{StaticResource PhoneTextTitle0Style}”/>
<!– Quick jump grid –>
<local:QuickJumpGrid x:Name=”QuickJumpGrid” Grid.Row=”1” Margin=”24,0,0,0”
Page=”{Binding ElementName=Page}”
ItemSelected=”QuickJumpGrid_ItemSelected”/>
</Grid>
</phone:PhoneApplicationPage>

[/code]

The QuickJumpGrid user control must be given an instance of the host page via its Page property, so it can automatically hide the status bar and application bar (if the page uses them) when showing the 4×7 grid of alphabet tiles shown at the beginning of this chapter. Otherwise, these would get in the way, as no elements can ever appear on top of them. This page uses data binding to set Page.

The Code-Behind

Listing 18.2 contains the code-behind for the main page, which handles the interaction with the QuickJumpGrid user control.

LISTING 18.2 MainPage.xaml.cs—The Code-Behind for the Cocktails App’s Main Page

[code]

using System;
using System.Collections.Generic;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using Microsoft.Phone.Controls;
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
{
bool listInitialized = false;
public MainPage()
{
InitializeComponent();
// Add no more than 10 items so the initial UI comes up quickly
for (int i = 0; i < 10 && i < Data.Cocktails.Length; i++)
this.QuickJumpGrid.Add(new KeyValuePair<string, object>(
Data.Cocktails[i].Name, Data.Cocktails[i]));
// Refresh the list
this.QuickJumpGrid.Update();
}
void MainPage_Loaded(object sender, RoutedEventArgs e)
{
if (!this.listInitialized)
{
// Now add the remaining items
for (int i = 10; i < Data.Cocktails.Length; i++)
this.QuickJumpGrid.Add(new KeyValuePair<string, object>(
Data.Cocktails[i].Name, Data.Cocktails[i]));
// Refresh the list
this.QuickJumpGrid.Update();
// Only do this once
this.listInitialized = true;
}
}
void QuickJumpGrid_ItemSelected(object sender, SelectionChangedEventArgs e)
{
if (e.AddedItems.Count == 0)
return;
// Each item in the list is a key/value pair, where each value is a Cocktail
KeyValuePair<string, object> item =
(KeyValuePair<string, object>)e.AddedItems[0];
// Show details for the chosen item
this.NavigationService.Navigate(new Uri(“/DetailsPage.xaml?url=” +
HttpUtility.UrlEncode((item.Value as Cocktail).Url.AbsoluteUri),
UriKind.Relative));
}
}
}

[/code]

Notes:

  • QuickJumpGrid exposes a few simple methods. The ones used here are Add and Update. These methods are a bit unorthodox for a Silverlight control, but it keeps the code in this chapter simple and performant. LongListSelector exposes a more flexible set of APIs.
  • Add adds an item to the list as a key/value pair. You cannot specify where to add the item in the list, as it is automatically alphabetized. The string key is used for sorting the list (and deciding which letter bucket each item belongs to) and the object value can be anything. This app uses Cocktail objects defined as follows in Cocktail.cs:

    [code]
    public class Cocktail
    {
    public string Name { get; private set; }
    public Uri Url { get; private set; }
    public Cocktail(string name, Uri url)
    {
    this.Name = name;
    this.Url = url;
    }
    public override string ToString()
    {
    return this.Name;
    }
    }
    [/code]

    The list of over 1,100 Cocktail objects is defined as an array in Data.cs:

    [code]
    public class Data
    {
    public static readonly Cocktail[] Cocktails = {
    new Cocktail(“#26 Cocktail”, new Uri(
    “http://cocktails.about.com/od/cocktailrecipes/r/number_26cktl.htm”)),
    new Cocktail(“50-50”, new Uri(
    “http://cocktails.about.com/od/cocktailrecipes/r/50_50_mrtni.htm”)),

    };
    }
    [/code]

  • Update refreshes the control with its current set of data. Until you call Update, any Add/Remove calls have no visual effect. This is done for performance reasons. In Listing 15.1, Update is called after adding the first 10 items, to make the list appear quickly. It is then called only one more time, after the entire list has been populated. This is all done on the UI thread, as the underlying collection is not threadsafe. However, populating the list is fairly fast because the visuals aren’t updated until the end.
  • When an item is selected, this page navigates to the details page, passing along the URL of the details page from About.com. The code calls HttpUtility.UrlEncode to ensure that the About.com URL can be passed as a query parameter to the DetailsPage.xaml URL without its colon and slashes interfering with URL parsing done by the system.

The Details Page

FIGURE 18.4 The details for any cocktail is shown directly from About.com, thanks to the WebBrowser control.
FIGURE 18.4 The details for any cocktail is shown directly from About.com, thanks to the WebBrowser control.

The details page, shown in Figure 18.4 for the Jack-o-Lantern Punch drink, simply hosts a WebBrowser control to show the relevant page from About.com inline. Its XAML is shown in Listing 18.3, and its code-behind is shown in Listing 18.4.

LISTING 18.3 DetailsPage.xaml—The User Interface for the Cocktails App’s Details Page

[code]

<phone:PhoneApplicationPage
x:Class=”WindowsPhoneApp.DetailsPage”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:phone=”clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone”
xmlns:toolkit=”clr-namespace:Microsoft.Phone.Controls;
➥assembly=Microsoft.Phone.Controls.Toolkit”
SupportedOrientations=”PortraitOrLandscape” Background=”White”>
<Grid>
<phone:WebBrowser x:Name=”WebBrowser” Navigating=”WebBrowser_Navigating”
Navigated=”WebBrowser_Navigated”/>
<toolkit:PerformanceProgressBar x:Name=”ProgressBar” VerticalAlignment=”Top”/>
</Grid>
</phone:PhoneApplicationPage>

[/code]

LISTING 18.4 DetailsPage.xaml.cs—The Code-Behind for the Cocktails App’s Details Page

[code]

using System;
using System.Windows;
using System.Windows.Navigation;
using Microsoft.Phone.Controls;
namespace WindowsPhoneApp
{
public partial class DetailsPage : PhoneApplicationPage
{
public DetailsPage()
{
InitializeComponent();
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Navigate to the correct details page
this.WebBrowser.Source = new Uri(this.NavigationContext.QueryString[“url”]);
}
void WebBrowser_Navigating(object sender, NavigatingEventArgs e)
{
this.ProgressBar.Visibility = Visibility.Visible;
// Avoid a performance problem by only making it indeterminate when needed
this.ProgressBar.IsIndeterminate = false;
}
void WebBrowser_Navigated(object sender, NavigationEventArgs e)
{
// Avoid a performance problem by only making it indeterminate when needed
this.ProgressBar.IsIndeterminate = true;
this.ProgressBar.Visibility = Visibility.Collapsed;
}
}
}

[/code]

Notes:

  • In Listing 18.3, the page is given an explicit white background to prevent a jarring experience when the web browser is shown (which is white until a page loads).
  • The web browser’s Source property is set to the URL passed via the query string, causing the appropriate navigation. Note that HttpUtility.UrlDecode did not need to be called, because the query string is automatically decoded when retrieved via NavigationContext.QueryString.
  • Because all the state for this page is passed via the query string (just the relevant URL), this page behaves appropriately if deactivated and then reactivated. Because the query string is preserved on reactivation, the page is still populated correctly.
  • A progress bar is shown while the page is still loading. This applies not only to the initial navigation, but any navigation caused by the user clicking links inside the web page.
  • Instead of using the ProgressBar control that ships with Silverlight, this page uses a PerformanceProgressBar control that ships with the Silverlight for Windows Phone Toolkit. This fixes some performance problems with the built-in ProgressBar control when its indeterminate (dancing dots) mode is used. Whether you use ProgressBar or PerformanceProgressBar, you should still only set IsIndeterminate to true when the progress bar is shown to avoid performance problems.

Indeterminate progress bars continue to do a lot of work on the UI thread, even when hidden!

When a standard progress bar’s IsIndeterminate property is set to true, it performs a complicated animation that unfortunately involves significant work on the UI thread.What comes as a shock to most is that this work still happens even when the progress bar’s Visibility is set to Collapsed! The easiest workaround for this is to set IsIndeterminate to false whenever you set Visibility to Collapsed, and temporarily set it to true when Visibility is Visible. In addition, if you use PerformanceProgressBar from the Silverlight for Windows Phone Toolkit instead of ProgressBar, the animation runs on the compositor thread rather than the UI thread.

Some websites are not yet formatted appropriately for Windows Phone 7!

At the time of writing, sites such as About.com present their desktop-formatted pages to a Windows phone rather than their mobile-formatted pages. It may take a while for many websites to recognize the user agent string passed by Internet Explorer on Windows Phone 7, because the platform is so new.

The QuickJumpGrid User Control

The QuickJumpGrid user control, used in Listing 18.1

The User Interface

Listing 18.5 contains the XAML for the QuickJumpGrid user control used by the main page.

LISTING 18.5 QuickJumpGrid.xaml—The User Interface for the Quick Jump Grid

[code]

<UserControl x:Class=”WindowsPhoneApp.QuickJumpGrid”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:local=”clr-namespace:WindowsPhoneApp”>
<!– Add two items to the user control’s resource dictionary –>
<UserControl.Resources>
<!– An empty storyboard used as a timer from code-behind –>
<Storyboard x:Name=”DelayedPopupCloseStoryboard” Duration=”0:0:.15”
Completed=”DelayedPopupCloseStoryboard_Completed”/>
<!– A frame-rooted popup shown by code-behind –>
<Popup x:Name=”Popup” Width=”480” Height=”800”>
<Canvas Width=”480” Height=”800”>
<Rectangle Fill=”{StaticResource PhoneBackgroundBrush}” Opacity=”.68”
Width=”480” Height=”800”/>
<Canvas x:Name=”QuickJumpTiles”
MouseLeftButtonUp=”QuickJumpTiles_MouseLeftButtonUp”>
<local:QuickJumpTile Text=”#” Canvas.Left=”24” Canvas.Top=”24”/>
<local:QuickJumpTile Text=”a” Canvas.Left=”135” Canvas.Top=”24”/>
<local:QuickJumpTile Text=”b” Canvas.Left=”246” Canvas.Top=”24”/>
<local:QuickJumpTile Text=”c” Canvas.Left=”357” Canvas.Top=”24”/>
<local:QuickJumpTile Text=”d” Canvas.Left=”24” Canvas.Top=”135”/>
<local:QuickJumpTile Text=”e” Canvas.Left=”135” Canvas.Top=”135”/>
<local:QuickJumpTile Text=”f” Canvas.Left=”246” Canvas.Top=”135”/>
<local:QuickJumpTile Text=”g” Canvas.Left=”357” Canvas.Top=”135”/>
<local:QuickJumpTile Text=”h” Canvas.Left=”24” Canvas.Top=”246”/>
<local:QuickJumpTile Text=”i” Canvas.Left=”135” Canvas.Top=”246”/>
<local:QuickJumpTile Text=”j” Canvas.Left=”246” Canvas.Top=”246”/>
<local:QuickJumpTile Text=”k” Canvas.Left=”357” Canvas.Top=”246”/>
<local:QuickJumpTile Text=”l” Canvas.Left=”24” Canvas.Top=”357”/>
<local:QuickJumpTile Text=”m” Canvas.Left=”135” Canvas.Top=”357”/>
<local:QuickJumpTile Text=”n” Canvas.Left=”246” Canvas.Top=”357”/>
<local:QuickJumpTile Text=”o” Canvas.Left=”357” Canvas.Top=”357”/>
<local:QuickJumpTile Text=”p” Canvas.Left=”24” Canvas.Top=”468”/>
<local:QuickJumpTile Text=”q” Canvas.Left=”135” Canvas.Top=”468”/>
<local:QuickJumpTile Text=”r” Canvas.Left=”246” Canvas.Top=”468”/>
<local:QuickJumpTile Text=”s” Canvas.Left=”357” Canvas.Top=”468”/>
<local:QuickJumpTile Text=”t” Canvas.Left=”24” Canvas.Top=”579”/>
<local:QuickJumpTile Text=”u” Canvas.Left=”135” Canvas.Top=”579”/>
<local:QuickJumpTile Text=”v” Canvas.Left=”246” Canvas.Top=”579”/>
<local:QuickJumpTile Text=”w” Canvas.Left=”357” Canvas.Top=”579”/>
<local:QuickJumpTile Text=”x” Canvas.Left=”24” Canvas.Top=”690”/>
<local:QuickJumpTile Text=”y” Canvas.Left=”135” Canvas.Top=”690”/>
<local:QuickJumpTile Text=”z” Canvas.Left=”246” Canvas.Top=”690”/>
</Canvas>
</Canvas>
</Popup>
</UserControl.Resources>
<!– The list box –>
<ListBox x:Name=”ListBox” SelectionChanged=”ListBox_SelectionChanged”>
<ListBox.ItemTemplate>
<DataTemplate>
<local:QuickJumpItem Margin=”0,6” KeyValuePair=”{Binding}”
local:Tilt.IsEnabled=”True”/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</UserControl>

[/code]

Notes:

  • There are two main pieces to the quick jump grid—a list box to contain the alphabetized items, and a canvas that contains the 27 tiles in a grid formation. The list box makes use of a QuickJumpItem user control to render each item (the key/value pairs seen in Listing 18.2) and the canvas uses 27 instances of a QuickJumpTile user control.
  • This control uses a frame-rooted popup, defined as a resource, to contain the canvas with 27 tiles. A popup is used so it is able to cover the entire screen regardless of where the QuickJumpGrid is placed on the page. (In this app’s main page, for example, the top of the QuickJumpGrid is 91 pixels down the page due to the status bar and “COCKTAILS” header, but the popup is able to cover everything.)
  • The empty storyboard is used from code-behind as a handy way to do delayed work. Once DelayedPopupCloseStoryboard.Begin is called, DelayedPopupCloseStoryboard_ Completed will be called .15 seconds later (the duration of the storyboard).

The Code-Behind

Listing 18.6 contains the code-behind for the QuickJumpGrid user control.

LISTING 18.6 QuickJumpGrid.xaml.cs—The Code-Behind for the Quick Jump Grid

[code]

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;
namespace WindowsPhoneApp
{
public partial class QuickJumpGrid : UserControl
{
List<KeyValuePair<string, object>> items =
new List<KeyValuePair<string, object>>();
bool isPageStatusBarVisible;
bool isPageAppBarVisible;
public event SelectionChangedEventHandler ItemSelected;
public QuickJumpGrid()
{
InitializeComponent();
//
// HACK: Transfer the popup’s content to a new popup to avoid a bug
//
// Remove the popup’s content
UIElement child = this.Popup.Child;
this.Popup.Child = null;
// Create a new popup with the same content
Popup p = new Popup { Child = child };
// Make this the new popup member
this.Popup = p;
}
// Add the item to the sorted list, using the key for sorting
public void Add(KeyValuePair<string, object> item)
{
// Find where to insert it
int i = 0;
while (i < this.items.Count && string.Compare(this.items[i].Key,
item.Key, StringComparison.InvariantCultureIgnoreCase) <= 0)
i++;
this.items.Insert(i, item);
}
// Remove the items from the list
public void Remove(KeyValuePair<string, object> item)
{
this.items.Remove(item);
}
// Refresh the list box with the current collection of items
public void Update()
{
this.ListBox.ItemsSource = GetAllItems();
}
// Return the list of items, with header items injected
// in the appropriate spots
IEnumerable<KeyValuePair<string, object>> GetAllItems()
{
char currentBucket = ‘’;
foreach (KeyValuePair<string, object> item in this.items)
{
char bucket = CharHelper.GetBucket(item.Key);
if (bucket != currentBucket)
{
// This is a new bucket, so return the header item.
// The key is the letter (or #) and the value is null.
yield return new KeyValuePair<string, object>(bucket.ToString(), null);
currentBucket = bucket;
}
// Return the real item
yield return item;
}
}
// Return a list of only header items
IEnumerable<KeyValuePair<string, object>> GetUsedLetterItems()
{
char currentBucket = ‘’;
foreach (KeyValuePair<string, object> item in this.items)
{
char bucket = CharHelper.GetBucket(item.Key);
if (bucket != currentBucket)
{
// This is a new bucket, so return the header item.
// The key is the letter (or #) and the value is null.
yield return new KeyValuePair<string, object>(bucket.ToString(), null);
currentBucket = bucket;
}
}
}
// A Page dependency property
public static readonly DependencyProperty PageProperty =
DependencyProperty.Register(“Page”, // name
typeof(PhoneApplicationPage), // property type
typeof(QuickJumpGrid), // owner type
new PropertyMetadata(
null, // default value
new PropertyChangedCallback(OnPageChanged) // callback
)
);
// A wrapper .NET property for the dependency property
public PhoneApplicationPage Page
{
get { return (PhoneApplicationPage)GetValue(PageProperty); }
set { SetValue(PageProperty, value); }
}
// When Page is set, intercept presses on the hardware Back button
static void OnPageChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
QuickJumpGrid quickJumpGrid = d as QuickJumpGrid;
if (e.OldValue != null)
(e.OldValue as PhoneApplicationPage).BackKeyPress -=
quickJumpGrid.Page_BackKeyPress;
quickJumpGrid.Page.BackKeyPress += quickJumpGrid.Page_BackKeyPress;
}
void Page_BackKeyPress(object sender, CancelEventArgs e)
{
// If the popup is open, close it rather than navigating away from the page
if (this.Popup.IsOpen)
{
ClosePopup();
e.Cancel = true;
}
}
void ClosePopup()
{
// Animate each tile out
foreach (QuickJumpTile tile in this.QuickJumpTiles.Children)
tile.FlipOut();
// Close the popup after the tiles have a chance to animate out
this.DelayedPopupCloseStoryboard.Begin();
}
// Handle item selection from the list box
void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
// Make sure the consumer has set the Page property
if (this.Page == null)
throw new InvalidOperationException(
“The Page property must be set to the host page.”);
if (e.AddedItems.Count != 1)
return;
KeyValuePair<string, object> item =
(KeyValuePair<string, object>)e.AddedItems[0];
if (item.Value != null)
{
// This is a normal item, so raise the event to consumers of this control
if (this.ItemSelected != null)
this.ItemSelected(sender, e);
}
else
{
// This is a header, so show the popup
foreach (QuickJumpTile tile in this.QuickJumpTiles.Children)
{
// Start by “disabling” each tile
tile.HasItems = false;
// Animate it in
tile.FlipIn();
}
// “Enable” the tiles that actually have items
foreach (var pair in GetUsedLetterItems())
{
char bucket = CharHelper.GetBucket(pair.Key);
QuickJumpTile tile;
if (pair.Key == “#”)
tile = this.QuickJumpTiles.Children[0] as QuickJumpTile;
else
tile = this.QuickJumpTiles.Children[pair.Key[0] – ‘a’ + 1]
as QuickJumpTile;
tile.HasItems = true;
tile.Tag = pair; // Also store the item from the list for later
}
// Remember the current visibility of the status bar & application bar
this.isPageStatusBarVisible = SystemTray.GetIsVisible(this.Page);
this.isPageAppBarVisible = this.Page.ApplicationBar != null ?
this.Page.ApplicationBar.IsVisible : false;
// Ensure that both bars are hidden, so they don’t overlap the popup
SystemTray.SetIsVisible(this.Page, false);
if (this.Page.ApplicationBar != null)
this.Page.ApplicationBar.IsVisible = false;
// Now open the popup
this.Popup.IsOpen = true;
}
// Clear selection so repeated taps work
this.ListBox.SelectedIndex = -1;
}
// Handle taps on tiles in the popup
void QuickJumpTiles_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
QuickJumpTile tile = e.OriginalSource as QuickJumpTile;
if (tile != null && tile.HasItems)
{
// Retrieve the header item from the list
KeyValuePair<string, object> header =
(KeyValuePair<string, object>)tile.Tag;
// Scroll to the end, THEN scroll the tile into view,
// so the tile is at the top of the page rather than the bottom
// Prevent flicker from seeing the end
this.ListBox.Opacity = 0;
// Scroll to the end
this.ListBox.ScrollIntoView(
this.ListBox.Items[this.ListBox.Items.Count – 1]);
this.Dispatcher.BeginInvoke(delegate()
{
// Now scroll to the chosen header
this.ListBox.ScrollIntoView(header);
this.ListBox.Opacity = 1; // Restore
ClosePopup();
});
}
}
void DelayedPopupCloseStoryboard_Completed(object sender, EventArgs e)
{
this.Popup.IsOpen = false;
// Restore the visibility of the status bar and application bar
SystemTray.SetIsVisible(this.Page, this.isPageStatusBarVisible);
if (this.Page.ApplicationBar != null)
this.Page.ApplicationBar.IsVisible = this.isPageAppBarVisible;
}
}
}

[/code]

Notes:

  • Although the popup is defined in Listing 18.5 as a XAML resource, a Silverlight bug causes it to not be showable unless it starts out with IsOpen set to true. Therefore, the constructor performs a workaround of transferring the popup’s child to a new popup created in C#. This new popup can start out hidden and can be shown correctly when desired.
  • The GetAllItems method, used internally by Update for populating the list box, is a C# iterator that returns the true list of items in the list box, including the letter-tile headers that separate each group of real items. The GetUsedLetterItems method works the same way, but only returns the letter-tile headers.
  • This control defines its Page property as a dependency property, discussed after these notes. As mentioned earlier, Page is used to temporarily hide the page’s status bar and application bar (if present) so it doesn’t overlap the popup. It is also used to enable pressing the hardware back button to dismiss the popup.
  • Inside ClosePopup, the empty DelayedPopupCloseStoryboard storyboard is leveraged to delay the actual closing of the popup by .15 seconds. This is done to give the content of the popup a chance to animate out.
  • Inside ListBox_SelectionChanged, the code checks whether a real item has been selected or a header tile. If it’s a header tile, then the popup is shown. Before this is done, however, each tile has its HasItems property set appropriately. This enables letters with no items to appear disabled and unclickable.
  • The code that performs the actual quick-jumping (QuickJumpTiles_ MouseLeftButtonUp) uses list box’s ScrollIntoView method to ensure that the passedin item (the letter-tile header) is on-screen. However, it performs a little trick to provide proper behavior. ScrollIntoView scrolls the smallest amount necessary to get the target item on-screen. This means that if the target tile is lower in the list, it will end up at the bottom of the list after ScrollIntoView is called. We want jumping to a letter to put the letter-tile header at the top of the list, however. Therefore, this code first scrolls to the very end of the list, and then it scrolls to the target tile. This ensures that it is always on the top (unless only a few number of items remain after the tile).

Dependency Properties

Dependency properties play a very important role in Silverlight. Dependency properties are also the only type of property that can be used in a style’s setter, and the only type of property that can be used as a target of data binding.

A dependency property is named as such because it depends on multiple providers for determining its value at any point in time. These providers could be an animation continuously changing its value, a parent element whose property value propagates down to its children, and so on.

Recall that the main page in Listing 18.1 uses data binding to set the value of QuickJumpGrid’s Page property:

[code]

<local:QuickJumpGrid x:Name=”QuickJumpGrid” Grid.Row=”1” Margin=”24,0,0,0”
Page=”{Binding ElementName=Page}”
ItemSelected=”QuickJumpGrid_ItemSelected”/>

[/code]

This is the reason that Listing 18.6 defines Page as a dependency property; to enable consumers to use data binding to set it. If Page were a normal .NET property instead, parsing the XAML file in Listing 18.1 would throw an exception.

Most commonly, a dependency property is created to take advantage of automatic change notification. In this scenario, the dependency property is used as the source of a binding and changes to its value are automatically reflected in the target element.

To define a dependency property, you call DependencyProperty.Register and assign its result to a static field, as done in Listing 18.6:

[code]

// A Page dependency property
public static readonly DependencyProperty PageProperty =
DependencyProperty.Register(“Page”, // name
typeof(PhoneApplicationPage), // property type
typeof(QuickJumpGrid), // owner type
new PropertyMetadata(
null, // default value
new PropertyChangedCallback(OnPageChanged) // callback
)
);

[/code]

The optional property-changed callback gets called whenever the property’s value changes. It must be a static method, but the relevant instance is passed as the first parameter, as seen in the implementation of OnPageChanged in Listing 18.6.

A dependency property’s value can be get and set via GetValue and SetValue methods on the class defining the property (QuickJumpGrid, in this example). All controls have these methods, inherited from a base DependencyObject class. However, to make things simpler for C# and XAML consumers, it’s common practice to define a .NET property that wraps these two methods, as done in Listing 18.6:

[code]

public PhoneApplicationPage Page
{
get { return (PhoneApplicationPage)GetValue(PageProperty); }
set { SetValue(PageProperty, value); }
}

[/code]

.NET properties are ignored at runtime when setting dependency properties in XAML!

When a dependency property is set in XAML, GetValue is called directly.Therefore, to maintain parity between setting a property in XAML versus C#, it’s crucial that property wrappers, such as Page in Listing 18.6, not contain any logic in addition to the GetValue/SetValue calls. If you want to add custom logic, that’s what the property-changed callback is for.

Visual Studio has a snippet called propdp that automatically expands into a definition of a dependency property and a wrapper .NET property, which makes defining one much faster than doing all the typing yourself! (It also has a snippet called propa for defining an attachable property.) Note that this snippet was originally created for WPF, so it needs a small tweak in order to work for Silverlight. It attempts to construct a class called UIPropertyMetadata for the last parameter of DependencyProperty.Register, but you must change this to PropertyMetadata instead.

The CharHelper Class

In Listing 18.6, QuickJumpGrid uses a class called CharHelper to figure out which of the 27 “buckets” each string key belongs in (a-z or #). This class is implemented in Listing 18.7.

LISTING 18.7 CharHelper.cs—A Static Helper Class for Bucketizing Entries

[code]

using System;
using System.Collections.Generic;
namespace WindowsPhoneApp
{
public static class CharHelper
{
static Dictionary<char, char> accentMap = new Dictionary<char, char>();
static CharHelper()
{
// Map some common accented letters to non-accented letters
accentMap.Add(‘à’, ‘a’); accentMap.Add(‘á’, ‘a’); accentMap.Add(‘â’, ‘a’);
accentMap.Add(‘ã’, ‘a’); accentMap.Add(‘ä’, ‘a’); accentMap.Add(‘˙a’, ‘a’);
accentMap.Add(‘æ’, ‘a’);
accentMap.Add(‘è’, ‘e’); accentMap.Add(‘é’, ‘e’); accentMap.Add(‘ê’, ‘e’);
accentMap.Add(‘ë’, ‘e’);
accentMap.Add(‘ì’, ‘i’); accentMap.Add(‘í’, ‘i’); accentMap.Add(‘î’, ‘i’);
accentMap.Add(‘ï’, ‘i’);
accentMap.Add(‘ò’, ‘o’); accentMap.Add(‘ó’, ‘o’); accentMap.Add(‘ô’, ‘o’);
accentMap.Add(‘õ’, ‘o’); accentMap.Add(‘ö’, ‘o’);
accentMap.Add(‘ù’, ‘u’); accentMap.Add(‘ú’, ‘u’); accentMap.Add(‘û’, ‘u’);
accentMap.Add(‘ü’, ‘u’);
}
public static char GetBucket(string s)
{
char c = Char.ToLowerInvariant(s[0]);
if (!Char.IsLetter(c))
return ‘#’;
return RemoveAccent(c);
}
static char RemoveAccent(char letter)
{
if (letter >= ‘a’ && letter <= ‘z’)
return letter;
if (accentMap.ContainsKey(letter))
return accentMap[letter];
// Unknown accented letter
return ‘#’;
}
}
}

[/code]

If it weren’t for accented letters, all GetBucket would need to do is check if the first character of the string is a letter and return that letter (in a case-insensitive fashion), otherwise return #. However, this code maps several common accented letters to their non-accented version so strings beginning with such characters can appear where expected. In Cocktails, this enables a drink called Épicé Sidecar to appear in the e list.

The QuickJumpTile User Control

The QuickJumpTile user control, used 27 times in QuickJumpGrid’s popup, is implemented in Listings 18.8 and 18.9.

LISTING 18.8 QuickJumpTile.xaml—The User Interface for the QuickJumpTile User Control

[code]

<UserControl x:Class=”WindowsPhoneApp.QuickJumpTile”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
Foreground=”{StaticResource PhoneForegroundBrush}”
FontFamily=”{StaticResource PhoneFontFamilySemiBold}”
FontSize=”49” Width=”99” Height=”99”>
<!– Add two storyboards to the user control’s resource dictionary –>
<UserControl.Resources>
<!– Flip in –>
<Storyboard x:Name=”FlipInStoryboard” Storyboard.TargetName=”PlaneProjection”
Storyboard.TargetProperty=”RotationX”>
<DoubleAnimation From=”-90” To=”0” Duration=”0:0:.8” BeginTime=”0:0:.2”>
<DoubleAnimation.EasingFunction>
<QuinticEase/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
<!– Flip out –>
<Storyboard x:Name=”FlipOutStoryboard” Storyboard.TargetName=”PlaneProjection”
Storyboard.TargetProperty=”RotationX”>
<DoubleAnimation From=”0” To=”90” Duration=”0:0:.15”/>
</Storyboard>
</UserControl.Resources>
<Canvas x:Name=”Canvas” Background=”{StaticResource PhoneChromeBrush}”>
<Canvas.Projection>
<PlaneProjection x:Name=”PlaneProjection” RotationX=”-90”/>
</Canvas.Projection>
<TextBlock x:Name=”TextBlock” Foreground=”{StaticResource PhoneDisabledBrush}”
Canvas.Left=”9” Canvas.Top=”34”/>
</Canvas>
</UserControl>

[/code]

LISTING 18.9 QuickJumpTile.xaml.cs—The Code-Behind for the QuickJumpTile User Control

[code]

using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace WindowsPhoneApp
{
public partial class QuickJumpTile : UserControl
{
static SolidColorBrush whiteBrush = new SolidColorBrush(Colors.White);
string text;
bool hasItems;
public QuickJumpTile()
{
InitializeComponent();
}
public string Text
{
get { return this.text; }
set
{
this.text = value;
this.TextBlock.Text = this.text;
}
}
public bool HasItems
{
get { return this.hasItems; }
set
{
this.hasItems = value;
if (this.hasItems)
{
// Enable this tile
this.Canvas.Background =
Application.Current.Resources[“PhoneAccentBrush”] as Brush;
this.TextBlock.Foreground = whiteBrush;
Tilt.SetIsEnabled(this, true);
}
else
{
// Disable this tile
this.Canvas.Background =
Application.Current.Resources[“PhoneChromeBrush”] as Brush;
this.TextBlock.Foreground =
Application.Current.Resources[“PhoneDisabledBrush”] as Brush;
Tilt.SetIsEnabled(this, false);
}
}
}
public void FlipIn()
{
this.FlipInStoryboard.Begin();
}
public void FlipOut()
{
this.FlipOutStoryboard.Begin();
}
}
}

[/code]

Notes:

  • The two storyboards animate RotationX on the canvas’s plane projection to make each tile flip in when the popup appears and flip out before it disappears. This is shown in Figure 18.5.
FIGURE 18.5 The tiles can flip in and out.
FIGURE 18.5 The tiles can flip in and out.
  • FlipInStoryboard uses a slight delay (a BeginTime of .2 seconds) to help ensure that the animation can be seen.
  • The plane projection is placed on the canvas rather than the root user control so it doesn’t interfere with the tilt effect (enabled in code-behind when HasItems is true).
  • The text foreground (when HasItems is true) is set to white—not PhoneForegroundBrush—to match the behavior of the quick jump grid used by the built-in apps.

The QuickJumpItem User Control

The QuickJumpItem user control, used to represent every item in QuickJumpGrid’s list box, is implemented in Listings 18.10 and 18.11.

LISTING 18.10 QuickJumpItem.xaml—The User Interface for the QuickJumpItem User Control

[code]

<UserControl x:Class=”WindowsPhoneApp.QuickJumpItem”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
Foreground=”{StaticResource PhoneForegroundBrush}”
FontFamily=”{StaticResource PhoneFontFamilySemiLight}” FontSize=”42”>
<StackPanel Orientation=”Horizontal” Background=”Transparent”>
<Canvas x:Name=”Canvas” Width=”62” Height=”62”
Background=”{StaticResource PhoneAccentBrush}”>
<TextBlock x:Name=”TextBlock” Foreground=”White” FontSize=”49”
Canvas.Left=”7” Canvas.Top=”-3”/>
</Canvas>
<ContentPresenter Margin=”18,0,0,0” x:Name=”ContentPresenter”/>
</StackPanel>
</UserControl>

[/code]

LISTING 18.11 QuickJumpItem.xaml.cs—The Code-Behind for the QuickJumpItem User Control

[code]

using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace WindowsPhoneApp
{
public partial class QuickJumpItem : UserControl
{
public QuickJumpItem()
{
InitializeComponent();
}
public bool IsHeader { get; private set; }
// A KeyValuePair dependency property
public static readonly DependencyProperty KeyValuePairProperty =
DependencyProperty.Register(“KeyValuePair”, // name
typeof(KeyValuePair<string, object>), // property type
typeof(QuickJumpItem), // owner type
new PropertyMetadata(
new KeyValuePair<string, object>(), // default value
new PropertyChangedCallback(OnKeyValuePairChanged) // callback
)
);
// A wrapper .NET property for the dependency property
public KeyValuePair<string, object> KeyValuePair
{
get { return (KeyValuePair<string, object>)GetValue(KeyValuePairProperty); }
set { SetValue(KeyValuePairProperty, value); }
}
static void OnKeyValuePairChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
QuickJumpItem item = d as QuickJumpItem;
if (item.KeyValuePair.Value != null)
{
// Show this as a normal item
item.Canvas.Background =
Application.Current.Resources[“PhoneChromeBrush”] as Brush;
item.ContentPresenter.Content = item.KeyValuePair.Value;
item.TextBlock.Text = null;
}
else
{
// Show this as a special header tile
item.Canvas.Background =
Application.Current.Resources[“PhoneAccentBrush”] as Brush;
item.ContentPresenter.Content = null;
item.TextBlock.Text = item.KeyValuePair.Key;
item.IsHeader = true;
}
}
}
}

[/code]

Notes:

  • The item consists of a square tile stacked to the left of a content presenter. The content presenter enables an arbitrary element to be rendered inside the item, even though this app uses plain text for each cocktail (which comes from the Cocktail class’s ToString method).
  • As with QuickJumpTile, the text foreground inside QuickJumpItem’s tile is hardcoded to white to match the built-in quick jump grid.
  • KeyValuePair is defined as a dependency property, which is what enables it to be databound to each item in the data template for QuickJumpGrid’s list box back in Listing 18.5.
  • The display of this item is updated based on whether the underlying key/value pair is a letter-tile header (indicated by a null value) or a normal item. For normal items, the tile is a plain square filled with PhoneChromeBrush.

The Finished Product

Cocktails (Quick Jump Grid)