Silverlight 3 Countdown Counter


This post is part of my Crazy Cancer Tour developer series. It covers the site as it is today (countdown counter only). You can view the site at http://crazycancertour.com. I have some additional work going on behind the scenes, but the only visible portion right now is the counter, which is done in Silverlight 3. This is how the final site looks:

FinishedSite

Setting up a counter in Silverlight is actually quite easy, as you will see in this post.

Layout In Blend

I am a firm believer in saving time with tools, so I used Blend 3 to layout the site (free trial if you need a copy to play along). This is not completely necessary, however, as XAML is just an XML dialect, which means, for the designer/developer, you have to nest things properly to get them working. When you start a new Blend 3 project (Silverlight+ Web Application in this case), you end up with a blank XAML canvas. You can then attack with either code or drag and drop, with the same end result.

The main reason to choose drag and drop over coding XAML is the ability to visually layout your design. The designer then creates the XAML for you. Now to the steps I did.

When you open Blend 3, choose a new Project. I don’t really need a screenshot, as you cannot avoid this screen, but visual is good:

NewProject

From here you create a Silverlight + Web Application project and give it a location:

Create

What you have at this time is a solution folder with two projects in it: {Projectname} and {ProjectName}Site. The later holds the HTML and JavaScript necessary to run the Silverlight application. The Projects window shows the files:

ProjectWindow

Note that I have a few extra files in the screencap above, but this is due to my having already compiled the project and having added fonts to the project. When you first open the project, you will have App.xaml and MainPage.xaml and little else. When you look at MainPage.xaml, it looks like this. The XAML button is on the upper right side of the design window and looks like less than greater than <>.

<UserControl
    xmlns="
http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    x:Class="CrazyCancerCountdown.MainPage"
    Width="640" Height="480">

    <Grid x:Name="LayoutRoot" Background="White"/>
</UserControl>

When you add items, the Grid becomes the container that holds them. In my case, I am going to start by adding a screencapture that is 879 pixels by 705 pixels (I really should resize, but it works for what I need right now). I hand altered the width and height of the UserControl element and then dragged the picture on the surface, making sure its margins are 0 in the layout panel (screen cap below):

LayoutPanel

To get the screen cap on the surface, I have two choices:

  1. Add as an asset and then pull from the Assets onto the design surface. This is too many steps for me.
  2. Drag an image from Windows Explorer directly on the surface. With two monitors, like I have here, it is very easy.

The XAML now looks like the following:

<UserControl
    xmlns="
http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    x:Class="CrazyCancerTour.MainPage"
    Width="879" Height="705">

    <Grid x:Name="LayoutRoot" Background="White">
        <Image x:Name="BackgroudImage" Margin="0" Source="CrazyCancer.png"
           
Stretch="Fill" Opacity="0.5"/>
    </Grid>
</UserControl>

I then need a rectangle to place my elements, so I drag one from the toolbar:

Rectangle

If the toolbar is greyed out, it is due to having your design surface in XAML mode. Click on the design icon and you should be able to drag and drop a rectangle on the surface. The Grid in the XAML then looks like the following:

<Grid x:Name="LayoutRoot" Background="White">
    <Image x:Name="BackgroudImage" Margin="0" Source="CrazyCancer.png" Stretch="Fill" Opacity="0.5"/>
    <Rectangle Stroke="Black" Margin="335,231,282,347">
        <Rectangle.Fill>
            <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
                <GradientStop Color="Black" Offset="0"/>
                <GradientStop Color="#FF340435" Offset="1"/>
            </LinearGradientBrush>
        </Rectangle.Fill>
    </Rectangle>
</Grid>

Rather than hand hold through the rest in a very explicit step-by-step, I am simply going to show the XAML. To get to this XAML, I dragged a header textbox on the rectangle, 4 textboxes for my numbers (days, hours, minutes and seconds), and a footer textbox that states “ Days      Hours   Minutes  Seconds”. I then tweaked the look and feel, by changing graphics, etc. The Grid now looks like this, in XAML:

<Grid x:Name="LayoutRoot" Background="White">
    <Image x:Name="BackgroudImage" Margin="0" Source="CrazyCancer.png" Stretch="Fill" Opacity="0.5"/>
    <Rectangle Stroke="Black" Margin="335,231,282,347">
        <Rectangle.Fill>
            <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
                <GradientStop Color="Black" Offset="0"/>
                <GradientStop Color="#FF340435" Offset="1"/>
            </LinearGradientBrush>
        </Rectangle.Fill>
    </Rectangle>
    <TextBlock x:Name="TourBeginsTextBox" Height="35" Margin="345,237,298,0" VerticalAlignment="Top" Text="The Tour Begins In" TextWrapping="Wrap" FontSize="21.333" Foreground="#FFF587EB" TextAlignment="Center" FontFamily="Fonts/Fonts.zip#Planet Benson 2"/>
    <TextBlock x:Name="DayTextBox" Height="61" Margin="345,274,0,0" VerticalAlignment="Top" Foreground="#FFFCFCFC" TextAlignment="Center" TextWrapping="Wrap" HorizontalAlignment="Left" Width="51" FontSize="32" FontFamily="Fonts/Fonts.zip#Virginie"><Run Text="00"/><LineBreak/><Run Text=""/></TextBlock>
    <TextBlock Loaded="StartTimer" x:Name="LegendTextBox" Height="25" Margin="345,322,282,0" VerticalAlignment="Top" TextWrapping="Wrap" Foreground="#FFF587EB" FontSize="14.667" FontFamily="Arial" Text=" Days      Hours   Minutes  Seconds" FontWeight="Bold" FontStyle="Italic"/>
    <TextBlock x:Name="HourTextBox" Height="61" Margin="407,274,421,0" VerticalAlignment="Top" Foreground="#FFFCFCFC" TextAlignment="Center" TextWrapping="Wrap" FontSize="32" FontFamily="Fonts/Fonts.zip#Virginie"><Run Text="00"/><LineBreak/><Run Text=""/></TextBlock>
    <TextBlock x:Name="MinuteTextBox" Height="61" HorizontalAlignment="Right" Margin="0,274,360,0" VerticalAlignment="Top" Width="51" Foreground="#FFFCFCFC" TextAlignment="Center" TextWrapping="Wrap" FontSize="32" FontFamily="Fonts/Fonts.zip#Virginie"><Run Text="00"/><LineBreak/><Run Text=""/></TextBlock>
    <TextBlock x:Name="SecondTextBox" Height="61" HorizontalAlignment="Right" Margin="0,274,297,0" VerticalAlignment="Top" Width="51" Foreground="#FFFCFCFC" TextAlignment="Center" TextWrapping="Wrap" FontSize="32" FontFamily="Fonts/Fonts.zip#Virginie"><Run Text="00"/><LineBreak/><Run Text=""/></TextBlock>
</Grid>

One thing of note in the XAML that I have not mentioned. There is a statement Loaded=”StartTime” in the Legend textbox (the footer mentioned); I have bolded it in the code above. This is a method in the C# code, which we will cover in a moment. If you try to run the code with this statement, you will have to add the following to your MainPage.Xaml.cs file, or remove the Loaded="StartTimer" from the XAML:

        public void StartTimer(object o, RoutedEventArgs sender)
        {
        }

You can now test the page and it should work, although it will not countdown.

Before moving to code, there is one more item. I have added some fonts from http://simplythebest.net/fonts/. These are freeware/donateware fonts. In particular, I have used Planet Benson and Virginie. In order to have them in my Silverlight app, I have to add them with the Font Manager. There are two ways to do this:

  1. Tools menu >> Font Manager
  2. With a textbox element selected, click on the Font Manager … button in the Text section of the Properties window

Creating the Countdown Code

I am a strong proponent of separating out concerns, both in tiers and inside individual classes. I did not start this way with this app, but every time I found a dupe, I refactored. Thus, I am not going to do a straightforward step by step here. Instead, I am going to cover regions of the code.

First, the using statements. The only one I added was the threading statement (the bottommost (bolded) statement), as that is where our timer class is:

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Windows.Threading;

Next, I have some “global” elements. I am not fond of global bits, but this is a quick, down and dirty project, and the global bits help avoid adding too much of a perf hit to the project. Essentially, I have one “counter” for each of the time elements (day, hour, minute and second), plus a dispatcher timer, which is why I added the threading namespace.

        private int seconds = 0;
        private int minutes = 0;
        private int hours = 0;
        private int days = 0;
        private DispatcherTimer dispatcherTimer;

One of hte bits I refactored out, due to dupes, is a routine that takes a number and makes sure it is two in length (in my code). The routine is flexible enough to be any length, however, so you could conceivably have a millisecond textbox in your code (with the repaint every second, it would be largely worthless, however). This “helper” routine looks like this:

        private string CreateNumericStringOfSetLength(int unit, int length)
        {
            if (unit.ToString().Length == length)
                return unit.ToString();
            else
                return unit.ToString().PadLeft(length, ‘0’);
        }

Fairly simple. If the string is already the right length it is returned, else it is padded to length with a leading zero (or leading zeros, if you are displaying milliseconds). Another refactored routine is my BindPage() routine, which will be used from my StartTimer routine and the event handler that handles updating the countdown timer. It is fairly simple, as well.

        private void BindForm()
        {
            HourTextBox.Text = CreateNumericStringOfSetLength(hours, 2);
            MinuteTextBox.Text = CreateNumericStringOfSetLength(minutes, 2);
            DayTextBox.Text = CreateNumericStringOfSetLength(days, 2);
            SecondTextBox.Text = CreateNumericStringOfSetLength(seconds, 2);
        }

Note how it uses the helper routine for the display.

To create the counter, I use my countdown date, as well as DateTime.Now to determine the initial values to bind. I am using the global day, hours, minutes, seconds to hold the values.

        public void StartTimer(object o, RoutedEventArgs sender)
        {
            DateTime tourDate = new DateTime(2009, 9, 1, 18, 10, 0, 0);
            DateTime startDate = DateTime.Now;
            TimeSpan diff = tourDate – startDate;
            days = diff.Days;
            hours = diff.Hours;
            minutes = diff.Minutes;
            seconds = diff.Seconds;

            BindForm();
        }

I can now test the form and it binds the initial values, but no countdown. To add a coundown, I need to create a routine for the countdown. here I could either repeat the steps above (and eliminate most of the globals), or I can simply countdown each second and adjust. I chose the later, as it should be less of a perf hog (untested assumption, however). The timer_Tick routine is below:

private void timer_Tick(object sender, EventArgs e)
{
    if (days + hours + minutes + seconds == 0)
    {
        dispatcherTimer.Stop();
        return;
    }

        if (seconds == 0)
        {
            seconds = 59;

            if (minutes == 0)
            {
                minutes = 59;

                if (hours == 0)
                {
                    hours = 23;
                    days–;
                }
                else
                {
                    hours–;
                }
            }
            else
            {
                minutes–;
            }

        }
        else
        {
            seconds–;
        }

        BindForm();
}

The logic is this.

  1. Check if everything is zero. If so, stop wasting cycles (stop the timer)
  2. Roll down in reverse order. Essentially decrement seconds until you get to zero, then make it 59 and decrement minutes.
  3. Repeat for minutes. When minutes are zero, then make them 59 and decrease hours.
  4. Hours are essentially the same, but reset to 23. When 0, set to 23 and decrement days.
  5. When days reach zero, they will not go any further.

The form is then bound again, using the updated “counter” values. The code should still display a static set of numbers now, as the tick routine does not run. We do have to change StartTimer to get things rollling. The bolded lines are the ones added:

        public void StartTimer(object o, RoutedEventArgs sender)
        {
            DateTime tourDate = new DateTime(2009, 9, 1, 18, 10, 0, 0);
            DateTime startDate = DateTime.Now;
            TimeSpan diff = tourDate – startDate;
            days = diff.Days;
            hours = diff.Hours;
            minutes = diff.Minutes;
            seconds = diff.Seconds;

            dispatcherTimer = new DispatcherTimer();
            dispatcherTimer.Interval = new TimeSpan(10000000); // 1 second
            dispatcherTimer.Tick += new EventHandler(timer_Tick);
            dispatcherTimer.Start();

            BindForm();
        }

Fairly simple, once again. DispatherTimer is the class that does time functions in Silverlight 2 or later. You set the Interval to 10,000,000 ticks (1 second) and add an event handler for the tick event (which we have already coded). Then you start the timer. That is all there is too it. When I am done, I have the following page:

FinishedSite

If you want to see this running, just go to http://crazycancertour.com prior to September 1, 2009 at 6:10 PM (when the “tour” begins – I may change this time, depending). If you do not have Silverlight loaded, it will take less than a minute (a few second broadband) to download and install. I will be moving my blog entries over to the Developer section of the site once it goes live and I am aiming for videos of most of these entries. I have set up a Twitter account @crazycancerdev to Tweet out new entries that come out later this month and in September. For those more interested in childhood cancer awareness, there is Twitter @crazycancertour, as well.

Peace and Grace,
Greg

Twitter: @gbworld

Advertisements

One Response to Silverlight 3 Countdown Counter

  1. Maulik says:

    Here is my version of reverse countdown timerhttp://mauliksoni.spaces.live.com/blog/cns!D1697DEC80818A03!537.entry

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: