Click here to Skip to main content
Click here to Skip to main content

WPF Weather App

, 5 Nov 2010
Rate this:
Please Sign up or sign in to vote.
A WPF application that provides weather information using GOOWAPI
Prize winner in Competition "Best VB.NET article of November 2010"
WeatherScreenshot1.jpg     WeatherScreenshot2.jpg

Introduction

The WPF weather app provides the user with information on the current and forecast weather conditions. The app shows the current weather condition (e.g. Cloudy), temperature (in both degrees Celsius and Fahrenheit), wind speed and direction, and humidity. The application also provides two day forecast data of expected conditions and temperature highs and lows (in degrees Fahrenheit). The user can specify a location and, if internet connection is available, get the weather data for the specified location.

Background

The WPF weather app makes use of the Google Weather API which returns XML data with weather information (I will explain more on this API later). You will also note that in the code I make use of the LINQ to XML API, to query data from the weather API, plus XML axis properties which are a VB.NET language feature.

Requirements

To open the solution provided from the download link above, you require either of the following;

  • Visual Studio 2010 Express
  • Expression Blend 4

The other requirements are that you should be conversant with XML, LINQ and threading in WPF.

NB: If you experience compilation errors when trying to run the project, ensure that you have installed the WPF Toolkit - February 2010 Release. If you don't have the toolkit installed, you will notice the following error:

Missing reference to 'System.Windows.Controls.Input.Toolkit'.  

The Google Weather API  

The Google Weather API returns weather information but as of the time of writing it has no official documentation. The use of the API therefore has a caveat since the structure of the data returned may change or the API may be deprecated entirely. Its only official use is on iGoogle for getting weather information. That said, its current 'unofficial' status does not prevent us from making use of it while we still can.

The Google Weather API returns weather information by passing one of the following four values:

  1. Zip code
  2. City name, e.g. Dubai
  3. City name and state, e.g. Boston, MA
  4. City name and country name, e.g. Nairobi, Kenya

For the WPF Weather App, the listed values will suffice to provide you with the necessary information. That said, some unofficial sources indicate that the API accepts the following three parameters:

  • Place: Either one of the four possible values listed above. This is a required field.
  • Language: An ISO 639-1 Language Code. This is an optional field.
  • Unit: Temperature values passed in degrees Celsius or Fahrenheit (default). This is an optional field.

NB: The API does not provide feedback for all locations so don't be surprised if you don't get data for an area that you may be familiar with.

The following is the structure of XML data returned by GOOWAPI [goo-wap-i], I will refer to the weather API by this name from here onwards:

<?xml version="1.0" encoding="utf-8"?>
<xml_api_reply version="1">
  <weather module_id="0" tab_id="0" mobile_row="0" mobile_zipped="1" row="0" section="0">
    <forecast_information>
      <city data="Nairobi, Nairobi" />
      <postal_code data="Nairobi, Kenya" />
      <latitude_e6 data="" />
      <longitude_e6 data="" />
      <forecast_date data="2010-10-27" />
      <current_date_time data="2010-10-27 04:00:00 +0000" />
      <unit_system data="US" />
    </forecast_information>
    <current_conditions>
      <condition data="Cloudy" />
      <temp_f data="63" />
      <temp_c data="17" />
      <humidity data="Humidity: 94%" />
      <icon data="/ig/images/weather/cloudy.gif" />
     <wind_condition data="Wind: NE at 6 mph" />
    </current_conditions>
    <forecast_conditions>
      <day_of_week data="Wed" />
      <low data="59" />
      <high data="75" />
      <icon data="/ig/images/weather/chance_of_storm.gif" />
      <condition data="Chance of Storm" />
    </forecast_conditions>
    <forecast_conditions>
      <day_of_week data="Thu" />
      <low data="57" />
      <high data="82" />
      <icon data="/ig/images/weather/chance_of_rain.gif" />
      <condition data="Chance of Rain" />
    </forecast_conditions>
    <forecast_conditions>
      <day_of_week data="Fri" />
      <low data="57" />
      <high data="78" />
      <icon data="/ig/images/weather/chance_of_rain.gif" />
      <condition data="Chance of Rain" />
    </forecast_conditions>
    <forecast_conditions>
      <day_of_week data="Sat" />
      <low data="59" />
      <high data="77" />
      <icon data="/ig/images/weather/chance_of_storm.gif" />
      <condition data="Chance of Storm" />
    </forecast_conditions>
  </weather>
</xml_api_reply>

As you can see, there's a <current_conditions> element with child elements containing values of current weather conditions in their data attributes. The same applies to <forecast_conditions> elements that have forecast data in the data attributes of their child elements. Our focus of interest will therefore be on the <current_conditions> and the <forecast_conditions> elements.

NB: While goowapi provides four day forecast information, the WPF Weather App will only gather information for two days, i.e., the first day after the current day and the second day after the current day.

The Weather App

How It Works

In order to display the current weather and forecast conditions of a particular location, the user types in a location in the textbox and clicks on the Go button (the arrow next to the text box). For this app, it is recommended that the user enters a location based on the 2nd, 3rd and 4th values specified as possible parameters in the above Google Weather API section, i.e., city name, city name & state (for State side data), or city name and country.

WeatherScreenshot3.jpg

Note that when entering certain values, like Berlin, a list of possible location(s) is displayed as you type. This is because the textbox is an AutoCompleteBox that contains several values in its ItemSource property.

WeatherScreenshot4.jpg

A progress bar is shown at the top of the application while data is being downloaded. This shouldn't be a very visible feature if you have fast internet connection.

WeatherScreenshot5.jpg

To display weather information, two conditions have to be fulfilled:

  1. There should be internet connection.
  2. If there's internet connection, the location provided should be one that goowapi is conversant with. Otherwise, the application displays a message box informing the user that it was unable to get weather information for the desired location.

The User Interface

I first designed the UI in Expression Design and added some extra elements in Expression Blend. I will not cover the details of the design process, but I recommend that if you want to have a better look at the UI elements, do so in Expression Blend (4).

The following image shows the UI elements that are of interest for displaying weather information. The rest of the elements are just rectangles, ellipses, and a combination of rectagles and ellipses (the thermometer and wind turbine).

WeatherDetails.jpg

The Code

When the application is initialized, the following code is executed:

Private Sub MainWindow_Initialized(ByVal sender As Object, _
	ByVal e As System.EventArgs) Handles Me.Initialized
    timer = New DispatcherTimer
    AddHandler timer.Tick, AddressOf timer_Tick
    timer.Interval = New TimeSpan(0, 5, 0)
    timer.Start()

    ' Fill the AutoCompleteBox collection. 
    SearchAutoTextBox.ItemsSource = My.Settings.UserLocations
End Sub

We initialize a DispatcherTimer object and add a handler for its Tick event. We also set the ItemsSource property of the AutoCompleteBox with values from an application setting named UserLocations. This setting is of type StringCollection, which is found in the System.Collections.Specialized namespace. To view the settings, open the project properties window and click on the Settings tab.

WeatherScreenshot6.jpg

The UserLocations setting contains a collection of string values and in this case, the default values provided are city and country names in the format; City, CountryName. Click on the ellipse in the Value section of the UserLocations setting to view or add default values in the String Collection Editor.

StringCollection.jpg

In the Loaded event of the Window, the following method is called:

Private Sub MainWindow_Loaded(ByVal sender As Object, _
	ByVal e As System.Windows.RoutedEventArgs) Handles Me.Loaded
    Dim thread As New Thread(AddressOf WeatherConditions)
    thread.Start()
End Sub

Here we simply call the WeatherConditions method on a background thread.

Private Sub WeatherConditions()
    location = My.Settings.Location
    src = "http://www.google.com/ig/api?weather=" & location
    
    ' Attempt to get xml data. 
    Try
        weatherDoc = XDocument.Load(src)
    Catch ex As System.Net.WebException
        MessageBox.Show("Internet connection unavailable. " & _
                        "Unable to acquire latest weather info.", "Error!", _
                        MessageBoxButton.OK, MessageBoxImage.Error)
        Exit Sub
    End Try
    
    '' Check for error from Google server.
    'This portion of code will only execute if there's an invalid
    'value in the location setting, something that shouldn't occur
    'since only valid locations are saved in the location application
    'setting on calls to UserSpecifiedLocationForecast method.
    Dim problem = weatherDoc...<problem_cause>(0)
    If problem IsNot Nothing Then
        MessageBox.Show("Unable to get the weather conditions of " & location & _
                        vbCrLf & "Try again and enter data in the format " & _
                        ": city, country" & vbCrLf & "e.g. London, England", "Error")
        ' Clear textbox.
        Me.Dispatcher.BeginInvoke(DispatcherPriority.Normal, _
		New ThreadStart(AddressOf ClearSearchTextBox))
        Exit Sub
    End If
    
    ' Update UI elements.
    Me.Dispatcher.BeginInvoke(DispatcherPriority.Normal, _
		New ThreadStart(AddressOf CurrentConditions))
    Me.Dispatcher.BeginInvoke(DispatcherPriority.Normal, _
		New ThreadStart(AddressOf ForecastConditions))
    
End Sub

The WeatherConditions method assigns a location value to a string variable, location, based on a value from the Location application setting. A Try...Catch block is used to catch a System.Net.WebException that may be thrown when attempting to download data from goowapi and a possible goowapi error is dealt with accordingly.

GOOWAPI Exceptions

Goowapi will throw an exception if a location is passed that it is not conversant with (this is an assumption based on personal tests). There may be other reasons for generating exceptions which, as of the time of writing, I'm not familiar with. The following XML data is returned by the API if it throws an exception:

<?xml version="1.0" encoding="utf-8"?>
<xml_api_reply version="1">
  <weather module_id="0" tab_id="0" mobile_row="0" 
	mobile_zipped="1" row="0" section="0">
    <problem_cause data="" />
  </weather>
</xml_api_reply>

The code Dim problem = weatherDoc...<problem_cause>(0), in WeatherConditons creates an XElement variable, using type inference, which we check to ensure that an error was not returned. The line of code can also be written as...

Dim problem = weatherDoc...<problem_cause>.Single() 

...since there's only a single element in the generated sequence.

Updating the UI

Since WeatherConditions is executed on a background thread, we schedule the CurrentConditions and ForecastConditons methods as tasks for the Dispatcher. These two methods update the UI elements with values generated from goowapi:

' Set the values of controls showing current weather conditions.
Private Sub CurrentConditions()
    Dim currCondition = weatherDoc...<current_conditions>.Single() _
                       .Element("condition").@data
                       
    Dim temp_f = weatherDoc...<current_conditions>.Single() _
                        .Element("temp_f").@data
                        
    Dim temp_c = weatherDoc...<current_conditions>.Single() _
                        .Element("temp_c").@data
                        
    Dim humidity = weatherDoc...<current_conditions>.Single() _
                        .Element("humidity").@data
                        
    Dim imageSrc = "http://www.google.com" & weatherDoc...<current_conditions> _
                   .Single().Element("icon").@data
                   
    Dim windCondition = (weatherDoc...<current_conditions>.Single() _
                        .Element("wind_condition").@data).Substring(6)
                        
    ' Set text block values
    cityTxtBlck.Text = My.Settings.Location
    currConditionTxtBlck.Text = currCondition
    temp_fTxtBlck.Text = temp_f & "° F"
    temp_cTxtBlck.Text = temp_c & "° C"
    humidityTxtBlck.Text = humidity
    windSpeedTxtBlck.Text = windCondition
    
    ' Set image control image
    Dim bi As New BitmapImage
    
    bi.BeginInit()
    bi.UriSource = New Uri(imageSrc)
    bi.EndInit()
    
    currConditionIcon.Source = bi
End Sub
' Set the values of controls showing the latest forecast conditions.
Private Sub ForecastConditions()

    Dim dayOfWeek1 = weatherDoc...<forecast_conditions>(0).Element("day_of_week").@data
    Dim dayOfWeek2 = weatherDoc...<forecast_conditions>(1).Element("day_of_week").@data
    
    Dim low1 = weatherDoc...<forecast_conditions>(0).Element("low").@data
    Dim low2 = weatherDoc...<forecast_conditions>(1).Element("low").@data
    
    Dim high1 = weatherDoc...<forecast_conditions>(0).Element("high").@data
    Dim high2 = weatherDoc...<forecast_conditions>(1).Element("high").@data
    
    Dim condition1 = weatherDoc...<forecast_conditions>(0).Element("condition").@data
    Dim condition2 = weatherDoc...<forecast_conditions>(1).Element("condition").@data
    
    Dim imageSrc1 = "http://www.google.com" & _
                    weatherDoc...<forecast_conditions>(0).Element("icon").@data
    Dim imageSrc2 = "http://www.google.com" & _
                    weatherDoc...<forecast_conditions>(1).Element("icon").@data
                    
    fcastDayTxtBlck1.Text = dayOfWeek1
    fcastDayTxtBlck2.Text = dayOfWeek2
    
    fcastLowTxtBlck1.Text = low1 & "° F"
    fcastLowTxtBlck2.Text = low2 & "° F"
    
    fcastHighTxtBlck1.Text = high1 & "° F"
    fcastHighTxtBlck2.Text = high2 & "° F"
    
    fcastConditionTxtBlck1.Text = condition1
    fcastConditionTxtBlck2.Text = condition2
    
    ' Set images.
    Dim bi1 As New BitmapImage
    Dim bi2 As New BitmapImage
    
    bi1.BeginInit()
    bi1.UriSource = New Uri(imageSrc1)
    bi1.EndInit()
    
    bi2.BeginInit()
    bi2.UriSource = New Uri(imageSrc2)
    bi2.EndInit()
    
    fCastIcon1.Source = bi1
    fCastIcon2.Source = bi2
End Sub

Forecast Data of User Specified Location

When the user enters a location in the AutoCompleteBox and clicks on the GoButton, the following code is executed:

Private Sub GoButton_Click(ByVal sender As Object, _
	ByVal e As System.Windows.RoutedEventArgs) Handles GoButton.Click
    CheckAutoCompleteBox()
    ' Exit sub-procedure if textbox is empty.
    If proceed = False Then
        Exit Sub
    Else
        Dim thread As New Thread(AddressOf UserSpecifiedLocationForecast)
        thread.Start()
    End If
End Sub

In the GoButton_Click event handler, we first call a method that checks whether the AutoCompleteBox is empty and proceed to call the UserSpecifiedLocationForecast method on a background thread if a value is present.

' Check whether the user has entered a location in the AutoCompleteBox.
Private Sub CheckAutoCompleteBox()
    If SearchAutoTextBox.Text <> String.Empty Then
        location = SearchAutoTextBox.Text
        src = "http://www.google.com/ig/api?weather=" & location
        proceed = True
    Else
        MessageBox.Show("Enter a location in the location textbox.", "Weather")
        proceed = False
    End If
End Sub
' Get forecast and current weather conditions using location 
' specified by user in the AutoCompleteBox.
Private Sub UserSpecifiedLocationForecast()
    ' Stop timer from updating controls.
    timer.Stop()
    
    ' Attempt to load xml data from Google server.
    Try
        weatherDoc = XDocument.Load(src)
    Catch ex As System.Net.WebException
        MessageBox.Show("Internet connection failed. Ensure that you are" & _
                        " connected to the internet.", "Error", MessageBoxButton.OK, _
                        MessageBoxImage.Error)
        Exit Sub
    End Try
    
    ' Show the progressbar.
    Me.Dispatcher.BeginInvoke(DispatcherPriority.Normal, _
		New ThreadStart(AddressOf ShowProgressBar))
    
    ' Check for error returned by Google server.
    Dim problem = weatherDoc...<problem_cause>(0)
    If problem IsNot Nothing Then
        MessageBox.Show("Unable to get the weather conditions of " & location & _
                        vbCrLf & "Try again and enter data in the format " & _
                        ": city, country" & vbCrLf & "e.g. London, England", "Error")
        ' Clear textbox.
        Me.Dispatcher.BeginInvoke(DispatcherPriority.Normal, _
		New ThreadStart(AddressOf ClearSearchTextBox))
        
        ' Hide the progressbar.
        Me.Dispatcher.BeginInvoke(DispatcherPriority.Normal, _
		New ThreadStart(AddressOf HideProgressBar))
        Exit Sub
    End If
    
    ' Save the location entered by the user as user's preferred 
    ' location setting and add the location to the UserLocations
    ' collection.
    My.Settings.Location = location
    ' Check whether location is already available in setting.
    If My.Settings.UserLocations.Contains(location) <> True Then
        My.Settings.UserLocations.Add(location)
    End If
    My.Settings.Save()
    
    ' Update UI elements.
    Me.Dispatcher.BeginInvoke(DispatcherPriority.Normal, _
		New ThreadStart(AddressOf CurrentConditions))
    Me.Dispatcher.BeginInvoke(DispatcherPriority.Normal, _
		New ThreadStart(AddressOf ForecastConditions))
    
    ' Clear textbox.
    Me.Dispatcher.BeginInvoke(DispatcherPriority.Normal, _
		New ThreadStart(AddressOf ClearSearchTextBox))
    ' Hide progress bar.
    Me.Dispatcher.BeginInvoke(DispatcherPriority.Normal, _
		New ThreadStart(AddressOf HideProgressBar))
    
    ' Restart timer.
    timer.Start()
    
End Sub

In the UserSpecifiedLocationForecast method, we first stop the timer, whose purpose I'll explain later, and check for network and goowapi errors. We then set the Location setting to the value provided in the AutoCompleteBox, which at this point is valid having passed the necessary checks, and add the location provided to the collection of strings in the UserLocations setting. We then persist the settings by calling the Settings.Save() method and proceed to update the user interface. Notice that when downloading data from the Google server, a ProgressBar will be shown and hidden when necessary.

UI Updates

Weather changes with time. It can be warm-and-sunny now and cold-and-rainy the 'next' minute. To keep up with weather changes, the WPF Weather App checks for current weather conditions every five minutes. This is done using the DispatcherTimer object which we initialized in the MainWindow's Initialized event handler. The event handler for timer's Tick event is shown below:

    Private Sub timer_Tick(ByVal sender As Object, ByVal e As EventArgs)
        ' Get the latest weather conditions every five minutes.
        Dim thread As New Thread(AddressOf WeatherConditions)
        thread.Start()
    End Sub

NB: I'm not sure how often the weather data is updated by Google, so I have used my own time assumption for updating the UI.

Animation

You must have noticed the wind turbine whose blades are constantly rotating. I wrote no code for this, just grouped three ellipses, adjusted the group's center point and created a storyboard, OnLoaded1, where I rotated the group 360° clockwise on a timeline of two seconds. I then set the RepeatBehavior to Forever. You can view the storyboard in Expression Blend by opening OnLoaded1 from the Objects and Timeline panel.

WeatherScreenshot7.jpg

WeatherScreenshot8.jpg

The storyboard is triggered by the application's OnLoaded event.

Current Weather Condition Image

The image shown in the application for the current weather condition is certainly not the prettiest due to scaling. If the Image control were smaller it would look better, like those of the forecast conditions. You can therefore write a Select Case checking for a particular image and display your custom images or elements. To do that, check for the following values that are returned by goowapi in the data attributes of the <icon> elements of either the <current_conditions> or <forecast_conditions> elements:

  • /ig/images/weather/sunny.gif
  • /ig/images/weather/mostly_sunny.gif
  • /ig/images/weather/partly_cloudy.gif
  • /ig/images/weather/mostly_cloudy.gif
  • /ig/images/weather/chance_of_storm.gif
  • /ig/images/weather/rain.gif
  • /ig/images/weather/chance_of_rain.gif
  • /ig/images/weather/chance_of_snow.gif
  • /ig/images/weather/cloudy.gif
  • /ig/images/weather/mist.gif
  • /ig/images/weather/storm.gif
  • /ig/images/weather/thunderstorm.gif
  • /ig/images/weather/chance_of_tstorm.gif
  • /ig/images/weather/sleet.gif
  • /ig/images/weather/snow.gif
  • /ig/images/weather/icy.gif
  • /ig/images/weather/dust.gif
  • /ig/images/weather/fog.gif
  • /ig/images/weather/smoke.gif
  • /ig/images/weather/haze.gif
  • /ig/images/weather/flurries.gif

Tip: You can create animations in Blend and run them when appropriate.

Credits

I have to give credit to Minhajul Shaoun whose article spurred my interest in the Google Weather API. I'm not an ASP.NET guy, but the article was quite enlightening. Credit also goes to Joseph C. Rattz, JR. and Dennis Hayes whose book, Pro LINQ: Language Integrated Query in VB 2008, provided me with wonderful insight on LINQing in VB.NET. I recommend this book to anyone interested in learning the details of Language Integrated Query.

Conclusion

If you've read the whole article up to this point, "thank you" for taking the time to do so. I hope it was useful in one way or another. That said, I hope goowapi will be around for a long time. It is definitely useful to both desktop and web developers. The acronym I've christened the Google Weather API will also hopefully stick and save us from having to say two words and an acronym too many times when describing the API to friends, family, clients or fellow developers.

History

  • 1st November, 2010: Initial post

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Meshack Musundi
Software Developer
Kenya Kenya
Meshack is an avid programmer with a bias towards WPF and VB.NET. He has about 5 years of programming experience initially starting off with Java before shifting to .NET, thanks to the allure of WPF. He has developed several applications, and written several articles about them, which can be viewed here on CodeProject. He currently resides in a small town in Kiambu county, Kenya.
 
Awards;
  • CodeProject MVP 2013
  • CodeProject MVP 2012
  • Best VB.NET article of August 2013
  • Best VB.NET article of February 2013
  • Best VB.NET article of October 2012
  • Best VB.NET article of July 2012
  • Best VB.NET article of February 2012
  • Best VB.NET article of January 2012
  • Best VB.NET article of November 2011
  • Best VB.NET article of June 2011
  • Best VB.NET article of May 2011
  • Best VB.NET article of March 2011
  • Best VB.NET article of February 2011
  • Best VB.NET article of January 2011
  • Best VB.NET article of December 2010
  • Best VB.NET article of November 2010

Comments and Discussions

 
GeneralMy vote of 5 Pinmemberridoy7-Aug-13 0:51 
GeneralRe: My vote of 5 PinmvpMeshack Musundi7-Aug-13 1:10 
GeneralMy vote of 5 Pinmembermanoj kumar choubey29-Jul-13 18:45 
GeneralRe: My vote of 5 PinmvpMeshack Musundi30-Jul-13 5:01 
GeneralRe: My vote of 5 Pinmembermanoj kumar choubey30-Jul-13 18:51 
GeneralRe: My vote of 5 PinmvpMeshack Musundi7-Aug-13 1:10 
GeneralRe: My vote of 5 Pinmembermanoj kumar choubey7-Aug-13 1:59 
QuestionI would convert it to C# PinmemberMember 193768720-Aug-12 22:45 
AnswerRe: I would convert it to C# PinmvpMeshack Musundi21-Aug-12 3:51 
GeneralMy vote of 5 Pinmemberaofeng325-Jun-12 0:55 
GeneralMy vote of 5 Pinmemberaofeng325-Jun-12 0:47 
Questionxml in Spanish PinmemberSandra Calvo24-Apr-12 7:52 
AnswerRe: xml in Spanish PinmvpMeshack Musundi24-Apr-12 22:49 
GeneralRe: xml in Spanish PinmemberSandra Calvo25-Apr-12 5:05 
Hi, thanks for answering.
That was what I had tried, but does not work, do not know why.
I get error here:
weatherDoc = XDocument.Load (src)
was controlled not XmlException
GeneralRe: xml in Spanish PinmvpMeshack Musundi25-Apr-12 20:14 
GeneralMy vote of 5 Pinmemberstefangab9510-Feb-12 8:36 
GeneralRe: My vote of 5 PinmvpMeshack Musundi12-Mar-12 20:39 
GeneralMy vote of 5 Pinmemberrizkyaqew18-Aug-11 23:32 
GeneralWell deserved PinmemberMarcelo Ricardo de Oliveira2-Jan-11 5:22 
GeneralRe: Well deserved PinmemberMeshack Musundi2-Jan-11 18:38 
GeneralMy vote of 5 PinmemberSChristmas14-Dec-10 4:31 
GeneralRe: My vote of 5 PinmemberMeshack Musundi17-Dec-10 19:49 
GeneralMy vote of 5 PinmemberRaviRanjankr13-Dec-10 18:58 
GeneralRe: My vote of 5 PinmemberMeshack Musundi13-Dec-10 20:32 
GeneralMy vote of 5 PinmemberAbhinav S11-Dec-10 4:44 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web03 | 2.8.140718.1 | Last Updated 6 Nov 2010
Article Copyright 2010 by Meshack Musundi
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid