Windows Phone

Make a countdown timer for a WP7 app

Justin James presents his code he for making a countdown timer for one of his Windows Phone 7 apps. He also shares his one frustration with making the timer.

For the longest time, I wanted to add stopwatch functionality to my Lifter Calculator Windows Phone 7 (WP7) application. The stopwatch applications that are available are aimed at people like joggers who just need to track their endless jogs; I needed specialized timers to help me in the weight room. I wanted a timer to count down, to make sure that my time between sets was the right length. I also wanted a timer for interval training. Here's what I did to make the countdown timer.

Working with timers in WP7 is extremely easy. All you need is an instance of the System.Windows.Threading.DispatchTimer class. Set the Interval property to a TimeSpan object that represents how often it should alert your application, and add an EventHandler to the Ticks event. From there, you call the Start method, and your event handler will be called every time the interval has passed. The typical stopwatch feature just tracks the time started, and in the event handler it figures out how much time has elapsed and updates a TextBlock with the current number.

My countdown timer needed a little bit more functionality than just a stopwatch. For one thing, the user needs to specify how long the countdown should be. Secondly, I wanted to play a sound when the time is up. Since the phone screen lock time can often be set lower than the rest period between sets, I needed to prevent the screen from locking while the timer was running. Finally, I wanted a visual indicator showing the remaining time.

I decided to keep the code a bit simple, but my trade-off is a slight fraction of accuracy, and if the user switches applications, our timers will be paused. For the user interface, I made two buttons, a progress bar, a media element, and a text block:

<Grid Name="setTimerGrid" Visibility="Visible">

<Grid.ColumnDefinitions>

<ColumnDefinition Width="126" />

<ColumnDefinition Width="*" />

</Grid.ColumnDefinitions>

<Grid.RowDefinitions>

<RowDefinition Height="80" />

<RowDefinition Height="80" />

<RowDefinition Height="80" />

<RowDefinition Height="80" />

</Grid.RowDefinitions>

<TextBlock Height="30" HorizontalAlignment="Left" Margin="7,25,0,0" Name="textBlock7" Text="Seconds/set:" VerticalAlignment="Top" Grid.Column="0" Grid.Row="0" />

<TextBox Height="72" HorizontalAlignment="Left" Margin="7,6,0,0" Name="secondsPerSet" Text="" VerticalAlignment="Top" Width="305" Grid.Column="1" Grid.Row="0" InputScope="Number" TextChanged="secondsPerSet_TextChanged" />

<Button Content="Start Timer" Grid.Column="1" Grid.Row="1" Height="72" HorizontalAlignment="Left" Margin="7,6,0,0" Name="setTimerButton" VerticalAlignment="Top" Width="305" Click="setTimerButton_Click" IsEnabled="False" />

<Button Content="Reset Timer" Grid.Column="1" Grid.Row="2" Height="72" HorizontalAlignment="Left" Margin="7,6,0,0" Name="setTimerResetButton" VerticalAlignment="Top" Width="305" IsEnabled="True" Click="setTimerResetButton_Click" />

<ProgressBar Name="setTimerProgress" Grid.Row="3" FlowDirection="RightToLeft" />

<TextBlock Height="72" HorizontalAlignment="Left" Margin="7,6,0,0" Name="setTimerValue" Text="00:00" VerticalAlignment="Top" Width="305" Grid.Column="1" Grid.Row="3" FontWeight="ExtraBold" FontSize="56" />

<MediaElement Height="120" Name="intervalStopBuzzer" Width="160" Visibility="Collapsed" Source="/Sounds/intervalstop.wav" AutoPlay="False" />

</Grid>

Now, for the actual meat of the code:

private DispatcherTimer setTimer = new DispatcherTimer();

private TimeSpan setTimerRemaining = TimeSpan.FromSeconds(0d);

private TimeSpan setTimerInterval = TimeSpan.FromSeconds(1d);

private bool isSetTimerStopped = false;

private bool isBuzzerPlaying = false;

private DispatcherTimer buzzerPlay = new DispatcherTimer();

private void setTimerButton_Click(object sender, RoutedEventArgs e)

{

ToggleSetTimer();

}
private void ValidateSetTimerAllowed()

{

int seconds;
if (isSetTimerRunning)

{

setTimerButton.IsEnabled = true;

return;

}
setTimerButton.IsEnabled = false;
if (int.TryParse(secondsPerSet.Text, out seconds))

{

setTimerButton.IsEnabled = seconds > 0;

}

}
private void secondsPerSet_TextChanged(object sender, TextChangedEventArgs e)

{

ValidateSetTimerAllowed();

}
private void setTimerTick(object sender, EventArgs e)

{

setTimerRemaining = setTimerRemaining.Subtract(setTimerInterval);

setTimerValue.Text = setTimerRemaining.ToString(@"mm\:ss");

setTimerProgress.Value = setTimerRemaining.Seconds;
if (setTimerRemaining.Ticks <= 0)

{

setTimerButton.Content = "Start Timer";

setTimer.Stop();

secondsPerSet.IsEnabled = true;

setTimerRemaining = TimeSpan.Zero;

setTimerValue.Text = setTimerRemaining.ToString(@"mm\:ss");

setTimerProgress.Value = 0;

isSetTimerRunning = false;

PhoneApplicationService.Current.UserIdleDetectionMode = IdleDetectionMode.Enabled;

buzzerPlay.Start();

timerBuzzer.Play();

}

}
private void setTimerResetButton_Click(object sender, RoutedEventArgs e)

{

timerBuzzer.Stop();

buzzerPlay.Stop();
if (isSetTimerRunning)

{

setTimer.Stop();

}
int seconds;

if (int.TryParse(secondsPerSet.Text, out seconds))

{

setTimerProgress.Maximum = seconds;

setTimerProgress.Value = seconds;

setTimerRemaining = TimeSpan.FromSeconds(seconds);

setTimerValue.Text = setTimerRemaining.ToString(@"mm\:ss");

}
if (isSetTimerRunning)

{

setTimer.Start();

}

}
private void ToggleSetTimer()

{

timerBuzzer.Stop();

buzzerPlay.Stop();
if (isSetTimerRunning)

{

setTimerButton.Content = "Start Timer";

setTimer.Stop();

secondsPerSet.IsEnabled = true;

isSetTimerStopped = true;

PhoneApplicationService.Current.UserIdleDetectionMode = IdleDetectionMode.Enabled;

}

else

{

setTimerButton.Content = "Stop Timer";

setTimer.Start();

secondsPerSet.IsEnabled = false;

PhoneApplicationService.Current.UserIdleDetectionMode = IdleDetectionMode.Disabled;
if (!isSetTimerStopped)

{

int seconds;

if (int.TryParse(secondsPerSet.Text, out seconds))

{

setTimerProgress.Maximum = seconds;

setTimerProgress.Value = seconds;

setTimerRemaining = TimeSpan.FromSeconds(seconds);

setTimerValue.Text = setTimerRemaining.ToString(@"mm\:ss");

}

}
isSetTimerStopped = false;
}
isSetTimerRunning = !isSetTimerRunning;

}

private void buzzerPlayTick(object sender, EventArgs e)

{

timerBuzzer.Play();

}

In the page's constructor:

setTimer.Interval = setTimerInterval;
setTimer.Tick += new EventHandler(setTimerTick);
buzzerPlay.Interval = TimeSpan.FromSeconds(1d);
buzzerPlay.Tick += new EventHandler(buzzerPlayTick);

I know this is a lot to digest! Here is what it does:

  • When the user edits the seconds per set, it validates the data to ensure that it is a whole number. If it is, the "Start Timer" button is enabled.
  • When the "Start Timer" button is pushed, it starts running the timer and makes a few changes to the UI (it sets the progress bar's maximum value to the seconds per set, disables the input box, changes the button text to be "Stop Timer"). Also, it disables the screen lock.
  • When the "Stop Timer" button (the same button as "Start Timer" but with different text) is pushed, it stops the timer, and if the media element is playing, it stops that as well as the timer putting it on endless repeat. In addition, it re-enables the screen lock.
  • When the "Reset Timer" button is pushed, it sets the current countdown time to the user input, and it stops the media element from playing as well as its repeater timer. If the stopwatch is currently running, it is not stopped, though.
  • When the "Ticks" event is fired, it calculates the current time remaining and updates the display and the progress bar. If the time is less than or equal to 0, it stops the main timer, plays the sound file in the media element, and starts the timer for the media element to replay the noise.

While this is a fair amount of code, it's not really doing anything too complicated. It was very straightforward to write this, and my only frustration was that I had to use a second timer to make the "buzzer" keep playing and that there was no "endless repeat" option.

J.Ja

About

Justin James is the Lead Architect for Conigent.

0 comments