One handy application on a phone
is a stopwatch—an ideal use for a ToggleButton as well as the Stopwatch
class defined in the System.Diagnostics namespace.
I deliberately spelled the name
of the StopWatch project
in camel case to avoid confusion with the .NET Stopwatch class.
To make the program a little more interesting, I decided that the
elapsed time should be displayable in three different formats,
corresponding to the members of this enumeration:
Example 1. Silverlight
Project: StopWatch File: ElapsedTimeFormat.cs
namespace StopWatch { public enum ElapsedTimeFormat { HourMinuteSecond, Seconds, Milliseconds } }
|
This elapsed time format is an application setting in StopWatch, so
it is exposed as a public property in the App
class. As usual with application
settings, it is saved to isolated storage when the
program is deactivated or closed, and retrieved when the program is
launched or activated:
Example 2. Silverlight
Project: StopWatch File: App.xaml.cs (excerpt)
public partial class App : Application { // Application Setting public ElapsedTimeFormat ElapsedTimeFormat { set; get; }
. . .
private void Application_Launching(object sender, LaunchingEventArgs e) { LoadSettings(); }
private void Application_Activated(object sender, ActivatedEventArgs e) { LoadSettings(); }
private void Application_Deactivated(object sender, DeactivatedEventArgs e) { SaveSettings(); }
private void Application_Closing(object sender, ClosingEventArgs e) { SaveSettings(); }
void LoadSettings() { IsolatedStorageSettings settings = IsolatedStorageSettings.ApplicationSettings;
if (settings.Contains("elapsedTimeFormat")) ElapsedTimeFormat = (ElapsedTimeFormat)settings["elapsedTimeFormat"]; else ElapsedTimeFormat = ElapsedTimeFormat.HourMinuteSecond; }
void SaveSettings() { IsolatedStorageSettings settings = IsolatedStorageSettings.ApplicationSettings; settings["elapsedTimeFormat"] = ElapsedTimeFormat; settings.Save(); } }
|
The content
area in the XAML file is a bit more extensive than you might expect
because it includes a type of “dialog box” that’s used by the user to
select the elapsed time format.
So as not to overwhelm you, only
the portion of the content area devoted to the operation of the stopwatch is shown
here. It consists of just a ToggleButton
to turn the stopwatch
on and off, and a TextBlock to display
the elapsed time.
Example 3. Silverlight
Project: StopWatch File: MainPage.xaml (excerpt)
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
<!-- Stopwatch display --> <Grid VerticalAlignment="Center" Margin="25 0"> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions>
<TextBlock Name="elapsedText" Text="0" Grid.Row="0" FontFamily="Arial" FontSize="{StaticResource PhoneFontSizeExtraLarge}" TextAlignment="Center" Margin="0 0 0 50"/>
<ToggleButton Name="startStopToggle" Content="Start" Grid.Row="1" Checked="OnToggleButtonChecked" Unchecked="OnToggleButtonChecked" /> </Grid>
<!-- Rectangle to simulate disabling --> . . . <!-- "Dialog Box" to select TimeSpan formatting --> . . . </Grid>
|
The code-behind file
defines just three fields; using
directives are required for System.Diagnostics
and System.Globaliztion.
Example 4. Silverlight
Project: StopWatch File: MainPage.xaml.cs (excerpt)
public partial class MainPage : PhoneApplicationPage { Stopwatch stopwatch = new Stopwatch(); TimeSpan suspensionAdjustment = new TimeSpan(); string decimalSeparator = NumberFormatInfo.CurrentInfo.NumberDecimalSeparator; public MainPage() { InitializeComponent(); DisplayTime(); } . . . void DisplayTime() { TimeSpan elapsedTime = stopwatch.Elapsed + suspensionAdjustment; string str = null;
switch ((Application.Current as App).ElapsedTimeFormat) { case ElapsedTimeFormat.HourMinuteSecond: str = String.Format("{0:D2} {1:D2} {2:D2}{3}{4:D2}", elapsedTime.Hours, elapsedTime.Minutes, elapsedTime.Seconds, decimalSeparator, elapsedTime.Milliseconds / 10); break;
case ElapsedTimeFormat.Seconds: str = String.Format("{0:F2} sec", elapsedTime.TotalSeconds); break;
case ElapsedTimeFormat.Milliseconds: str = String.Format("{0:F0} msec", elapsedTime.TotalMilliseconds); break;
} elapsedText.Text = str; } . . . }
|
The most important field is an instance of the Stopwatch. Programmers customarily use this class to determine
how long a program spends in a particular method. It’s not often used
as an actual stopwatch!
You’ll see shortly how the suspensionAdjustment
field is used in connection with tombstoning.
The .NET Stopwatch object provides an elapsed time in the form of a TimeSpan object. I
couldn’t quite persuade the TimeSpan
object to display
the elapsed time in precisely the format I wanted, so I ended up doing
my own formatting. The decimalSeparator
field represents a tiny nod to internationalization.
The DisplayTime
method is devoted to setting the Text
property of the TextBlock. It accesses
the Elapsed property of the Stopwatch and adds the suspensionAdjustment.
This is formatted in one of three ways depending on the ElapsedTimeFormat
property of the App class.
When pressed, the ToggleButton
fires Checked and Unchecked events, which are both handled by the OnToggleButtonChecked
method. This method uses the IsChecked
property of the ToggleButton to start or stop the Stopwatch object and also to change the text displayed by
the button. To keep the display promptly updated, a CompositionTarget.Rendering event simply
calls DisplayTime:
Example . Silverlight
Project: StopWatch File: MainPage.xaml.cs (excerpt)
void OnToggleButtonChecked(object sender, RoutedEventArgs e) { if ((bool)startStopToggle.IsChecked) { stopwatch.Start(); startStopToggle.Content = "Stop"; CompositionTarget.Rendering += OnCompositionTargetRendering; } else { stopwatch.Stop(); startStopToggle.Content = "Start"; CompositionTarget.Rendering -= OnCompositionTargetRendering; } }
void OnCompositionTargetRendering(object sender, EventArgs args) { DisplayTime(); }
|
Here it is in action:
As you can see, the program also contains an ApplicationBar. The two
buttons are labeled “format” and “reset.” Here’s the definition of the ApplicationBar in the XAML file:
Example 6. Silverlight
Project: StopWatch File: MainPage.xaml (excerpt)
<phone:PhoneApplicationPage.ApplicationBar> <shell:ApplicationBar> <shell:ApplicationBarIconButton IconUri="/Images/appbar.feature.settings.rest. png" Text="format" Click="OnAppbarFormatClick" />
<shell:ApplicationBarIconButton IconUri="/Images/appbar.refresh.rest.png" Text="reset" Click="OnAppbarResetClick" /> </shell:ApplicationBar> </phone:PhoneApplicationPage.ApplicationBar>
|
The simpler of the two Click
methods is the one for resetting the stopwatch. Resetting the .NET Stopwatch object also
causes it to stop, so the ToggleButton is explicitly unchecked and suspensionAdjustment
is set to zero:
Example 7. Silverlight
Project: StopWatch File: MainPage.xaml.cs (excerpt)
void OnAppbarResetClick(object sender, EventArgs args) { stopwatch.Reset(); startStopToggle.IsChecked = false; suspensionAdjustment = new TimeSpan(); DisplayTime(); }
|
Selecting the elapsed time format is a little more complex. I
chose to handle this not with menu items on the ApplicationBar but
with something resembling a little dialog box. This dialog box is
defined right in the XAML file in the same Grid cell as the main display:
Example 8. Silverlight
Project: StopWatch File: MainPage.xaml.cs (excerpt)
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
<!-- Stopwatch display --> . . . <!-- Rectangle to simulate disabling --> <Rectangle Name="disableRect" Fill="#80000000" Visibility="Collapsed" /> <!-- "Dialog Box" to select TimeSpan formatting --> <Border Name="formatDialog" Background="{StaticResource PhoneChromeBrush}" BorderBrush="{StaticResource PhoneForegroundBrush}" BorderThickness="3" HorizontalAlignment="Center" VerticalAlignment="Center" Visibility="Collapsed"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions>
<Grid.ColumnDefinitions> <ColumnDefinition Width="*" /> <ColumnDefinition Width="*" /> </Grid.ColumnDefinitions>
<StackPanel Name="radioButtonPanel" Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" HorizontalAlignment="Center">
<RadioButton Content="Hour/Minute/Seconds" Tag="HourMinuteSecond" />
<RadioButton Content="Seconds" Tag="Seconds" />
<RadioButton Content="Milliseconds" Tag="Milliseconds" /> </StackPanel>
<Button Grid.Row="1" Grid.Column="0" Content="ok" Click="OnOkButtonClick" />
<Button Grid.Row="1" Grid.Column="1" Content="cancel" Click="OnCancelButtonClick" /> </Grid> </Border> </Grid>
|
Notice that both the Rectangle and the Border
have Visibility settings of Collapsed so
they are normally absent from the display. The Rectangle covers the entire content area and is used solely
to “gray out” the background. The Border is structured much like a traditional dialog
box, with three RadioButton controls
and two Button controls labeled “ok”
and “cancel.”
Notice that the RadioButton
controls do not have handlers set for their Checked events, but they do have text strings set to their Tag properties. The Tag property is defined by FrameworkElement and is
available to attach arbitrary data on elements and controls. It’s no
coincidence that the text strings I’ve set to these TagElapsedTimeFormat
enumeration. properties are
exactly the members of the
When the user presses the ApplicationBar button labeled “format,” the OnAppbarFormatClick method takes over,
making the disableRect and formatDialog elements visible:
Example 9. Silverlight
Project: StopWatch File: MainPage.xaml.cs (excerpt)
void OnAppbarFormatClick(object sender, EventArgs args) { disableRect.Visibility = Visibility.Visible; formatDialog.Visibility = Visibility.Visible;
// Initialize radio buttons ElapsedTimeFormat currentFormat = (Application.Current as App).ElapsedTimeFormat;
foreach (UIElement child in radioButtonPanel.Children) { RadioButton radio = child as RadioButton; ElapsedTimeFormat radioFormat = (ElapsedTimeFormat)Enum.Parse(typeof(ElapsedTimeFormat), radio.Tag as string, true); radio.IsChecked = currentFormat == radioFormat; } }
|
The logic sets the IsChecked
property of a particular RadioButton
if its Tag property (when converted
into an ElapsedTimeFormat enumeration
member) equals the ElapsedTimeFormat stored as an application setting. (Easier
logic would have been possible if the Tag properties were simply set to 0, 1, and 2
for the integer values of the enumeration members.)
Here’s the displayed dialog box:
No event handlers are
attached to the RadioButton controls. After the dialog is display, the next event the
program will receive signals whether the user has press the “ok” or
“cancel” button:
Example 10. Silverlight
Project: StopWatch File: MainPage.xaml.cs (excerpt)
void OnOkButtonClick(object sender, RoutedEventArgs args) { foreach (UIElement child in radioButtonPanel.Children) { RadioButton radio = child as RadioButton; if ((bool)radio.IsChecked) (Application.Current as App).ElapsedTimeFormat = (ElapsedTimeFormat)Enum.Parse(typeof(ElapsedTimeFormat), radio.Tag as string, true); } OnCancelButtonClick(sender, args); }
void OnCancelButtonClick(object sender, RoutedEventArgs args) { disableRect.Visibility = Visibility.Collapsed; formatDialog.Visibility = Visibility.Collapsed; DisplayTime(); }
|
The routine for the “ok”
button checks which RadioButton is now clicked and then sets the application
setting with that value. It also calls the “cancel” handler, which
“dismisses” the “dialog box” by setting the Visibility
properties of disableRect and formatDialog back to Collapsed.
A program such as this presents a
bit of a challenge with respect to tombstoning. I decided to ignore issues involving
the dialog box. If someone navigates away from the program with the
dialog box displayed, it’s no big deal if it’s no longer there when the
user returns.
But ideally, you want an
active stopwatch to
continue running if the user navigates to another application. Of
course, it can’t really keep running because in reality the program is
terminated.
What the program can do, however, is
save the current elapsed
time and the clock time as it is being tombstoned. When the program
returns, it can use that information to adjust the time shown on the
stopwatch. This occurs in the OnNavigatedFrom
and OnNavigatedTo methods:
Example 11. Silverlight
Project: StopWatch File: MainPage.xaml.cs (excerpt)
protected override void OnNavigatedFrom(NavigationEventArgs args) { PhoneApplicationService service = PhoneApplicationService.Current; service.State["stopWatchRunning"] = (bool)startStopToggle.IsChecked;
service.State["suspensionAdjustment"] = suspensionAdjustment + stopwatch.Elapsed; service.State["tombstoneBeginTime"] = DateTime.Now;
base.OnNavigatedFrom(args); }
protected override void OnNavigatedTo(NavigationEventArgs args) { PhoneApplicationService service = PhoneApplicationService.Current;
if (service.State.ContainsKey("stopWatchRunning")) { suspensionAdjustment = (TimeSpan)service.State["suspensionAdjustment"];
if ((bool)service.State["stopWatchRunning"]) { suspensionAdjustment += DateTime.Now - (DateTime)service.State["tombstoneBeginTime"]; startStopToggle.IsChecked = true; } else { DisplayTime(); } } base.OnNavigatedTo(args); }
|
Whenever the program starts up
again, the .NET Stopwatch
object always begins at an elapsed time
of zero. That Stopwatch object can’t be
adjusted directly. Instead, the suspensionAdjustment field represents the time that elapsed when the
program was tombstoned plus the elapsed time of the Stopwatch when tombstoning began. A user could navigate away
several times while the stopwatch is running, so this field could be the
accumulation of several periods of tombstoning.
For OnNavigatedTo, the simplest case is when the stopwatch is not
actively running. All that’s necessary is to set the suspensionAdjustment
from the saved value. But if the stopwatch has conceptually been running
all this time, then the suspensionAdjustment must be increased by the period of time that
elapsed based on the value returned by DateTime.Now.
In actual use, the StopWatch
program will appear to be running and keeping track of elapsed time even
when it’s not, and it’s that illusion that make the program much more
useful than it would be otherwise.