Stopwatch

0
184

The Stopwatch app enables you to time any event with a start/stop button and a reset button. When running, the reset button turns into a “lap” button that adds an intermediate time to a list at the bottom of the page without stopping the overall timing. This is a common stopwatch feature used in a number of sporting events.

Stopwatch displays an interesting bar toward the top that visualizes the progress of the current lap compared to the
length of the preceding lap. (During the first lap, the bar is a solid color, as it has no relative progress to show.) With
this bar and the start/stop button, this app shows that you can use a bit of color and still fit in with the Windows Phone style. Not everything has to be black, white, and gray!

Stopwatch supports all orientations, but it provides an “orientation lock” feature on its application bar that enables users to keep it in portrait mode when holding the phone sideways, and vice versa. This is a handy feature that other apps in this book share. The main downside is that if future phones have a built-in orientation lock feature, this functionality will be redundant for those phones.

The orientation lock is a neat trick, but this app does something even more slick. It provides the illusion of running in
the background. You can start the timer, leave the app (even reboot the phone!), return to it 10 minutes later, and see the timer still running with 10 more minutes on the clock. Of course, Stopwatch, like all third-party apps at the time of writing, is unable to actually run in the background. Instead, it remembers the state of the app when exiting—including the current time—so it can seamlessly continue when restarted and account for the missing time.

This app takes advantage of previously unseen stack panel and grid features to produce a relatively-sophisticated user interface that looks great in any orientation. Therefore, before building Stopwatch, this chapter examines how Silverlight layout works and describes the features provided by stack panel and grid.

Controlling Layout with Panels

Sizing and positioning of elements in Silverlight is often called layout. A number of rich layout features exist to create flexible user interfaces that can act intelligently in the face of a number of changes: the screen size changing due to an orientation change, elements being added and removed, or elements growing or shrinking—sometimes in ways you didn’t originally anticipate, such as later deciding to translate your app’s text into a different language.

One piece to the layout story is a number of properties on individual elements: the size properties discussed in the preceding chapter (Width, MinWidth, MaxWidth, Height, MinHeight, and MaxHeight) and some alignment properties introduced later in this chapter. The other piece is a handful of elements known as panels, whose job is to arrange child elements in specific ways. Windows Phone 7 ships with five panels:

  • Stack Panel
  • Grid
  • Canvas
  • Virtualizing Stack Panel
  • Panorama Panel

The virtualizing stack panel is just like a stack panel, but with performance optimizations for databound items (delaying the creation of off-screen elements until they are scrolled onto the screen and recycling item containers). This panel is used as an implementation detail for controls such as a list box, and is normally not used directly unless you are designing your own list control. The panorama panel is also an implementation detail of the panorama control discussed in Part IV of this book, “Pivot, Panorama, Charts, & Graphs,” and is not meant to be used directly.

You can arbitrarily nest panels inside each other, as each one is just a Silverlight element. You can also create your own custom panels by deriving from the abstract Panel class, although this is not a common thing to do.

Stack Panel

The stack panel is a popular panel because of its simplicity and usefulness. As its name suggests, it simply stacks its children sequentially. Although we’ve only seen it stack its children vertically, you can also make it stack its children horizontally by setting its Orientation property to Horizontal rather than the default Vertical.

Figure 4.1 renders the following XAML, which leverages a horizontal stack panel to provide a hypothetical user interface for entering a social security number (three groups of digits separated by dashes):

[code]<StackPanel Orientation=”Horizontal”>
<TextBox Width=”80”/>
<TextBlock Text=”-” VerticalAlignment=”Center”/>
<TextBox Width=”65”/>
<TextBlock Text=”-” VerticalAlignment=”Center”/>
<TextBox Width=”80”/>
</StackPanel>[/code]

The VerticalAlignment property is discussed later, in the “Alignment” section.

FIGURE 4.1 Five elements stacked in a horizontal stack panel create a form for entering a Social Security number.
FIGURE 4.1 Five elements stacked in a horizontal stack panel create a form for entering a Social Security number.

Grid

Grid is the most versatile panel and the one apps use most often for the root of their pages. (Apps that don’t use a grid tend to use a canvas, which is good for games and certain novelty apps.) Grid enables you to arrange its children in a multirow and multicolumn fashion, with many features to control the rows and columns in interesting ways. Working with grid is a lot like working with a table in HTML.

When using a grid, you define the number of rows and columns by adding that number of RowDefinition and ColumnDefinition elements to its RowDefinitions and ColumnDefinitions properties. (This is a little verbose but handy for giving individual rows and columns distinct sizes.) By default, all rows are the same size (dividing the height equally) and all columns are the same size (dividing the width equally). When you don’t explicitly specify any rows or columns, the grid is implicitly given a single cell.

You can choose a specific cell for every child element in the grid by using Grid.Row and Grid.Column, which are zero-based indices. When you don’t explicitly set Grid.Row and/or Grid.Column on child elements, the value 0 is used. Figure 4.2 demonstrates the appearance of the following grid:

[code]<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Button Content=”0,0”/>
<Button Grid.Column=”1” Content=”0,1”/>
<Button Grid.Row=”1” Content=”1,0”/>
<Button Grid.Row=”1” Grid.Column=”1” Content=”1,1”/>
</Grid>[/code]

FIGURE 4.2 Four buttons in a 2x2 grid.
FIGURE 4.2 Four buttons in a 2×2 grid.

Grid.Row and Grid.Column are called attachable properties because although they are defined by the Grid class, they can be attached to other elements in XAML. (Any XAML attribute whose name includes a period is an attachable property. The identifier to the left of the period is always the class defining the property named to the right of the period.)

Note that the size of the grid and the appearance of its contents, which usually stretch in both dimensions to fill each cell, depends on the grid’s parent element (or size-related properties on the grid itself). Figure 4.3 shows what the same grid from Figure 4.2 looks like if it is used to fill an entire page.

FIGURE 4.3 The grid from Figure 4.2, used to fill an entire page.
FIGURE 4.3 The grid from Figure 4.2, used to fill an entire page.

Multiple Elements in the Same Cell Grid cells can be left empty, and multiple elements can appear in the same cell. In this case, elements are simply rendered on top of one another according to their ordering in XAML. (Later elements are rendered on top of earlier elements.) You can customize this order, often called the z order or z index) by setting the
Canvas.ZIndex attachable property on any element—even though this example has nothing to do with a canvas!

Canvas.ZIndex is an integer with a default value of 0 that you can set to any number (positive or negative). Elements with larger values are rendered on top of elements with smaller values, so the element with the smallest value is in the back, and the element with the largest value is in the front. If multiple children have the same value, the order is determined by their order in the grid’s collection of children, as in the default case. Figure 4.4 shows what the following XAML produces, which contains empty cells and cells with multiple elements:

[code]<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<!– These are both in cell 0,0 –>
<Button Canvas.ZIndex=”1” Foreground=”Aqua” Content=”on top”/>
<Button Content=”on bottom”/>
<!– These are both in cell 1,1 –>
<Button Grid.Row=”1” Grid.Column=”1” Content=”on bottom”/>
<Button Grid.Row=”1” Grid.Column=”1” Foreground=”Red” Content=”on top”/>
</Grid>[/code]

FIGURE 4.4 Four buttons in a 2x2 grid, demonstrating overlap and the use of Canvas.ZIndex.
FIGURE 4.4 Four buttons in a 2×2 grid, demonstrating overlap and the use of Canvas.ZIndex.

Spanning Multiple Cells

Grid has two more attachable properties—Grid.RowSpan and Grid.ColumnSpan, both 1 by default—that enable a single element to stretch across multiple consecutive rows and/or columns. (If a value greater than the number of rows or columns is given, the element simply spans the maximum number that it can.) Therefore, the following XAML produces
the result in Figure 4.6:

[code]
<Grid ShowGridLines=”True”>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Button Grid.ColumnSpan=”3” Content=”0,0 – 0,2”/>
<Button Grid.Row=”1” Grid.RowSpan=”2” Content=”1,0 – 2,0”/>
<Button Grid.Row=”1” Grid.Column=”1” Grid.RowSpan=”2” Grid.ColumnSpan=”2”
Content=”1,1 – 2,2”/>
</Grid>
[/code]

FIGURE 4.6 Using Grid.RowSpan and Grid.ColumnSpan to make elements expand beyond their cell.
FIGURE 4.6 Using Grid.RowSpan and Grid.ColumnSpan to make elements expand beyond their cell.

Customizing Rows and Column Sizes Unlike a normal element’s Width and Height properties, RowDefinition’s and
ColumnDefinition’s corresponding properties do not default to Auto. Also, they are of type GridLength rather than
double, enabling grid to uniquely support three different types of sizing:

  • Absolute sizing—Setting Width or Height to a number of pixels. Unlike the other two types of sizing, an absolute-sized row or column does not grow or shrink as the size of the grid or size of the elements changes.
  • Autosizing—Setting Width or Height to Auto, which gives child elements the space they need and no more. For a row, this is the height of the tallest element, and for a column, this is the width of the widest element.
  • Proportional sizing (sometimes called star sizing)—Setting Width or Height to special syntax to divide available space into equal-sized regions or regions based on fixed ratios. A proportional-sized row or column grows and shrinks as the grid is resized.

Absolute sizing and autosizing are straightforward, but proportional sizing needs more explanation. It is done with star syntax that works as follows:

  • When a row’s height or column’s width is set to *, it occupies all the remaining space.
  • When multiple rows or columns use *, the remaining space is divided equally between them.
  • Rows and columns can place a coefficient in front of the asterisk (like 2* or 5.5*) to take proportionately more space than other columns using the asterisk notation. A column with width 2* is always twice the width of a column with width * (which is shorthand for 1*) in the same grid. A column with width 5.5* is always twice the width of a column with width 2.75* in the same grid.

The “remaining space” is the height or width of the grid minus any rows or columns that use absolute sizing or autosizing. Figure 4.7 demonstrates these different scenarios with simple columns in four different grids.

FIGURE 4.7 Proportional-sized grid columns in action.
FIGURE 4.7 Proportional-sized grid columns in action.

The default height and width for grid rows and columns is *, not Auto. That’s why the rows and columns are evenly distributed in Figures 4.2 through 4.6.

The User Interface

Listing 4.1 contains the XAML for Stopwatch, which uses a seven-row, two-column grid to arrange its user interface in a manner that works well for both portrait and landscape orientations. Figure 4.8 shows the user interface in two orientations, and with grid lines showing to help you visualize how the grid is being used.

FIGURE 4.8 The Stopwatch user interface with grid lines showing.
FIGURE 4.8 The Stopwatch user interface with grid lines showing.

LISTING 4.1 MainPage.xaml—The User Interface for Stopwatch

[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”
FontFamily=”{StaticResource PhoneFontFamilyNormal}”
FontSize=”{StaticResource PhoneFontSizeNormal}”
Foreground=”{StaticResource PhoneForegroundBrush}”
SupportedOrientations=”PortraitOrLandscape”
shell:SystemTray.IsVisible=”True”>
<!– Application bar containing the orientation lock button –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar>
<shell:ApplicationBarIconButton Text=”lock screen”
IconUri=”/Shared/Images/appbar.orientationUnlocked.png”
Click=”OrientationLockButton_Click”/>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<Grid>
<!– This grid has 6 rows of varying heights –>
<Grid.RowDefinitions>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”4*”/>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”3*”/>
</Grid.RowDefinitions>
<!– This grid has 2 equal-width columns –>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<!– Row 0: The standard header, with some tweaks –>
<StackPanel Grid.Row=”0” Grid.ColumnSpan=”2” Margin=”24,16,0,12”>
<TextBlock Text=”STOPWATCH” Margin=”-1,0,0,0”
FontFamily=”{StaticResource PhoneFontFamilySemiBold}”
FontSize=”{StaticResource PhoneFontSizeMedium}”/>
</StackPanel>
<!– Row 1: “current lap” text block –>
<TextBlock Grid.Row=”1” Grid.ColumnSpan=”2” Text=”current lap”
HorizontalAlignment=”Right”
Style=”{StaticResource PhoneTextSubtleStyle}”/>
<!– Row 2: current lap time display –>
<local:TimeSpanDisplay x:Name=”CurrentLapTimeDisplay” Grid.Row=”2”
Grid.ColumnSpan=”2” DigitWidth=”18”
HorizontalAlignment=”Right” Margin=”0,0,12,0”
FontSize=”{StaticResource PhoneFontSizeLarge}”/>
<!– Row 3: total time display and progress bar –>
<local:TimeSpanDisplay x:Name=”TotalTimeDisplay” Grid.Row=”3”
Grid.ColumnSpan=”2” DigitWidth=”67”
HorizontalAlignment=”Center”
FontFamily=”Segoe WP Black” FontSize=”108” />
<ProgressBar x:Name=”LapProgressBar” Grid.Row=”3” Grid.ColumnSpan=”2”
VerticalAlignment=”Top” />
<!– Row 4: the buttons:
Column 0: start and stop
Column 1: reset and lap –>
<Button Name=”StartButton” Grid.Row=”4” Content=”start” Margin=”12,0,0,0”
Foreground=”White” BorderBrush=”{StaticResource PhoneAccentBrush}”
Background=”{StaticResource PhoneAccentBrush}”
Click=”StartButton_Click” local:Tilt.IsEnabled=”True” />
<Button Name=”StopButton” Grid.Row=”4” Content=”stop” Margin=”12,0,0,0”
Foreground=”White” BorderBrush=”#E51400” Background=”#E51400”
Click=”StopButton_Click” local:Tilt.IsEnabled=”True”
Visibility=”Collapsed”/>
<Button Name=”ResetButton” Grid.Row=”4” Grid.Column=”1” Content=”reset”
IsEnabled=”False” Margin=”0,0,12,0” Click=”ResetButton_Click”
local:Tilt.IsEnabled=”True” />
<Button Name=”LapButton” Grid.Row=”4” Grid.Column=”1” Content=”lap”
Margin=”0,0,12,0” Click=”LapButton_Click” local:Tilt.IsEnabled=”True”
Visibility=”Collapsed”/>
<!– Row 5: the list of laps –>
<ScrollViewer Grid.Row=”5” Grid.ColumnSpan=”2”
FontSize=”{StaticResource PhoneFontSizeLarge}”>
<StackPanel x:Name=”LapsStackPanel” />
</ScrollViewer>
</Grid>
</phone:PhoneApplicationPage>
[/code]

Notes:

  • The auto-sized rows are important not just for sequential stacking behavior at the top, but to ensure that the contents in those rows are never clipped. With the remaining space, the row with the total elapsed time is given 33% more height than the row with the list of laps (4* versus 3*). This was chosen to ensure that there’s enough room for the total elapsed time in a landscape orientation.
  • On my first attempt at creating this user interface, I used a text block for both time displays (in rows 2 and 3), but realized there is a problem with this approach. Segoe WP (and its related fonts) are proportional-width fonts, so the time displays jiggle as the values change. Windows Phone ships one fixed-width font—Courier New—but
    using this font looked out-of-place. Therefore, this page instead uses a TimeSpanDisplay user control that is able to display time values in a fixed-width (non-jiggly) fashion, even with a proportional-width font like Segoe WP. This difference is demonstrated in Figure 4.9, involving the most narrow digit (1) and the widest digit (4).
FIGURE 4.9 Three potential display options for the same time span.
FIGURE 4.9 Three potential display options for the same time span.
  • The line that shows the current lap progress compared to the previous lap is implemented with a progress bar in row 3. Progress bars are discussed in the upcoming “Progress Bars” section.
  • In row 4, the start/stop button on the left is actually two different buttons, with only one visible at a time. The same is true for reset and lap on the right. This makes the code-behind a bit nicer, but the primary motivation for this is that the start and stop buttons have different background colors. If they were combined into a single button, we would need to change its background dynamically, and this is harder than you would expect. (This is explained in an upcoming FAQ sidebar.)
  • Unlike the application bar and status bar that are special shell controls, normal Silverlight elements do not have a Boolean IsVisible property. Instead, they have a Visibility property that can be set to either Visible or Collapsed
    (Visibility.Visible or Visibility.Collapsed in C#). This oddity comes from Silverlight’s roots in Windows  Presentation Foundation (WPF), which defines a third possible value for Visibility.
  • The buttons are given outer margins to make the space on their outer edges match the space between the left and right buttons (24 pixels). The stop button is given a red background, but it uses the value #E51400 rather than simply red because the former is the official red color available as one of the theme accent color choices. (Silverlight’s definition of red, on the other hand, is the standard #FF0000.) Now that you’re familiar with attachable properties, you should recognize that the tilt effect applied to the buttons is a class called Tilt with an attachable property called IsEnabled. Tilt.IsEnabled is used throughout this book. The code for this class is included with this book’s source code.
  • Although the bottom row starts out empty, a new item gets dynamically added to the stack panel from the code-behind each time LapButton is tapped. The stack panel is wrapped in a scroll viewer so it can scroll when the list is taller than the grid row.
  • The PhoneAccentBrush theme resource is used as the background (and border) of StartButton. This refers to the accent color from the user’s current theme. Combined with the fact that the progress bar automatically uses the same color, this app conforms to user preferences nicely. Figure 4.10 shows how the appearance changes when
    the light theme is used with a pink accent color.
FIGURE 4.10 The progress bar and customized start button use the accent color from the user’s theme.
FIGURE 4.10 The progress bar and customized start button use the accent color from the user’s theme.

Two aspects of this user interface deserve a deeper look: its use of alignment properties, and its use of a progress bar.

Alignment

Often, elements are given more space than they need. How they use this space depends on their values of HorizontalAlignment and VerticalAlignment. Just about any sophisticated user interface will have some occasions to customize the alignment of elements. Indeed, Listing 4.1 uses these properties on a few elements to override the default behavior that would cause them to stretch and fill their grid cell. Each property has a corresponding enumeration with the same name, giving the following options:

  • HorizontalAlignment—Left, Center, Right, and Stretch
  • VerticalAlignment—Top, Center, Bottom, and Stretch

Stretch is the default value for both properties, although some elements implicitly override this setting. The effects of HorizontalAlignment can easily be seen by placing a few buttons in a vertical stack panel and marking them with each value from the enumeration:

[code]
<StackPanel>
<Button HorizontalAlignment=”Left” Content=”Left” Background=”Red”/>
<Button HorizontalAlignment=”Center” Content=”Center” Background=”Orange”/>
<Button HorizontalAlignment=”Right” Content=”Right” Background=”Green”/>
<Button HorizontalAlignment=”Stretch” Content=”Stretch” Background=”Blue”/>
</StackPanel>
[/code]

The rendered result appears in Figure 4.11.

These alignment properties are useful only on an element whose parent has given it more space than it needs. For example, adding VerticalAlignment values to the buttons in Figure 4.11 would make no difference, as each element is already given the exact amount of height it needs (no more, no less). In a horizontal stack panel, VerticalAlignment has an effect but HorizontalAlignment does not. In a grid, both alignments have an effect. For example, back in Listing 4.1, the top-aligned progress bar and the center-aligned total time display share the same two grid cells.

FIGURE 4.11 The effects of HorizontalAlignment on buttons in a stack panel.
FIGURE 4.11 The effects of HorizontalAlignment on buttons in a stack panel.

This differing behavior of parent panels can sometimes cause surprising results. For example, if the text boxes inside the horizontal stack panel back in Figure 4.1 did not have explicit widths, they would start out extremely narrow and grow as the text inside them grows (occupying as much space as each needs but no more). If you were to place a grid inside a vertical stack panel, it would no longer stretch vertically, and even its proportional-height rows would collapse to nothing if they were left empty!

Content Alignment

In addition to HorizontalAlignment and VerticalAlignment properties, elements deriving from Control also have
HorizontalContentAlignment and VerticalContentAlignment properties. These properties determine how a
control’s content fills the space within the control, if there is extra space. (Therefore, the relationship between alignment and content alignment is somewhat like the relationship between margins and padding.)

The content alignment properties are of the same enumeration types as the corresponding alignment properties, so they provide the same options but with different default values. The default value for HorizontalContentAlignment is Left, and the default value for VerticalContentAlignment is Top. However, some elements implicitly choose different defaults. Buttons, for example, center their content in both dimensions by default.

Figure 4.12 demonstrates the effects of HorizontalContentAlignment, simply by taking the previous XAML snippet and changing the property name as follows:

[code]

<StackPanel>
<Button HorizontalContentAlignment=”Left” Content=”Left” Background=”Red”/>
<Button HorizontalContentAlignment=”Center” Content=”Center”
Background=”Orange”/>
<Button HorizontalContentAlignment=”Right” Content=”Right” Background=”Green”/>
<Button HorizontalContentAlignment=”Stretch” Content=”Stretch”
Background=”Blue”/>
</StackPanel>

[/code]

The last button in Figure 4.12 probably does not appear as you expected. Internally, it uses a text block to display
the “Stretch” string, and that text block is technically stretched. However, text blocks do not support stretching
their text in this manner, so the result looks no different than a

FIGURE 4.12 The effects of HorizontalContentAlignment on buttons in a stack panel.
FIGURE 4.12 The effects of HorizontalContentAlignment on buttons in a stack panel.

HorizontalContentAlignment of Left. For other types of content, Stretch can indeed have the intended effect.

Progress Bars

This app’s use of a progress bar is definitely unorthodox, but appropriate in this author’s opinion. A progress bar has three basic properties: Value (0 by default), Minimum (0 by default), and Maximum (100 by default). As progress is being made (whatever that means for your app), you can update Value until it matches Maximum, which should mean that the work being measured is complete. You can choose different values of Minimum and Maximum if it’s more convenient for you to work on a scale different than 0–100.

In addition, progress bars have two properties for customizing their appearance:

  • IsIndeterminate—When set to true, this turns the bar into the standard “dancing dots” progress animation that runs as long as the progress bar is visible, making the values of Value, Minimum, and Maximum irrelevant. This is meant for times when you have no clue how long something will take, such as when you’re waiting for a network request to complete.
  • Orientation—This is set to Horizontal by default but can be set to Vertical to make progress go from bottom to top rather than left to right. Vertical progress bars are unusual, however, and the design guidelines recommend only using horizontal ones.

The Code-Behind

Listing 4.2 contains the code-behind for MainPage, which must perform all the timer logic in response to the four main buttons, implement the orientation lock feature, and persist/restore the app’s state across multiple uses.

LISTING 4.2 MainPage.xaml.cs—The Code-Behind for Stopwatch

[code]

using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Navigation;
using System.Windows.Threading;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
{
// Use a Setting for each “normal” variable, so we can automatically persist
// the values and easily restore them
Setting<TimeSpan> totalTime =
new Setting<TimeSpan>(“TotalTime”, TimeSpan.Zero);
Setting<TimeSpan> currentLapTime =
new Setting<TimeSpan>(“CurrentLapTime”, TimeSpan.Zero);
Setting<TimeSpan> previousLapTime =
new Setting<TimeSpan>(“PreviousLapTime”, TimeSpan.Zero);
Setting<List<TimeSpan>> lapList =
new Setting<List<TimeSpan>>(“LapList”, new List<TimeSpan>());
Setting<DateTime> previousTick =
new Setting<DateTime>(“PreviousTick”, DateTime.MinValue);
// Two more pieces of state that we only use so we can return to the page
// in the same state that we left it
Setting<SupportedPageOrientation> savedSupportedOrientations =
new Setting<SupportedPageOrientation>(“SavedSupportedOrientations”,
SupportedPageOrientation.PortraitOrLandscape);
Setting<bool> wasRunning =
new Setting<bool>(“WasRunning”, false);
// A timer, so we can update the display every 100 milliseconds
DispatcherTimer timer =
new DispatcherTimer {Interval = TimeSpan.FromSeconds(.1) };
// The single button on the application bar
IApplicationBarIconButton orientationLockButton;
public MainPage()
{
InitializeComponent();
this.orientationLockButton =
this.ApplicationBar.Buttons[0] as IApplicationBarIconButton;
this.timer.Tick += Timer_Tick;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Update the time displays and progress bar with the data from last time
// (which has been automatically restored)
ShowCurrentTime();
// Refill the lap list with the data from last time
foreach (TimeSpan lapTime in this.lapList.Value)
InsertLapInList(lapTime);
// If we previously left the page with a non-zero total time, then the reset
// button was enabled. Enable it again:
if (this.totalTime.Value > TimeSpan.Zero)
this.ResetButton.IsEnabled = true;
// Restore the orientation setting to whatever it was last time
this.SupportedOrientations = this.savedSupportedOrientations.Value;
// If the restored value is not PortraitOrLandscape, then the orientation
// has been locked. Change the state of the application bar button to
// reflect this.
if (this.SupportedOrientations !=
SupportedPageOrientation.PortraitOrLandscape)
{
this.orientationLockButton.Text = “unlock”;
this.orientationLockButton.IconUri = new Uri(
“/Shared/Images/appbar.orientationLocked.png”, UriKind.Relative);
}
// If the page was left while running, automatically start running again
// to give the illusion of running in the background. The time will
// accurately reflect the time spent away from the app, thanks to the saved
// values of totalTime, currentLapTime, and previousTick.
if (this.wasRunning.Value)
Start();
}
void Timer_Tick(object sender, EventArgs e)
{
// Determine how much time has passed since the last tick.
// In most cases, this will be around 100 milliseconds (but not exactly).
// In some cases, this could be a very large amount of time, if the app
// was exited without stopping the timer. This is what gives the illusion
// that the timer was still running the whole time.
TimeSpan delta = DateTime.UtcNow – this.previousTick.Value;
// Remember the current time for the sake of the next Timer_Tick call
this.previousTick.Value += delta;
// Update the total time and current lap time
this.totalTime.Value += delta;
this.currentLapTime.Value += delta;
// Refresh the UI
ShowCurrentTime();
}
void ShowCurrentTime()
{
// Update the two numeric displays
this.TotalTimeDisplay.Time = this.totalTime.Value;
this.CurrentLapTimeDisplay.Time = this.currentLapTime.Value;
// Update the progress bar (and ensure its maximum value is consistent
// with the length of the previous lap, which occasionally changes)
this.LapProgressBar.Maximum = this.previousLapTime.Value.TotalSeconds;
this.LapProgressBar.Value = this.currentLapTime.Value.TotalSeconds;
}
void StartButton_Click(object sender, RoutedEventArgs e)
{
// Reset previousTick so the calculations start from the current time
this.previousTick.Value = DateTime.UtcNow;
Start();
}
void StopButton_Click(object sender, RoutedEventArgs e)
{
Stop();
}
void ResetButton_Click(object sender, RoutedEventArgs e)
{
Reset();
}
void LapButton_Click(object sender, RoutedEventArgs e)
{
// Add a new entry to the list on the screen
InsertLapInList(this.currentLapTime.Value);
// Add the new piece of data to the list of values
this.lapList.Value.Add(this.currentLapTime.Value);
// This is the start of a new lap, so update our bookkeeping
this.previousLapTime.Value = this.currentLapTime.Value;
this.currentLapTime.Value = TimeSpan.Zero;
}
void Start()
{
this.ResetButton.IsEnabled = true;
// Toggle the visibility of the buttons and the progress bar
this.StartButton.Visibility = Visibility.Collapsed;
this.StopButton.Visibility = Visibility.Visible;
this.ResetButton.Visibility = Visibility.Collapsed;
this.LapButton.Visibility = Visibility.Visible;
// Start the timer
this.timer.Start();
// Remember that the timer was running if the page is left before stopping
this.wasRunning.Value = true;
}
void Stop()
{
// Toggle the visibility of the buttons and the progress bar
this.StartButton.Visibility = Visibility.Visible;
this.StopButton.Visibility = Visibility.Collapsed;
this.ResetButton.Visibility = Visibility.Visible;
this.LapButton.Visibility = Visibility.Collapsed;
// Stop the timer
this.timer.Stop();
// Remember that the timer was stopped if the page is left
this.wasRunning.Value = false;
}
void Reset()
{
// Reset all data
this.totalTime.Value = TimeSpan.Zero;
this.currentLapTime.Value = TimeSpan.Zero;
this.previousLapTime.Value = TimeSpan.Zero;
this.lapList.Value.Clear();
// Reset the UI
this.ResetButton.IsEnabled = false;
this.LapsStackPanel.Children.Clear();
ShowCurrentTime();
}
void InsertLapInList(TimeSpan timeSpan)
{
int lapNumber = LapsStackPanel.Children.Count + 1;
// Dynamically create a new grid to represent the new lap entry in the list
Grid grid = new Grid();
// The grid has “lap N” docked on the left, where N is 1, 2, 3, …
grid.Children.Add(new TextBlock { Text = “lap “ + lapNumber,
Margin = new Thickness(24, 0, 0, 0) });
// The grid has a TimeSpanDisplay instance docked on the right that
// shows the length of the lap
TimeSpanDisplay display = new TimeSpanDisplay { Time = timeSpan,
DigitWidth = 18, HorizontalAlignment = HorizontalAlignment.Right,
Margin = new Thickness(0, 0, 24, 0) };
grid.Children.Add(display);
// Insert the new grid at the beginning of the StackPanel
LapsStackPanel.Children.Insert(0, grid);
}
// The “orientation lock” feature
void OrientationLockButton_Click(object sender, EventArgs e)
{
// Check the value of SupportedOrientations to see if we’re currently
// “locked” to a value other than PortraitOrLandscape.
if (this.SupportedOrientations !=
SupportedPageOrientation.PortraitOrLandscape)
{
// We are locked, so unlock now
this.SupportedOrientations = SupportedPageOrientation.PortraitOrLandscape;
// Change the state of the application bar button to reflect this
this.orientationLockButton.Text = “lock screen”;
this.orientationLockButton.IconUri = new Uri(
“/Shared/Images/appbar.orientationUnlocked.png”, UriKind.Relative);
}
else
{
// We are unlocked, so lock to the current orientation now
if (IsMatchingOrientation(PageOrientation.Portrait))
this.SupportedOrientations = SupportedPageOrientation.Portrait;
else
this.SupportedOrientations = SupportedPageOrientation.Landscape;
// Change the state of the application bar button to reflect this
this.orientationLockButton.Text = “unlock”;
this.orientationLockButton.IconUri = new Uri(
“/Shared/Images/appbar.orientationLocked.png”, UriKind.Relative);
}
// Remember the new setting after the page has been left
this.savedSupportedOrientations.Value = this.SupportedOrientations;
}
bool IsMatchingOrientation(PageOrientation orientation)
{
return ((this.Orientation & orientation) == orientation);
}
}
}
[/code]

Notes:

  • To keep the app in the state it was previously left in, and to provide the illusion of running in the background, this app has many Setting members. But rather than keeping a separate set of variables and copying the values to/from the corresponding Settings, as done in all previous apps, this app uses each Setting as the primary storage. This simplification doesn’t damage the performance of the app, despite the fact that several of these values are updated with every tick of the timer (10 times a second)! That’s because the Setting mechanism adds very little overhead.
    (Internally, the values aren’t actually persisted until the app is exiting.)
  • TimeSpans are used throughout this code whenever a length of time is needed, which is a natural choice. TimeSpan not only has many convenient methods for doing calculations and setting or extracting individual components of the time (such as minutes versus seconds), but it also happens to be what gets returned to you when you subtract one DateTime from another.
  • Inside OnNavigatedTo, various pieces of the UI are updated to match the current values of the Settings. This is not needed for the first run of the app; it is done for the sake of subsequent runs. This includes automatically restarting the timer if it was running when the app was previously left.
  • No OnNavigatedFrom method is needed for this app because all the Setting persistence is handled by keeping those members up-to-date at all times.
  • The timer’s Tick event handler (Timer_Tick) figures out how much time has elapsed so it can update the UI appropriately. Even though the handler is supposed to be called every 100 milliseconds, it’s important that the code checks to see how much time actually elapsed because the exact timing of the event varies. Also, this behavior enables the illusion of running in the background, because when returning to this app, the time of the last tick may have been several minutes ago!
  • DateTime.UtcNow is used to track the elapsed time rather than DateTime.Now. This is important for ensuring that the app works correctly if the user changes time zones while the timer is running (or pretending to run in the background). This might not be as far-fetched as it sounds. For example, perhaps a user wishes to time the duration of a cross-country flight.
  • ShowCurrentTime, called from a few places, refreshes both time displays and the progress bar to match the current values. To set the progress bar’s Value and Maximum values, TimeSpan’s TotalSeconds property is used to get a simple numeric representation of the length of time. TotalSeconds is of type double, so it has no problem
    representing a fractional number of seconds. (The choice of seconds is arbitrary. TotalMinutes, TotalHours, and so on, could have been used instead.)
  • The Start and Stop methods not only start and stop the timer, but toggle the visibility of the buttons so “stop” and “lap” are seen when running whereas “start” and “reset” are seen when stopped.
  • InsertLapInList is interesting because this is the first time we’ve created new UI elements on-the-fly. Every time the lap button is pressed, this is called to add a new single-cell grid to the laps stack panel at the bottom of the page. (Grid is used because it is the easiest way to get the desired display. This takes advantage of the fact that a grid in a vertical stack panel stretches horizontally but stays as small as it can vertically.) The grid is given two children: a text block providing the lap number, and another instance of a TimeSpanDisplay to display the corresponding lap time. Again, this TimeSpanDisplay could have been just another text block instead if we didn’t care about having a proportional-width time display. Although both elements are added to the same (one and only) grid cell, HorizontalAlignment is used to right-align the lap time so it doesn’t visually overlap the lap number. This new grid is equivalent to the following grid defined in XAML, for a lap number of 1 and a time of 0:14.2:
    [code]
    <Grid>
    <TextBlock Text=”lap 1” Margin=”24,0,0,0”/>
    <local:TimeSpanDisplay Time=”0:14.2” DigitWidth=”18”
    HorizontalAlignment=”Right” Margin=”0,0,24,0”/>
    </Grid>
    [/code]
  • Although both elements are added to the new grid’s Children collection inside InsertLapInList, nothing actually gets added to the page until the grid is placed in the stack panel’s Children collection. Notice that Insert is called with an index of 0, rather than the simpler Add method. That’s because Add places the new child at the end of the collection, but we want each new lap to appear at the beginning.
  • OrientationLockButton_Click and the IsMatchingOrientation helper method handle the implementation of orientation lock, discussed in the next section.

Orientation Lock

The idea behind the orientation lock is simple. When “unlocked,” the page’s SupportedOrientations property should be set to PortraitOrLandscape. When “locked,” the property should be updated to match whatever the current orientation is, so it stops responding to future orientation changes until “unlocked.” The implementation, however, is not quite that simple. SupportedOrientations can only be set to one of the three values in the SupportedPageOrientation enumeration, whereas the page’s Orientation property is actually a different enumeration type called PageOrientation.

PageOrientation defines seven values: Landscape, LandscapeLeft, LandscapeRight, Portrait, PortraitUp, PortraitDown, and None. However, a page’s Orientation property will only ever report one of three values: LandscapeLeft, LandscapeRight, or PortraitUp. PortraitDown (an upsidedown portrait mode) is not supported and None is a dummy value. The reason the generic Landscape and Portrait values are in the list is that PageOrientation is a bit-flags enumeration. This enables you to either check for an exact orientation (like LandscapeLeft) or a group of orientations (which would only apply to Landscape). Therefore, to reliably check for a specific value or a group of values, you
should perform a bit-wise AND with the desired value. This is exactly what is done by the IsMatchingOrientation method.

Figure 4.13 shows the result of locking to the portrait orientation and then tilting the phone sideways.

FIGURE 4.13 Locked to the portrait orientation, even when the phone is held in a landscape position.
FIGURE 4.13 Locked to the portrait orientation, even when the phone is held in a landscape position.

Although the orientation lock button initially has text “lock screen,” the code toggles this text to “unlock” and back.
Ideally it would have said “lock orientation” and “unlock orientation”—or at least a symmetric “lock screen” and “unlock screen”—but these options are too long to fit.

FIGURE 4.14 The two states of the orientation lock button.
FIGURE 4.14 The two states of the orientation lock button.

 

In each case, the icon represents the current state rather than the result of the action (e.g. the button for locking shows an unlocked icon and vice versa), as shown in Figure 4.14. This seems like the more appropriate choice, much like a mute/unmute button that shows a muted state even though clicking it would unmute the speaker.

The TimeSpanDisplay User Control

To show the numbers in a time display like a fixed-width font, even when the font is not a fixed-width font, a horizontal stack panel can do the trick. The idea is to add each character of the string as an individual element. The key is to give each element displaying a number a uniform width that’s wide enough for every digit. The nonnumeric parts of the text (the colon and period) should be left at their natural width, to prevent the display from looking odd. For the best results, each digit should be centered inside the space allocated to it. Figure 4.15 demonstrates this idea.

FIGURE 4.15 To prevent jiggling from changing digits, each character is added to a horizontal stack panel, and each digit is given a fixed width.
FIGURE 4.15 To prevent jiggling from changing digits, each character is added to a horizontal stack panel, and each digit is given a fixed width.

A single-row grid could also work, but a stack panel is easier to work with when you’ve got an unbounded number of
children.

Because Stopwatch needs this same kind of time display in multiple places (one for the total time, one for the current
lap time, and one for each previous lap time), it makes sense to encapsulate the UI and logic for this display into a control that can be used multiple times, just like a button or a text box.

Silverlight provides two mechanisms for creating your own control. One approach produces what is often called a custom control, and the other approach produces a user control. User controls have some constraints that custom controls don’t have, but they are also much easier to create. In addition, creating a user control is usually good enough,
especially if your motivation is reuse of the control among your own apps. For the most part, creating a custom control is unnecessary unless you’re planning on broadly releasing it as part of a library for others to use.

To add a new user control to a Visual Studio project, you can right-click on the project in Solution Explorer and select Add, New Item…, Windows Phone User Control. Give it a filename other than the default  WindowsPhoneControl1.xaml—such as TimeSpanDisplay. xaml in this case—and press OK. This generates two new files in your project: TimeSpanDisplay.xaml and its corresponding code-behind file, TimeSpanDisplay.xaml.cs. These two files work the same way as the two files for any page.

The initial contents of TimeSpanDisplay.xaml are as follows:

[code]

<UserControl x:Class=”WindowsPhoneApp.TimeSpanDisplay”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:d=”http://schemas.microsoft.com/expression/blend/2008”
xmlns:mc=”http://schemas.openxmlformats.org/markup-compatibility/2006”
mc:Ignorable=”d”
FontFamily=”{StaticResource PhoneFontFamilyNormal}”
FontSize=”{StaticResource PhoneFontSizeNormal}”
Foreground=”{StaticResource PhoneForegroundBrush}”
d:DesignHeight=”480” d:DesignWidth=”480”>
<Grid x:Name=”LayoutRoot” Background=”{StaticResource PhoneChromeBrush}”>
</Grid>
</UserControl>

[/code]

This defines a TimeSpanDisplay class that derives from UserControl. The PhoneChromeBrush background and the 480×480 design-time dimensions are completely arbitrary, and are often replaced with something completely different.

The TimeSpanDisplay.xaml.cs code-behind file contains the constructor that makes the required InitializeComponent call, as follows (omitting the using statements for brevity):

[code]

namespace WindowsPhoneApplication3
{
public partial class TimeSpanDisplay : UserControl
{
public TimeSpanDisplay()
{
InitializeComponent();
}
}
}

[/code]

With this in our project, we can now change the contents of both files to create the control that is needed by the rest of the app. Listing 4.3 contains the updated XAML file.

LISTING 4.3 TimeSpanDisplay.xaml—The User Interface for the TimeSpanDisplay User Control

[code]
<UserControl x:Class=”WindowsPhoneApp.TimeSpanDisplay”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
VerticalAlignment=”Center”>
<StackPanel x:Name=”LayoutRoot” Orientation=”Horizontal”/>
</UserControl>

[/code]

The XAML got a lot simpler! Unnecessary attributes were removed and the grid was replaced with a horizontal stack panel that gets filled from the code-behind.

Listing 4.4 contains the updated code-behind file.

LISTING 4.4 TimeSpanDisplay.xaml.cs—The Code-Behind for the TimeSpanDisplay User Control

[code]

using System;
using System.ComponentModel;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
namespace WindowsPhoneApp
{
public partial class TimeSpanDisplay : UserControl
{
int digitWidth;
TimeSpan time;
public TimeSpanDisplay()
{
InitializeComponent();
// In design mode, show something other than an empty StackPanel
if (DesignerProperties.IsInDesignTool)
this.LayoutRoot.Children.Add(new TextBlock { Text = “0:00.0” });
}
public int DigitWidth {
get { return this.digitWidth; }
set
{
this.digitWidth = value;
// Force a display update using the new width:
this.Time = this.time;
}
}
public TimeSpan Time
{
get { return this.time; }
set
{
this.LayoutRoot.Children.Clear();
// Carve out the appropriate digits and add each individually
// Support an arbitrary # of minutes digits (with no leading 0)
string minutesString = value.Minutes.ToString();
for (int i = 0; i < minutesString.Length; i++)
AddDigitString(minutesString[i].ToString());
this.LayoutRoot.Children.Add(new TextBlock { Text = “:” });
// Seconds (always two digits, including a leading zero if necessary)
AddDigitString((value.Seconds / 10).ToString());
AddDigitString((value.Seconds % 10).ToString());
// Add the decimal separator (a period for en-US)
this.LayoutRoot.Children.Add(new TextBlock { Text =
CultureInfo.CurrentUICulture.NumberFormat.NumberDecimalSeparator });
// The Remainder (always a single digit)
AddDigitString((value.Milliseconds / 100).ToString());
this.time = value;
}
}
void AddDigitString(string digitString)
{
Border border = new Border { Width = this.DigitWidth };
border.Child = new TextBlock { Text = digitString,
HorizontalAlignment = HorizontalAlignment.Center };
this.LayoutRoot.Children.Add(border);
}
}
}

[/code]

Notes:

  • In addition to the Time property for updating the display, this control defines a DigitWidth property so the consumer can customize how wide the slot for each digit should be. This is necessary because the app author can choose the font family, size, and weight, and different values require different widths. When DigitWidth is
    set, the code in the Time property setter is invoked to be sure that the display respects the new width instantly. Without this, the control would not work properly if the consumer happened to set Time before DigitWidth (which is actually done inside InsertLapInList back in Listing 4.2). Proper properties should be able to be set in any order and behave identically.
  • Inside the Time property, various digits are extracted from the TimeSpan one-by-one leveraging its Minutes, Seconds, and Milliseconds properties. Unlike the TotalMinutes, TotalSeconds, and TotalMilliseconds properties, these are integers and produce only the relevant slice of the time value. (For example, Seconds is 5 for
    both TimeSpan values of 1:05.1 and 2:05.9.) Note that this code clears the stack panel and recreates all the child elements each time. A more efficient approach would reuse existing elements instead.
  • This code supports an arbitrarily-long number of minutes, despite the fact that the app as-is would have trouble displaying a total time of 100 minutes or more in portrait mode unless the font size is reduced.
  • Rather than hardcoding a period after the seconds value, this code uses the NumberDecimalSeparator property. This ensures a proper display for the phone’s current region and language settings.
  • The AddDigitString method is responsible for adding each digit element to the stack panel. Each digit is not just a text block, however. In order to be properly horizontally centered inside the allocated width, it is wrapped inside a simple element called Border. (Text blocks have no HorizontalContentAlignment property, and recall that HorizontalAlignment is meaningless on an element directly inside a horizontal stack panel.) The border is given the appropriate DigitWidth, so the presumably narrower text block inside can be centered with the HorizontalContentAlignment marking. Because of this extra wrapping, the digits would not be aligned with the colon and period text blocks if it weren’t for the VerticalAlignment setting placed on the XAML file in Listing 4.3.
  • The constructor leverages the static DesignerProperties.IsInDesignTool property to provide a reasonable display at design-time inside Visual Studio and Expression Blend. Without this code, the instances of the control wouldn’t be visible on the design surface of MainPage.xaml because each one starts out as an empty stack panel. With this code, however, they are visible, as shown in Figure 4.16.
FIGURE 4.16 In the Visual Studio designer for MainPage.xaml, both instances of TimeSpanDisplay appear as “0:00.0” thanks to the logic inside the control’s constructor.
FIGURE 4.16 In the Visual Studio designer for MainPage.xaml, both instances of TimeSpanDisplay appear as “0:00.0” thanks to the logic inside the control’s constructor.

The Finished Product