Blograby

Stopwatch

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:

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.

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 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.

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 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.

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 and autosizing are straightforward, but proportional sizing needs more explanation. It is done with star syntax that works as follows:

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.

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.

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:

FIGURE 4.9 Three potential display options for the same time span.
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:

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.

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.

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:

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:

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.

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.

 

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.

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:

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

 

 

Exit mobile version