Deep Zoom Viewer (Pinch, Stretch, & Double Tap Gestures)

Deep Zoom is a slick technology for creating, viewing, and manipulating huge images or collections of images. It can be used to create experiences much like Bing Maps or Google Maps, but applied to any domain. With the samples available from this app, you can explore large panoramic photographs, scanned-in artwork, a computer-generated data visualization, an example of what a deep zoom advertisement might look like, and, yes, Earth.

To maximize performance, Deep Zoom images are multiresolution; the image file format includes many separate subimages—called tiles—at multiple zoom levels. Tiles are downloaded on-demand and rendered in a fairly seamless fashion with smooth transitions. For end users, the result is a huge image that can be loaded, zoomed, and panned extremely quickly.

Deep Zoom Viewer enables viewing and interacting with any online Deep Zoom image right on your Windows phone. You can enter a URL that points to any Deep Zoom image (or image collection), or you can browse any of the seven interesting samples that are already provided.

To render a Deep Zoom image, this app leverages Silverlight’s MultiScaleImage control, which does all the hard work. To view a file, you just need to place a MultiScaleImage on a page and then set its Source property to an appropriate URL. However, the control does not provide any built-in gestures for manipulating the image. Therefore, this app provides a perfect opportunity to demonstrate how to implement pinch-&-stretch zooming and double-tap gestures—practically a requirement for any respectable Deep Zoom viewer.

Pinching is the standard zoom-out gesture that involves placing two fingers on the screen and then sliding them toward each other. Stretching is the standard zoom-in gesture that involves placing two fingers on the screen and then sliding them away from each other. In this app, double tapping is used to quickly zoom in, centered on the point that was tapped.

Windows Phone style guidelines dictate that touch gestures should only be used for their intended purposes. As in Deep Zoom Viewer, a pinch should always zoom out, a stretch should always zoom in, and a double tap should always perform some kind of zoom in and/or zoom out.

How do I create my own Deep Zoom images?

Currently, the quickest and easiest way is to use Microsoft’s free Zoom.it service (http://zoom.it).This turns any JPG, PNG, or TIFF image into a Deep Zoom image.The service also supports SVG files, PDF files, and even web pages as input! You just enter an appropriate URL, and it does the conversion. It even hosts the file for you! Alternatively, the most powerful option is to use Microsoft’s Deep Zoom Composer, a free program that can be downloaded at http://bit.ly/deepzoomdownload.

Deep Zoom versus Seadragon

Although Deep Zoom refers to a Silverlight-specific feature, the underlying Seadragon technology (which Microsoft originally acquired from a company called Seadragon Software) has been exposed in other forms. For example,Microsoft has released an open-source JavaScript version called “Seadragon Ajax” in its Ajax Control Toolkit. It can view the same file types as Deep Zoom.

The User Interface

Deep Zoom Viewer is a single-page app (except for an instructions page) that dedicates all of its screen real estate to the MultiScaleImage control. On top of this, it layers a translucent application bar and a dialog that enables the user to type arbitrary Deep Zoom image URLs. Figure 41.1 shows the main page with its application bar menu expanded, and Figure 41.2 shows the main page with its dialog showing. The XAML for this page is in Listing 41.1.

The application bar menu is expanded on top of the Carina Nebula.
FIGURE 41.1 The application bar menu is expanded on top of the Carina Nebula.
Entering a custom URL is done via a dialog that appears on top of the current Deep Zoom image.
FIGURE 41.2 Entering a custom URL is done via a dialog that appears on top of the current Deep Zoom image.

LISTING 41.1 MainPage.xaml—The User Interface for Deep Zoom Viewers’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:toolkit=”clr-namespace:Microsoft.Phone.Controls;
➥assembly=Microsoft.Phone.Controls.Toolkit”
xmlns:local=”clr-namespace:WindowsPhoneApp”
FontFamily=”{StaticResource PhoneFontFamilyNormal}”
FontSize=”{StaticResource PhoneFontSizeNormal}”
Foreground=”{StaticResource PhoneForegroundBrush}”
SupportedOrientations=”PortraitOrLandscape”>
<!– The application bar –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar Opacity=”.5”>
<shell:ApplicationBarIconButton Text=”fit to screen”
IconUri=”/Images/appbar.fitToScreen.png”
Click=”FitToScreenButton_Click”/>
<shell:ApplicationBarIconButton Text=”zoom in”
IconUri=”/Shared/Images/appbar.plus.png”
Click=”ZoomInButton_Click”/>
<shell:ApplicationBarIconButton Text=”zoom out”
IconUri=”/Shared/Images/appbar.minus.png”
Click=”ZoomOutButton_Click”/>
<shell:ApplicationBarIconButton Text=”instructions”
IconUri=”/Shared/Images/appbar.instructions.png”
Click=”InstructionsButton_Click”/>
<shell:ApplicationBar.MenuItems>
<shell:ApplicationBarMenuItem Text=”[enter url]”
Click=”CustomUrlMenuItem_Click”/>
</shell:ApplicationBar.MenuItems>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<Grid>
<!– The Deep Zoom image –>
<MultiScaleImage x:Name=”DeepZoomImage”>
<!– Attach the gesture listener to this element –>
<toolkit:GestureService.GestureListener>
<toolkit:GestureListener DoubleTap=”GestureListener_DoubleTap”
PinchStarted=”GestureListener_PinchStarted”
PinchDelta=”GestureListener_PinchDelta”/>
</toolkit:GestureService.GestureListener>
</MultiScaleImage>
<!– Show a progress bar while loading an image –>
<ProgressBar x:Name=”ProgressBar” Visibility=”Collapsed”/>
<!– A dialog for entering a URL –>
<local:Dialog x:Name=”CustomFileDialog” Closed=”CustomFileDialog_Closed”>
<local:Dialog.InnerContent>
<StackPanel>
<TextBlock Text=”Enter the URL of a Deep Zoom file” Margin=”11,5,0,-5”
Foreground=”{StaticResource PhoneSubtleBrush}”/>
<TextBox InputScope=”Url” Text=”{Binding Result, Mode=TwoWay}”/>
</StackPanel>
</local:Dialog.InnerContent>
</local:Dialog>
</Grid>
</phone:PhoneApplicationPage>

[/code]

  • A gesture listener from the Silverlight for Windows Phone Toolkit is attached to the MultiScaleImage control, so we can very easily detect double taps and pinch/stretch gestures.
  • The MultiScaleImage control has a lot of automatic functionality to make the viewing experience as smooth as possible. For example, as tiles are downloaded, they are smoothly blended in with a blurry-to-crisp transition, captured in Figure 41.3.
You can occasionally catch pieces of the view starting out blurry and then seamlessly becoming crisp.
FIGURE 41.3 You can occasionally catch pieces of the view starting out blurry and then seamlessly becoming crisp.

The Code-Behind

Listing 41.2 contains the code-behind for the main page.

LISTING 41.2 MainPage.xaml.cs—The Code-Behind for Deep Zoom Viewers’Main Page

[code]

using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Navigation;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
{
// Persistent settings
Setting<Uri> savedImageUri = new Setting<Uri>(“ImageUri”,
new Uri(Data.BaseUri, “last-fm.dzi”));
Setting<Point> savedViewportOrigin = new Setting<Point>(“ViewportOrigin”,
new Point(0, -.2));
Setting<double> savedZoom = new Setting<double>(“Zoom”, 1);
// Used by pinch and stretch
double zoomWhenPinchStarted;
// Used by panning and double-tapping
Point mouseDownPoint = new Point();
Point mouseDownViewportOrigin = new Point();
public MainPage()
{
InitializeComponent();
// Fill the application bar menu with the sample images
foreach (File f in Data.Files)
{
ApplicationBarMenuItem item = new ApplicationBarMenuItem(f.Title);
// This assignment is needed so each anonymous method gets the right value
string filename = f.Filename;
item.Click += delegate(object sender, EventArgs e)
{
OpenFile(new Uri(Data.BaseUri, filename), true);
};
this.ApplicationBar.MenuItems.Add(item);
}
// Handle success for any attempt to open a Deep Zoom image
this.DeepZoomImage.ImageOpenSucceeded +=
delegate(object sender, RoutedEventArgs e)
{
// Hide the progress bar
this.ProgressBar.Visibility = Visibility.Collapsed;
this.ProgressBar.IsIndeterminate = false; // Avoid a perf issue
// Initialize the view
this.DeepZoomImage.ViewportWidth = this.savedZoom.Value;
this.DeepZoomImage.ViewportOrigin = this.savedViewportOrigin.Value;
};
// Handle failure for any attempt to open a Deep Zoom image
this.DeepZoomImage.ImageOpenFailed +=
delegate(object sender, ExceptionRoutedEventArgs e)
{
// Hide the progress bar
this.ProgressBar.Visibility = Visibility.Collapsed;
this.ProgressBar.IsIndeterminate = false; // Avoid a perf issue
MessageBox.Show(“Unable to open “ + this.savedImageUri.Value + “.”,
“Error”, MessageBoxButton.OK);
};
// Load the previously-viewed (or default) image
OpenFile(this.savedImageUri.Value, false);
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedFrom(e);
// Remember settings for next time
this.savedViewportOrigin.Value = this.DeepZoomImage.ViewportOrigin;
this.savedZoom.Value = this.DeepZoomImage.ViewportWidth;
}
// Attempt to open the Deep Zoom image at the specified URI
void OpenFile(Uri uri, bool resetPosition)
{
if (resetPosition)
{
// Restore these settings to their default values
this.savedZoom.Value = this.savedZoom.DefaultValue;
this.savedViewportOrigin.Value = this.savedViewportOrigin.DefaultValue;
}
this.savedImageUri.Value = uri;
// Assign the image
this.DeepZoomImage.Source = new DeepZoomImageTileSource(uri);
// Show a temporary progress bar
this.ProgressBar.IsIndeterminate = true;
this.ProgressBar.Visibility = Visibility.Visible;
}
// Three handlers (mouse down/move/up) to implement panning
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonDown(e);
// Ignore if the dialog is visible
if (this.CustomFileDialog.Visibility == Visibility.Visible)
return;
this.mouseDownPoint = e.GetPosition(this.DeepZoomImage);
this.mouseDownViewportOrigin = this.DeepZoomImage.ViewportOrigin;
this.DeepZoomImage.CaptureMouse();
}
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
// Ignore if the dialog is visible
if (this.CustomFileDialog.Visibility == Visibility.Visible)
return;
Point p = e.GetPosition(this.DeepZoomImage);
// ViewportWidth is the absolute zoom (2 == half size, .5 == double size)
double scale = this.DeepZoomImage.ActualWidth /
this.DeepZoomImage.ViewportWidth;
// Pan the image by setting a new viewport origin based on the mouse-down
// location and the distance the primary finger has moved
this.DeepZoomImage.ViewportOrigin = new Point(
this.mouseDownViewportOrigin.X + (this.mouseDownPoint.X – p.X) / scale,
this.mouseDownViewportOrigin.Y + (this.mouseDownPoint.Y – p.Y) / scale);
}
protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonUp(e);
// Stop panning
this.DeepZoomImage.ReleaseMouseCapture();
}
// The three gesture handlers for double tap, pinch, and stretch
void GestureListener_DoubleTap(object sender, GestureEventArgs e)
{
// Ignore if the dialog is visible
if (this.CustomFileDialog.Visibility == Visibility.Visible)
return;
// Zoom in by a factor of 2 centered at the place where the double tap
// occurred (the same place as the most recent MouseLeftButtonDown event)
ZoomBy(2, this.mouseDownPoint);
}
// Raised when two fingers touch the screen (likely to begin a pinch/stretch)
void GestureListener_PinchStarted(object sender,
PinchStartedGestureEventArgs e)
{
this.zoomWhenPinchStarted = this.DeepZoomImage.ViewportWidth;
}
// Raised continually as either or both fingers move
void GestureListener_PinchDelta(object sender, PinchGestureEventArgs e)
{
// Ignore if the dialog is visible
if (this.CustomFileDialog.Visibility == Visibility.Visible)
return;
// The distance ratio is always relative to when the pinch/stretch started,
// so be sure to apply it to the ORIGINAL zoom level, not the CURRENT
double zoom = this.zoomWhenPinchStarted / e.DistanceRatio;
this.DeepZoomImage.ViewportWidth = zoom;
}
void ZoomBy(double zoomFactor, Point centerPoint)
{
// Restrict how small the image can get (don’t get smaller than half size)
if (this.DeepZoomImage.ViewportWidth >= 2 && zoomFactor < 1)
return;
// Convert the on-screen point to the image’s coordinate system, which
// is (0,0) in the top-left corner and (1,1) in the bottom right corner
Point logicalCenterPoint =
this.DeepZoomImage.ElementToLogicalPoint(centerPoint);
// Perform the zoom
this.DeepZoomImage.ZoomAboutLogicalPoint(
zoomFactor, logicalCenterPoint.X, logicalCenterPoint.Y);
}
// Code for the custom file dialog
protected override void OnBackKeyPress(CancelEventArgs e)
{
base.OnBackKeyPress(e);
// If the dialog is open, close it instead of leaving the page
if (this.CustomFileDialog.Visibility == Visibility.Visible)
{
e.Cancel = true;
this.CustomFileDialog.Hide(MessageBoxResult.Cancel);
}
}
void CustomFileDialog_Closed(object sender, MessageBoxResultEventArgs e)
{
// Try to open the typed-in URL
if (e.Result == MessageBoxResult.OK && this.CustomFileDialog.Result != null)
OpenFile(new Uri(this.CustomFileDialog.Result.ToString()), true);
}
// Application bar handlers
void FitToScreenButton_Click(object sender, EventArgs e)
{
this.DeepZoomImage.ViewportWidth = 1; // Un-zoom
this.DeepZoomImage.ViewportOrigin = new Point(0, -.4); // Give a top margin
}
void ZoomInButton_Click(object sender, EventArgs e)
{
// Zoom in by 50%, keeping the current center point
ZoomBy(1.5, new Point(this.DeepZoomImage.ActualWidth / 2,
this.DeepZoomImage.ActualHeight / 2));
}
void ZoomOutButton_Click(object sender, EventArgs e)
{
// Zoom out by 50%, keeping the current center point
ZoomBy(1 / 1.5, new Point(this.DeepZoomImage.ActualWidth / 2,
this.DeepZoomImage.ActualHeight / 2));
}
void InstructionsButton_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(
new Uri(“/InstructionsPage.xaml”, UriKind.Relative));
}
void CustomUrlMenuItem_Click(object sender, EventArgs e)
{
// Show the custom file dialog, initialized with the current URI
if (this.savedImageUri.Value != null)
this.CustomFileDialog.Result = this.savedImageUri.Value;
this.CustomFileDialog.Show();
}
}
}

[/code]

  • The application bar menu is filled with a list of sample files based on the following two classes defined in a separate Data.cs file:

    [code]
    public struct File
    {
    public string Title { get; set; }
    public string Filename { get; set; }
    }
    public static class Data
    {
    public static readonly Uri BaseUri =
    new Uri(“http://static.seadragon.com/content/misc/”);
    public static File[] Files = {
    new File { Title = “World-Wide Music Scene”, Filename = “last-fm.dzi” },
    new File { Title = “Carina Nebula”, Filename = “carina-nebula.dzi” },
    new File { Title = “Blue Marble”, Filename = “blue-marble.dzi” },
    new File { Title = “Contoso Fixster”, Filename = “contoso-fixster.dzi” },
    new File { Title = “Milwaukee, 1898”, Filename = “milwaukee.dzi” },
    new File { Title = “Yosemite Panorama”, Filename=“yosemite-panorama.dzi” },
    new File { Title = “Angkor Wat Temple”, Filename = “angkor-wat.dzi” }
    };
    }
    [/code]

  • When constructing the URI for each filename, BaseUri is prepended to the filename using an overloaded constructor of Uri that accepts two arguments.
  • Much like the Image element, the MultiScaleImage element is told what to render by setting its Source property. This is done inside OpenFile. Note that the type of Source is MultiScaleTileSource, an abstract class with one concrete subclass: DeepZoomImageTileSource.
  • After setting Source, the image download is asynchronous and either results in an ImageOpenSucceeded or ImageOpenFailed event being raised. This listing leverages this fact to temporarily show an indeterminate progress bar while the initial download is occurring, although this is usually extremely fast.

Can MultiScaleImage work with a local image included with the app?

Surprisingly, no! Only online files are supported.

  • The current zoom level and visible region of the image are represented by two properties: ViewportWidth and ViewportOrigin.
    • ViewportWidth is actually the inverse of the zoom level. A value of .5 means that half the width is visible. (So the zoom level is 2.) A value of 2 means that the width of the viewport is double that of the image, so the image width occupies half of the visible area.
    • ViewportOrigin is the point in the image that is currently at the top-left corner of the visible area. The point is expressed in what Deep Zoom calls logical coordinates. In this system, (0,0) is the top-left corner of the image, and (1,1) is the bottom-right corner of the image.
  • This app’s panning functionality is supported with traditional MouseLeftButtonDown, MouseMove, and MouseLeftButtonUp handlers that implement a typical drag-and-drop scheme. In MouseMove, the amount that the finger has moved since MouseLeftButtonDown is applied to the ViewportOrigin, but this value is scaled appropriately based on the control’s width (480 or 800, depending on the phone orientation) and the zoom level. This is necessary because ViewportOrigin must be set to a logical point, and it also ensures that the panning gesture doesn’t get magnified as the user zooms in.

Be sure to use a logical point when setting ViewportOrigin!

Otherwise, the image will likely pan far offscreen. Luckily, MultiScaleImage provides two handy methods—ElementToLogicalPoint and LogicalToElementPoint—for converting between logical points and element-relative points. (When the MultiScaleImage control fills the screen and has no transforms applied, as in this app, element-relative points are equivalent to points on the screen.)

  • After the three handlers that implement panning, this listing contains the three handlers for gesture listener events. The first handler (GestureListener_ DoubleTap) performs a 2x zoom each time a double tap is detected.

MultiScaleImage has built-in inertia effects whenever you change the zoom level or viewport origin, so the panning and zooming done by this app exhibit smooth and inertial transitions without any extra work. If you do not want these effects, simply set MultiScaleImage’s UseSprings property to false.

  • The next two handlers (GestureListener_PinchStarted and GestureListener_PinchDelta) handle pinching and stretching gestures. The DistanceRatio property reveals how much further apart (>1) or closer together (<1) the two fingers are, compared to when they made contact with the screen. The key to getting the appropriate effect is to apply this ratio to the original zoom level captured in the PinchStarted event handler. Normally, as with a ScaleTransform or CompositeTransform, you would multiply the original value by the ratio. Because ViewportWidth is the inverse of the zoom level, however, this listing instead divides its value by the ratio.
  • GestureListener_PinchDelta directly updates ViewportWidth rather than calling the ZoomBy method used elsewhere. ZoomBy centers the zoom around a passedin point, but MultiScaleImage doesn’t work well when the viewport is continually and rapidly moved.

The same three gesture listener events—PinchStarted, PinchDelta, and PinchCompleted—can be used to detect both pinching and stretching.The key piece of data is the DistanceRatio property on PinchGestureEventArgs, which indicates how far apart or close together the two fingers are compared to when they first touched the screen. Be careful how you use this value, however. A value greater than 1 does not necessarily mean stretching is occurring, and a value less than one does not necessarily mean pinching is occurring. For example, users could stretch their fingers until the ratio is 5 and then pinch them until the ratio goes back down to 2. As long as the ratio is continually applied to the zooming element’s original zoom level when the pinch/stretch started rather than the current zoom level, pinching and stretching will work as intended.

  • ZoomBy, used by the double-tap handler and the zooming application bar button handlers, zooms the viewport by an amount relative to the current zoom level with MultiScaleImage’s ZoomAboutLogicalPoint method.

How do I determine the center point of a pinch or stretch gesture, so I can center my zoom on that point?

Although it’s not done by this app (due to flakiness in constantly recentering the viewport), it’s common practice to center the zoom of a pinch or stretch gesture based on the midpoint between the two fingers. Although this point is not directly exposed by the gesture listener, you can calculate it as follows:

[code]

void GestureListener_PinchDelta(object sender, PinchGestureEventArgs e)
{
Point firstPoint = e.GetPosition(this, 0); // Finger #1
Point secondPoint = e.GetPosition(this, 1); // Finger #2
// Calculate the midpoint
Point pinchOrigin = new Point(
(firstPoint.X + secondPoint.X) / 2,
(firstPoint.Y + secondPoint.Y) / 2);

}

[/code]

Both PinchGestureEventArgs and PinchStartedGestureEventArgs expose an overload of GetPosition that enables passing 0 or 1 to get the point for either of the two relevant fingers. (The regular GetPosition overload always gives the data for the first, primary finger.) By continually calculating the midpoint in a PinchDelta event handler rather than once in a PinchStarted event handler, the center is continually updated as the two fingers move,which gives the best experience.

The Finished Product

Deep Zoom Viewer (Pinch, Stretch, & Double Tap Gestures)

Cocktails (Quick Jump Grid)

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)

The StageWebView Class

The flash.media.StageWebView uclass is a new class for displaying and interacting with rich HTML content in an AIR for Android application. It is a subclass of the EventDis patcher class.

The flash.media.StageWebView class uses the web control as provided by the native system that may vary slightly from one device to another. There is currently very little interaction between AIR and the HTML content.

StageWebView is not a display object, and is therefore not adding to the displayList. Create a StageWebView object and set its stage property to attach it directly to the stage. It always lies on top of your content, so if you want other objects to occupy the same view area, you must remove it first:

[code]

import flash.media.StageWebView;
var webView:StageWebView = new StageWebView();
webView.stage = this.stage;

[/code]

The size of the area dedicated to web content is defined in the viewPort property as a rectangle. Here it covers the stage width and 75% of the stage height:

[code]

import flash.geom.Rectangle;
var verticalBounds:int = stage.stageHeight*0.75;
webView.viewPort = new Rectangle(0, 0, stage.stageWidth, verticalBounds);

[/code]

To load content, call the loadURL method and pass the URL of the site. Add a listener so that in case an error occurs you can notify your user. Location is the event property for the site to load:

[code]

import flash.events.ErrorEvent;
webView.addEventListener(ErrorEvent.ERROR, onError);
webView.loadURL(“http://www.npr.org”);
function onError(event:ErrorEvent):void {
trace(“not able to reach location: “, event.location);
}

[/code]

Once the website is loaded, you should see it inside your application. It displays and responds as expected.

There are several events you can register to monitor activity and collect information.

You can register for the LocationChangeEvent.LOCATION_CHANGE event that is fired after a location has been reached:

[code]

import flash.events.LocationChangeEvent;
var webView:StageWebView = new StageWebView();
webView.stage = this.stage;
webView.addEventListener(LocationChangeEvent.LOCATION_CHANGE, onChange);
webView.loadURL(“http://www.npr.org”);
function onChange(event:LocationChangeEvent):void {
trace(“you are now at: “, event.location);
var verticalBounds:int = stage.stageHeight*0.75;
webView.viewPort = new Rectangle(0, 0, stage.stageWidth, verticalBounds);
}

[/code]

To avoid displaying an empty screen while loading, only display the view when its content is fully loaded and complete by listening to Event.COMPLETE.

You can register for the LocationChangeEvent.LOCATION_CHANGING event that is fired just before a new web location is requested. You can use this event in three different scenarios. You can prevent navigating to the new location by calling the preventDefault function. Most importantly, you need to catch the event to prevent it from opening the
native browser and leaving your application. Force the new location to load into Stage WebView and its viewPort area:

[code]

webView.addEventListener(LocationChanged.LOCATION_CHANGING, onChanging);
// prevent going to a new location
function onChanging(event:LocationChanged):void {
event.preventDefault();
trace(“sorry, you cannot go to: “, event.location);
}
// load new location in the StageWebView
function onChanging(event:LocationChanged):void {
event.preventDefault();
webView.load(event.location);
}

[/code]

If your application needs to know when StageWebView is in focus, register for the FOCUS_IN and FOCUS_OUT events. These events get dispatched when clicking inside or outside the rectangle area:

[code]

import flash.events.FocusEvent;

webView.addEventListener(FocusEvent.FOCUS_IN, inFocus);
webView.addEventListener(FocusEvent.FOCUS_OUT, outFocus);
function inFocus(event:FocusEvent):void {
trace(“on webview now”);
}
function outFocus(event:FocusEvent):void {
trace(“off webview now”);
}

[/code]

You can force focus when first launching your application:

[code]webView.assignFocus();[/code]

StageWebView has methods that mirror the functionality of a traditional browser toolbar. In fact, you could re-create a navigation user interface if you wanted to simulate the desktop experience.

The title and location properties return the page information. The stop and reload methods will, as their names indicate, stop the loading of a page or reload it.

The historyBack and historyForward methods load pages that were previously visited. Check that the isHistoryBackEnabled and isHistoryForwardEnabled properties are true to ensure the pages exist in either direction. Currently, the history is not available as a whole. If you want to access it, you must store it yourself as the user navigates
through pages.

As mentioned before, StageWebView is not added to the display list. To remove it, use the dispose method. Setting the object and its viewPort property to null is meant to aid in the garbage collection process:

[code]

webView.viewPort = null;
webView.dispose();
webView = null;

[/code]

Design Considerations

You should design your application for the best possible user experience. Toward that end, here are some points you should consider.

First, you have full control of the viewPort dimensions, but not its content. The vast majority of sites are designed only for the desktop. The few that deliver a mobile version take stage dimension into account and offer an adaptive layout and optimized content.

You can reduce the locations to a limited choice using the preventDefault method as previously discussed. If you want to keep the user experience open, prepare your application to accommodate multiple designs as much as possible.

Present your application in landscape mode and full screen. Align your application to the upper-left corner. This will mirror the web experience and users’ expectations:

[code]

import flash.display.StageAlign;
stage.align = StageAlign.TOP_LEFT;

[/code]

If the web page is larger than the viewPort, the StageWebView displays a scroll bar and zoom controls. There is no option to suppress these controls.

If you choose to keep auto-rotation enabled, you need to set a listener for the stage Event.RESIZE event and prevent scaling:

[code]

import flash.display.StageScaleMode;
stage.scaleMode = StageScaleMode.NO_SCALE;
stage.addEventListener(Event.RESIZE, onStageResized);

[/code]

When the screen rotates, re-create the viewPort at the appropriate dimension. It does not resize dynamically:

[code]

import flash.events.Event;
function onStageResized(event:Event):void {
webView.viewPort = null;
var verticalBounds:int = stage.stageHeight*0.75;
webView.viewPort =
new Rectangle(0, 0, stage.stageWidth, verticalBounds);
}

[/code]

If you navigate between locations, the native back button takes you back to the previous open application or home. It does not work as a back button for StageWebView. This is the expected behavior, of course, because the Internet content is part of your application, not a separate intent.

Either provide an internal back button or override the native back button. Do consider that this is not a recommended Android practice. It is not possible to put elements on top of StageWebView, but you can simulate a top navigation above it.

The drawViewPortToBitmap function can be used to take a screen capture of the Stage WebView. You can take advantage of this feature to temporarily replace StageWebView with a bitmap and display other elements on top of it.

Local Use

You can package HTML files with your application and then load them inside Stage WebView. You can use this to display a help page for your users, or other HTML content.

Copy the HTML page into a temporary file and load it using its url:

[code]

import flash.filesystem.File;
var file:File = File.applicationDirectory.resolvePath(“assets/test.html”);
var local:File = File.createTempFile();
file.copyTo(local, true);
webView.loadURL(local.url);

[/code]

Note that it is possible to load a local HTML file to make an Ajax XMLHttpRequest on another domain or to make JavaScript calls, as demonstrated in the next paragraph.

The loadString method is for displaying HTML formatted text in the viewPort area. The text must be fully HTML formatted:

[code]

// define the string in html format
var myString:String =
“<html>”
+ “<body bgcolor=”#FFFF33″>”
+ “<font color=”#FF0000″>Hello</font color><br>”
+ “<font color=”#FFFFFF”>Look at me</font color><br><br>”
+ “<a href=”http://www.google.com”>Click Me</a>”
+ “</body>”
+ “</html>”
webView.loadString(myString, “text/html”);
webView.addEventListener(LocationChangeEvent.LOCATION_CHANGING, onChanging);
// load the Google site into the viewport when clicking on the text
function onChanging(event:LocationChangeEvent):void {
event.preventDefault();
webView.loadURL(event.location);
}

[/code]

At the time of this writing, this feature is limited and does not provide much more than a TextField with htmlText would provide. You cannot, for instance, use it to display local assets such as images. It is useful if you want text placed in the StageWebView area.

Mobile Ads

Local use in combination with JavaScript is handy when dealing with mobile ads.

At the time of this writing, mobile advertising providers do not offer an AS3 SDK. Rewriting their AS2 code into AS3 to use in AIR does not seem to get ad impressions. Using StageWebView to simulate the browser model seems to be the best solution.

Go to http://developer.admob.com/wiki/Android or http://developer.admob.com/wiki/Requests for instructions on how to set up an ad request for AdMob before we adapt them for AIR for Android.

You need an embedded HTML page that makes the request to the ad provider with your publisher ID and the StageWebView to load the page and monitor the navigation. The HTML page contains a JavaScript script that fetches the ad. It is important to set manual_mode to true. Set test to true during development or to false to receive live ads.

[code]

<html>
<head>
<title>Get Ad</title>
<script type=”text/javascript”>
var admob_vars = {pubid: ‘YOUR_ID’,
bgcolor: ‘FFFFFF’
text: ‘000000’
test: true,
manual_mode: true
};
function displayAd() {
admob.fetchAd(document.getElementById(‘ad_space’));
}
</script>
<script type=”text/javascript”
src=http://mm.admob.com/static/iphone/iadmob.js></script>
</head>
<body onload=”displayAd()”>
<div id=”ad_space”></div>
</body>
</html>

[/code]

Figure 13-1. AdMob ads with the test property set to true (top) and false (bottom)
Figure 13-1. AdMob ads with the test property set to true (top) and false (bottom)

The ActionScript looks like this. Put your ad request in a try catch block in case the ad provider site is down or it sends back invalid data:

[code]

import flash.media.StageWebView;
import flash.geom.Rectangle;
import flash.events.LocationChangeEvent;
import flash.events.ErrorEvent;
import flash.filesystem.File;
import flash.filesystem.FileMode;
import flash.filesystem.FileStream;
import flash.media.StageWebView;
import flash.net.navigateToURL;
import flash.net.URLRequest;

var view:StageWebView;
var local:File;
view = new StageWebView();
view.stage = this.stage;
view.addEventListener(LocationChangeEvent.LOCATION_CHANGE, onChange);
view.addEventListener(LocationChangeEvent.LOCATION_CHANGING, onChanging);
view.addEventListener(ErrorEvent.ERROR, onViewError);
view.viewPort = new Rectangle(0, 0, 480, 60);

var base:File = File.applicationDirectory.resolvePath(“adView.html”);
local = File.createTempFile();
base.copy(local, true);
try {
view.loadURL(local.url);
} catch(error:Error) {
}
function onChanging(event:LocationChangeEvent):void {
event.preventDefault();
navigateToURL(new URLRequest(event.location));
}
// when the user clicks on the ad
function onChange(event:LocationChangeEvent):void {
if (event.location != local.url) {
navigateToURL(new URLRequest(event.location));
try {
view.loadURL(local.url);
} catch(error:Error) {
}
}
}
function onViewError(error:ErrorEvent):void {
trace(error);
}
[/code]

Services and Authentication

Many services, such as Twitter and Facebook, require an authentication token that can only be obtained via an HTML web page. You can use StageWebView to call authentication methods to extract the token. Service data is then available and can be used inside your AIR application.

OAuth is a robust open standard authorization system. It allows users to hand out tokens instead of usernames and passwords. The token is for a specific site and resources for a defined duration.

Twitter uses OAuth. You can allow users to access their account for authentication and log in to your application so that they can use their account data seamlessly without leaving AIR. You need to register a URL on Twitter to obtain a Consumer key and a Consumer secret, both of which are necessary in your application (see http://twitter.com/oauth/ for more information).

Sandro Ducceschi offers a library called Tweetr that supports pinless OAuth (see http://wiki.swfjunkie.com/tweetr). For a similar application for Facebook, refer to Mark Doherty’s example at http://www.flashmobileblog.com/2010/10/14/facebook-connect-with-air-on-android/.

Limitations

Despite its benefits, StageWebView also has some limitations:

  • You cannot layer elements on top of StageWebView, although you can simulate it by taking a screen grab as described earlier.
  • The interaction between ActionScript and JavaScript is very limited. There is currently no support for ExternalInterface, even though you can fake URLs to communicate from HTML to AS.
  • You should not use StageWebView as a substitute for the native browser. Use it only as a bridge between your application and some needed Internet content. Android devices, for instance, do not support the QuickTime codec.
  • There is no direct way in ActionScript to prevent StageWebView instances from accepting cookies or to clear cookies.
  • The HTMLLoader class is supported in AIR for the desktop but not in AIR for Android.

If your application requires more advanced HTML features, consider using other tools. PhoneGap (http://www.phonegap.com), for instance, is an open source framework for developing applications for Android, iPhone, iPad, BlackBerry, and Symbian devices.

StageWebView and The Native Browser

StageWebView

WebKit is a layout engine that browsers use to render web pages and to support interactivity and navigation history. Developed by Apple in 2002 and open sourced in 2005, WebKit is the default browser built in for Android.

The AIR runtime for the desktop also includes a version of WebKit built in. However, AIR for Android does not include it, and instead leverages the device’s native implementation and gives you access to some of its features via StageWebView.

StageWebView brings the benefits of an Internet browser to your own application. In this chapter, we will briefly discuss accessing the native browser, and then go over what StageWebView has to offer.

The Native Browser

To access the Internet, you need to set the permission for it:

[code]<uses-permission android:name=”android.permission.INTERNET” />[/code]

You can launch the Android native browser from an AIR application using the naviga teToURL method. You pass a URL in the same way you do on AIR for the desktop:

[code]

function onPublicNews():void {
navigateToURL(new URLRequest(“http://www.npr.org”));
}

[/code]

You can also use this method to launch native applications, such as the Android Market, that in turn makes calls to the Internet. The following example opens the Android Market and sets the criteria of applications to display as a URLRequest. Note that the protocol used is market:// instead of http://:

[code]

function onSearchMarket():void {
navigateToURL(new URLRequest(“market://search?q=food”));
}

[/code]

In both cases, your application moves to the background and the native browser becomes the active application.

We need, instead, a solution whereby HTML content can be embedded inside an AIR for Android application. This is what the StageWebView class does.

 

Tuesday: Setting Up Google Analytics

Google Analytics is a robust analytics program that can help you gain valuable insight into your PPC performance. You can see where visitors are dropping out of your shopping process; you can see how long they stay on your landing page; and you can learn on which pages visitors convert most highly. The best part is, Google Analytics is free!

When you analyze them properly, you can gain invaluable, numerous insights that are from Google Analytics. We could write an entire book on installing and analyzing web analytics, but luckily we don’t have to! As we mentioned last week, a great resource for learning everything you need to know about analytics is Web Analytics: An Hour a Day, which shows you how to slice and dice your information so that you become a master of actionable analysis.

For now, here are the basic steps for getting Google Analytics installed on your website and landing pages:

  1. Open a Google Analytics account. You can do this by going to www.Google.com/Analytics.
  2. After you’re in your account, click Add Website Profile.
  3. Enter the URL of your website, select your country, and choose a time zone.
  4. The next page displays the universal tracking code that needs to be inserted on every page of your website. Copy the code.
  5. Insert the snippet of Google Analytics code on every page of your website.

After you complete this process, you should link your Google AdWords account to your Google Analytics account. This way, your PPC traffic is tracked correctly in Google Analytics, and you can access your Google Analytics account directly through AdWords. To link the accounts, follow these steps:

  1. Click the Reporting tab within AdWords.
  2. Choose Google Analytics from the drop-down menu.
  3. Enter your Google Analytics account ID to link the two accounts.

 

 

Web-Based E-zines, Email E-zines, Using E-zines as Marketing Tools

Web-Based E-zines

There are many Web-based e-zines that have only an online presence. These e-zines are accessed through Web sites by browsing from page to page. They have the look and feel of traditional magazines and include lots of pictures and advertisements. Usually there is no charge to view Web-based e-zines, but some do charge a subscription fee. These Web-based e-zines tend to be as graphically pleasing as offline magazines.

Email E-zines

Although email e-zines can come as text or as HTML, these days we are seeing most in HTML as they get a much higher readership. Today we are seeing a blur between newsletters and email e-zines as most newsletters now are sent as HTML and most are content-rich on a specific subject.

Email-based e-zines tend to be very content-rich and, as such, tend to be more of a target-marketing mechanism. Email e-zines tend to be several screens in length with one main article or several short articles and, sometimes, they include classified advertising. The benchmark is that these e-zines should be able to be read in about five minutes. Circulation is often in the thousands. Most run weekly or biweekly editions and are free to subscribers.

People interested in the subject of the e-zine have taken the time to subscribe and have asked to receive the information directly in their email box. Once you have found an e-zine that caters to your target market, that e-zine could be a valuable marketing vehicle.

A major advantage when you advertise in this type of medium and place your Internet address in the ad is that your prospective customer is not more than a couple of clicks away from your site.

People subscribe to e-zines because they are interested in the information that is provided. Even if they don’t read it immediately when it is received, they usually read it eventually. Otherwise, they would not have subscribed. No matter when they take the time to read it, if you advertise in these e-zines or have your business, products, or services profiled, subscribers will see your URL and advertisements. For this reason, email e-zines are a great marketing tool.

Using E-zines as Marketing Tools

Online publications are superior marketing tools for online businesses for several reasons. They can be used in a number of ways to increase the traffic to your Web site. You can:

  • Advertise directly
  • Be a sponsor
  • Submit articles
  • Send press releases
  • Be a contributing editor
  • Start your own e-zine.

 

Vibration Alerts and Web Browser

Vibration Alerts

Application code can initiate the vibration capability of the device to alert users of events occurring in the application. To use this feature,
you must do the following:

  • Add a reference to the namespace Microsoft.Devices to your class.
  • Call the static methods of the default VibrateController class to start and stop vibration.

To initiate vibration for the default duration, call the Start method. To specify the duration, include a TimeSpan that defines the duration of the vibration as the parameter of the Start method. To stop vibration, call the Stop method.

C#
VibrateController.Default.Start(); // Default duration
VibrateController.Default.Start(TimeSpan.FromSeconds(2));
VibrateController.Default.Stop();

Web Browser

You can invoke the Windows Phone 7 web browser in your applications using the WebBrowserTask class. You simply specify the URL
to open, and call the Show method. The following code example shows how to do this.

C#
WebBrowserTask browserTask = new WebBrowserTask();
browserTask.URL = “http://www.microsoft.com/windowsmobile”;
browserTask.Show();

For more information, see “WebBrowserTask Class” on MSDN (http://msdn.microsoft.com/en-us/library/microsoft.phone.tasks.webbrowsertask(VS.92).aspx).

Wednesday: Determining the Number and Granularity of Ad Groups

A common question we get when making PPC presentations is, “How many keywords should you include in an ad group?”

There is no single correct numerical answer to this question. Remember our mantra: (Almost) every keyword should appear in the ad text. This really means that you should split your keyword lists into as many small subsets as possible. Here again is the example we used in the previous section:

Keywords

  • Women’s hiking boots
  • Hiking boots for women
  • Best hiking boots for women
  • Buy women’s hiking boots

Ad

Buy Women’s Hiking Boots
Hiking Boots for All Terrains.
Name Brands. 20% off. Buy Now!
www.AwesomeShoeStore.com/Hiking+Boots

This ad doesn’t include the words compare and online. But that’s OK. The most important thing here is that the ad is displayed when the searcher uses a query related to women’s hiking boots, and your ad pertains specifically to that kind of shoe. It mentions hiking boots twice, and highlights all terrains and name brands, which could be important benefits to an online shopper. So although you don’t have every word from the keyword list in the ad, you certainly have included  the most important ones.

You should now see that it’s OK to create ad groups with thousands of keywords, and other ad groups with only one keyword in each. For example, because you won’t typically need to create a separate ad group for each individual misspelling, each of thousands of URL variations can coexist in a single ad group. The limit to the granularity of such ad groups will be the limit of the number of keywords that can be included in a single ad group, which varies from engine to engine, but numbers in the thousands.

Understanding the Benefits of a Well-Structured Account

You’ve listened to us preach about the importance of a well-structured PPC account. We walked you through the process of setting up campaigns and ad groups that are tightly grouped with small keyword lists and ads that feature most keywords from each ad group. Let’s look now at the specific benefits of well-structured accounts:

Faster reporting and analysis: If your keywords are scattered all over your account with little or no rhyme or reason, it’s going to be extremely diffi cult to analyze the account’s performance and make improvements. When you run reports for your campaigns, ad groups, keywords, ads, or any other aspect of your account, you need to be able to look at the data and draw conclusions. With poorly structured campaigns and ad groups, reporting will take a lot more time, it will be signifi cantly less reliable, and trending will be diffi cult to determine.

Effi cient account management: After your PPC account is up and running, you want to be able to make changes quickly and effi ciently in order to enhance the account’s performance. If your account is set up properly, you can make notes and formulate an optimization strategy quickly, and drive straight into your account with confi dence that you know exactly what needs to be done and exactly where the strengths and weaknesses lie.

Better PPC ad copy: The more tightly themed your ad groups are, the better targeted your PPC ad copy will be. Yes, the chanting of our mantra continues: Your ad copy should be able to feature almost all of the keywords in your ad group. If you have to use dynamic keyword insertion (DKI), or if you have so many keywords that your ad copy can’t feature them, you have some ad-group segmentation to do. Remember, highly relevant, benefi t-driven, keyword-focused PPC ads and landing pages are the crux of a successful PPC account. You have to get the account structure right for all of this to occur.

Higher quality scores: Great quality scores hinge on your CTR and the relationship of your PPC ad text to the keywords within your ad group. Your quality score can get a lift when Google can easily determine the theme of your ad group, because all of the keywords are relevant to your ad text and they’re relevant to each other. With a higher quality score, you will pay less per click, and your ad position will improve.

Media

Windows Phone 7 physical devices can store and display photos, play video and audio files, and have a built-in FM radio. To display photos and images, you can use the standard Silverlight controls such as the Image control. You can include the image file in your project and set the Build Action property to Content and the Copy to Output Directory property to Copy if newer or Copy Always, so that it is included in the XAP file that uploads to the device. You then specify the image as the source for the control. Alternatively, you can specify a remote URL for the image, and it will be downloaded and displayed
just as in a web browser. Images must be in JPEG (.jpg) or PNG (.png) format.

To display a video or play an audio file, you can use the Silverlight MediaElement control. You can include the video or audio file in your project and set the Build Action property to Content and the Copy to Output Directory property to Copy if newer or Copy Always, and then specify it as the source of the control.

XML
<MediaElement Source=”MyVideo.wmv” Width=”300″ Height=”300″
AutoPlay=”True”/>

Alternatively, you can specify a remote URL for the video or audio file, and it will be streamed or downloaded and played just as in a Web browser. For a list of the supported video and audio formats, see “Supported Media Codecs for Windows Phone” on MSDN (http://msdn.microsoft.com/en-us/library/ff462087(VS.92).aspx). For information about using the MediaElement control, see “MediaElement” on MSDN (http://msdn.microsoft.com/en-us/library/bb980132(VS.95).aspx).

An alternative approach for video and audio files is to use the MediaPlayerLauncher task. You create an instance of this class, set the required properties, and the call the Show method. In this example, the Build Action property of the video file is set to Content and the Copy to Output Directory property is set to Copy if newer so that the video file is included in the XAP file uploaded to the device.

C#
MediaPlayerLauncher mediaLauncher = new MediaPlayerLauncher();
mediaLauncher.Controls = MediaPlaybackControls.All;
mediaLauncher.Location = MediaLocationType.Install;
mediaLauncher.Media = new Uri(“MyVideo.wmv”, UriKind.Relative);
mediaLauncher.Show();

Alternatively, you can copy the media item to isolated storage on the device and specify the MediaLocation property as Media LocationType.Data. For more information, see “MediaPlayerLauncher Class” on MSDN (http://msdn.microsoft.com/en-us/library/microsoft.phone.tasks.mediaplayerlauncher(VS.92).aspx).

Note: When using the MediaPlayerLauncher task in the emulator, you may find that video frames render only if you repeatedly click on the UI. Each click advances the video by one frame and then the display turns black.

Selecting a Photo on the Device

You can select an existing photo from the user’s media collection using the PhotoChooserTask class. You specify a callback handler to execute when the user selects a photo, and optionally the size for the picture that will be returned.

C#
PhotoChooserTask photoTask = new PhotoChooserTask();
photoTask.Completed += new EventHandler<PhotoResult>
(PhotoTask_Completed);
photoTask.PixelHeight = 500;
photoTask.PixelWidth = 500;
photoTask.Show();

When the user selects a picture, the callback can obtain the name of the picture and the Stream instance pointing to it. You must decode the byte stream into an appropriate image format for display, such as in an Image control on the page, as shown here. To use this code, you must add a reference to the namespaces System.Windows. Media.Imaging and Microsoft.Phone (for the PictureDecoder class).

C#
void PhotoTask_Completed(object sender, PhotoResult result)
{
if (result.Error == null && result.TaskResult == TaskResult.OK)
{
string fileName = result.OriginalFileName;
WriteableBitmap thePhoto = PictureDecoder.DecodeJpeg(
result.ChosenPhoto);
MyImageControl.Source = thePhoto;
}
}

For more information about using media such as photos, video, and audio on Windows Phone 7, see “Media for Windows Phone” (http://msdn.microsoft.com/en-us/library/ff402560(VS.92).aspx) and “Photos for Windows Phone” (http://msdn.microsoft.com/en-us/library/ff402553(VS.92).aspx) on MSDN.

 

Friday: Researching Your Competitor’s Keywords

Monitoring your competitor’s PPC efforts is a good way to keep your edge. By conducting competitive research, you can discover new variations of your core keywords, or completely new ones. There are reasons to take on your competitors head-on for the same keywords, and there are other reasons to make flanking maneuvers in order to avoid direct competition. You can think of competitive research as both a defensive and an offensive tactic.

First, let’s cover the defensive aspect of competitive research. There are going to be core keywords that will drive your business. Often there are several competitors who are also bidding on these core keywords. As we said earlier, you want to have bid on many variations of your core keywords, because your target audience is going to be made up of people from varying backgrounds and locations. Your competitors may have thought of terms that you’ve missed. Adding variations that you discover through competitive research can help you maintain a position as the toughest competitor.

Another goal of competitive research is to gain an understanding of your competitors’ messaging. What are your competitors saying in their PPC ads? Here’s what to look for:

Headline: Does their headline speak more clearly to your target audience? If so, why? If not, why not? Make a list of reasons why their headline is better than yours.

Ad copy: Are your competitors utilizing their limited ad copy more efficiently than you? What benefits and features do they highlight? Make a list of benefits you’re not using in your ads.

Call to action: How are your competitors motivating users to click on their PPC ads? Are they offering special deals, free shipping, or free information? Write down reasons why their offers are more compelling than yours.

Some people say the best defense is a good offense. Researching your competitors’ keywords will also help you think of new terms that neither you nor they are targeting. During this process, you’re looking for holes in your competitors’ keyword armor, so to speak. The motivation here isn’t to go head-on against your competition; rather, you are looking for terms that all your competitors are missing.

Now that you understand how competitive research is both a defensive and an offensive tactic, how exactly do you find out which keywords your competitors are targeting? You will never know exactly which terms your competitors are bidding on. None of the search engines will provide you with this information, and there is no reputable third-party tool that will provide precise insight into your competitors’ PPC keywords.

Competitive analysis shouldn’t be the cornerstone of your keyword research. Instead, this process should supplement the construction of your keyword list and make it stronger. There are numerous tools that you could buy to provide good insight into your competitors’ PPC activities, but to get you started, we’ll describe a few ways that you can do such research on the cheap.

The Ad Preview Tool

By now, you should have a grasp on which keywords are mission critical for your campaign. To gauge the level of competition for each term, you should conduct a search query for each of these. This will give you an idea of how competitive your core terms are, and let you create a list of the competitors with the highest visibility.

To conduct these competitive search queries, you should use the Google Ad Preview tool (https://adwords.google.com/select/AdTargetingPreviewTool). By using this tool, you won’t generate false impressions for yourself or your competitors (let’s play fair!). Another reason to use the tool: Each subsequent search query on Google may cause a different set of ads to be displayed. Google does this because if you don’t click any ads during a single search session, it’s assumed you are not interested in the set of ads being displayed, and eventually no ads will be displayed for the queries you are making from your computer and your IP address. The Google Ad Preview tool helps you avoid these issues.

When you conduct a search query by using the Google Ad Preview tool, you are not actually conducting a live search on Google; you are getting a preview of what the SERP may look like for that particular keyword. Figure 4.14 shows the results of a search conducted using the term organic white tea.

The Google Ad Preview tool can give you an idea of how a SERP will appear for your core keywords.

As you can see in the figure, the Ad Preview tool enables you to view ads as if you were searching from a different location. You can select to view ads as they may appear in a different country, state, region, and city, or even a location defined by longitude and latitude coordinates. This way, you can see how search results look in locations other than your own. For example, if you live in Indiana but you want to see what ads are being displayed in California for a certain term, you can do this by using the Ad Preview tool.

Competitors’ Website Review

Believe it or not, your competitors may just simply tell you their core keywords. For SEO purposes, companies will utilize a line of code called the meta keyword tag, which tells the search engines which keywords are most important to this particular page of their website.

Where do you find this information? When viewing your competitor’s website home page, at the top of the browser you should click View and then select Page Source from the drop-down menu that appears. A new window will open that displays the HTML code of this page. The meta keyword tag is usually located at the top of the code, and looks similar to Figure 4.15.

A home page’s meta name, meta description, and meta keywords tags

Third-Party Tools

The tools in this section are not free, but their publishers offer free features. They can give you some insight into your competitors’ keywords, messaging within their PPC ads, and traffic trends for their websites. Here are some of our favorites:

Compete (www.compete.com): As with any of the tools listed in this section, you’ll get much more competitive information with the paid version of this software, but the free version will get you started. For example, we entered a competitor’s website into Compete and received quite a bit of useful information, as you can see in Figure 4.16 and Figure 4.17.

SpyFu (www.spyfu.com): SpyFu offers two methods of competitive research: by keyword and by URL. When searching by keyword, you are provided with quite a bit of information, including projected CPC, clicks per day, and the average number of advertisers. Also, you can see top PPC domains for this term as well as samples of PPC ads. The information provided by SpyFu is helpful, and Figure 4.18 shows just the top of the results page—additional information is provided on the rest of the page.

 

In Compete, you can see traffic trends for your competitors, as well as visits and unique visitors.

You can also gain additional insight from Compete, such as site description, top referring websites, and some keyword ideas.

SpyFu can indicate how competitive a keyword may be and show the main advertisers bidding on the term.

You can use SpyFu to conduct competitive research by using a specific URL, and get the projected daily AdWords spending range, average ad position, top 10 paid keywords, and other information. You should not, however, take this information as solid fact, but rather as a point of reference. Figure 4.19 shows a sample URL analysis from SpyFu.

A SpyFu URL analysis

KeywordSpy (www.keywordspy.com): If you went ahead and hopped on the Compete and SpyFu bandwagons, you already have quite a bit of information about your competitors. Like the others, KeywordSpy provides speculative stats, top keywords, main competitors, and PPC ad variations. However, it also provides top organic keywords and competitors.