Click here to Skip to main content
Click here to Skip to main content
Go to top

TwittiBot: Adventures in Developing a WPF Hybrid Smart Client for Twitter

, 1 Jun 2009
Rate this:
Please Sign up or sign in to vote.
An article on creating a Hybrid Smart Client for Twitter using the Windows Presentation Foundation and the TwitterLib class library
twittibot_sm.jpg

Table of Contents

Introduction

This article demonstrates TwittiBot, one programmer's first fumbling attempt to learn and use Windows Presentation Foundation and the TwitterLib class library to create a hybrid smart client for Twitter.

The project source code is included; the solution was produced on Windows XP SP 2 using Visual Studio 2008, .NET Framework 3.5, and SQL Server 2000.

Background, or A Man Hug For Mr. Gates

Although I've been programming for over eleven years now, I've never published anything to a web venue with a large audience before (much less a technical article), that is unless you count the time that I sent in some haiku poetry to MSDN’s now-defunct Technical Column “Web Team Talking”. Go ahead, I dare you to Google “web men talking haiku”; if you follow the link in the top search result, you'll find the following pithy piece I penned:

My ActiveX doc
Displaying in the browser.

Finally, it WORKS

But I digress.

I'm currently employed as a telecommuting web application developer for a Fortune 50 managed health company. I get to work at home, in an industry that helps people live healthier lives, and I get to play with cool technology tools.

Best. Job. EVER.

Our team is standardized on .NET, but even more than that, I'm a Microsoft fan and I'm not ashamed to say it. It’s because of Microsoft that I even have a job. If I were to ever meet Bill Gates one day, I'd go up to him and say, “Mr. Gates, Thank You for all your hard work over the years. I'm able to make a living because of all you've done, and I just wanted to express my appreciation in person."

And then I'd give him a great big Man Hug.

Actually, there are about a dozen other people that I should probably thank, including my buddy Big Dan Zumwalt, who was my programming mentor early on in my career. Don't worry, he's already gotten his Man Hug. But again, I digress.

Was ist dieses Ding? The Gestalt of Twitter

I'd been dimly aware of social networking sites such as MySpace, Facebook, and Twitter (all my family and friends all live I.R.L.; what did I need virtual friends for?), and I'd recently read how Twitter is one of the fastest growing social networking sites. So, one day I browsed to the site, more curious about whether there was a Twitter API than wanting to chat with strangers (which I guess confirms my status as a card-carrying, propeller-headed geek).

I'd read enough about the site to know that it had to do with SMS (Short Message Service) from your browser. To be honest, the whole Twitter idea kind of irritated me, because I have two daughters, 22 and 18, and the whole 140-character messaging thing reminded me of their near-incessant texting. Guess I must be of the wrong generation: tail end of the Baby Boom instead of Gen Y.

Now, at first glance, Twitter seemed to me like nothing more than a 140-character online chat for the digirati. But I slowly began to notice that, in between the flirting and smileys, there were some users that were broadcasting some actually useful information. The more "tweets" (Twitter posts) I read, the more convinced I became that Twitter users were defining a whole new publishing platform, a sort of digital news stand with 140-character headlines.

As a Twitter member, one of the objectives is to get other users to subscribe or "follow" your updates. Could I possibly get other Twitter users to follow me if I posted regular updates, and if so, how often would I have to post? Once an hour? Twice? Three times? It occurred to me that frequent posts were desirable; the more frequent, the better. And the Twitter population was organized loosely around "birds of a feather" or special interest groups. Oddly enough, one special interest group was composed of those Twitter users with a special interest in the opposite sex. Could I possibly get other members to follow me if I supplied URLs to information around the web on relationships, dating, marriage, divorce, etc.?

So the little programmer voice inside my head (you've got one of those too, don't you?) said:

“There’s no way I'd waste my time manually posting to a web site. But if I could create an automated process that grabbed information from a database…”

And you're familiar with that sensation, right? That weird, wonderful, feeling of inspiration, of visions forming in your mind: seeing yourself creating the database, coding the app, slapping controls on your form. I immediately set to work on a proof-of-concept.

Agile: That's Just The Way I Roll

So the initial, modest goal was simply to create the code to generate a single "tweet" containing a URL and description. Once that was accomplished, the next phase would be to automate the post to occur at five-minute intervals, retrieving the description and URL at random from a database. As part of the testing/debugging process, I'd also have the code update a form label with a countdown between intervals, and a running status update each time an interval was reached.

I use Visual Basic every day, so naturally I began writing the proof-of-concept in VB.NET. (For the hybrid smart client, I decided to switch to C#, so the code snippets in this article are in C#).

The first challenge was writing the Tweet automation code. After signing up for a Twitter account, I soon discovered that Twitter had an API available for developers. Reviewing the documentation was a bit daunting, since it involved interacting directly with XML, with which I had little experience. A quick Google search yielded Bruno Piovan's blog entry wherein he detailed updating his Twitter account using Microsoft's Web client. Eureka! I copied his code and was rewarded with a working proof-of-concept! And even though I was a team of one, I was developing my app in rapid, Agile iterations!

Still, I felt strangely unfulfilled somehow. The POC worked, but for some reason it just didn't seem "slick" enough. Personally, when working with APIs, I prefer to employ some sort of wrapper class or library to abstract the details of the API (alright, I'm just plain lazy.) And ideally, the class library would be designed specifically to work with the target API. Was there such an animal, preferably of the .NET variety, for the Twitter API?

"...And hast thou found The TwitterLib? Come to my arms, my beamish boy!"

I was intrigued by some Google search results that mentioned a mysterious "TwitterLib" DLL, and in doing a little more research I found Rod Paddock's informative article (on another site that shall, of course, remain nameless) detailing his usage of the library, along with some sample code. "Cool!" I thought, "There IS a library out there! Now, where the heck do I download it?" Mr. Paddock's article merely teased me with the tantalizing prospect, but unfortunately didn't provide a link to TwitterLib.

By now, you can tell that Google is my constant friend. I finally discovered a post on Alan Le's blog, where he shares his journey into creating a wrapper class to the Twitter API for his "Witty" Twitter client, which he named "TwitterLib". Thank you, Mr. Le!

Tip: to work with TwitterLib, download and install Witty, then copy the TwitterLib.dll file to your Visual Studio project and set a reference.

Beyond Rod Paddock's somewhat sparse sample code, however, I could find precious few other articles documenting the TwitterLib classes and methods, and I certainly didn't have the energy to wade through the TwitterLib source on Google Code.

So, that meant experimentation. After a bit of trial and error, my efforts paid off. Here's my code for Tweeting using TwitterLib:

//update Twitter using Twitternet class from TwitterLib
private void UpdateTwitter()
{
    //use secure string object to encrypt password
    SecureString ssPassword = new SecureString();
    TwitterNet oTwittiClient = default(TwitterNet);

    try
    {
        //encrypt password
        foreach (char chCharacter in mstrPassword.ToCharArray())
        {
            ssPassword.AppendChar(chCharacter);
        }

        //instantiate new twitter client
        oTwittiClient = new TwitterNet(mstrUserName, ssPassword);

        //login
        oTwittiClient.Login();

        //set the client name; default is "Witty"
        oTwittiClient.ClientName = "The TwittiBot";

        //tweet
        oTwittiClient.AddTweet(mstrTweetText.ToString());
    }
    catch (Exception ex)
    {
       mstrErrorMessage += ex.Message.ToString();
    }
}

Mr. Le's employment of the SecureString class sent me scurrying for more research, but other than that, TwitterLib works like a champ!

Timers, Countdowns and Code Conversion...Oh, My!

The second challenge to overcome in writing the hybrid smart client was converting VB.NET to C#, because I'd quite forgotten how different the two languages really are. Case in point: a simple timer. In VB of course, you'd simply drop a timer control onto the designer surface and bam, you're done. Not so with C#. After Googling "C# timer" and examining various articles on the subject, I pounded out the following code using the DispatcherTimer class:

  • StartTimer. Initializes our DispatcherTimer and the countdown.
  • Main. Driven by the DispatcherTimer, our main subroutine keeps our five minute countdown and updates Twitter.
  • InitializeFiveMinuteCountdown. Invoked at start up, this function returns an integer representing the remaining seconds until the next five minute interval is reached.
  • GetNextFiveMinuteInterval. Given the current minutes of the hour, it calculates the nearest five minute increment.
  • LoadTimeLine. Populates a collection of tweets to be bound to a listbox.
public partial class Window1 : Window
{
    #region "Private Member Variables"
        private DispatcherTimer timer;
        private bool blnIntervalReached;
        private int iFiveMinuteCountdown;
        private string mstrErrorMessage;
        private Twittibot oTwittiBot;
    #endregion

    #region "Public Constructors"
        public Window1()
        {
            InitializeComponent();
            LoadTimeLine();    
        }

    #endregion

    #region "Public Methods"

        public void Timer_Tick(object sender, EventArgs eArgs)
        {
            if (sender == timer)
            {
	  	        //main routine
                Main();
            }
        }

        /// <summary>
        /// calculates the number of seconds until the next five minute
        /// increment of the hour
        public int InitializeFiveMinuteCountdown(DateTime StartDate)
        {
            DateTime dtStartDateTime = default(DateTime);
            int iNextFiveMinuteInterval = 0;
            int iSecondsCountDown = 0;
            TimeSpan tsNextFiveMinuteInterval = default(TimeSpan);
            int iHour = 0;

            try
            {
                //get the start date and time
                dtStartDateTime = StartDate;

                //get the next five minute interval
                iNextFiveMinuteInterval = 
			GetNextFiveMinuteInterval(dtStartDateTime.Minute);

                //check for the top of the hour
                if (iNextFiveMinuteInterval == 60)
                {
                    iHour = DateTime.Now.Hour + 1;                
                }
                else
                {
                    iHour = DateTime.Now.Hour;
                }

                //add five to the interval if it equals the current time
                if (iNextFiveMinuteInterval == DateTime.Now.Minute)
                {
                    iNextFiveMinuteInterval += 5;
                }

                //get the end time
                DateTime dtEndDateTime = new DateTime(DateTime.Now.Year, 
			DateTime.Now.Month, DateTime.Now.Day, 
			iHour, iNextFiveMinuteInterval, 0);

                //calculate the time difference and set a timespan
                tsNextFiveMinuteInterval = dtEndDateTime.Subtract(DateTime.Now);

                //get the seconds
                iSecondsCountDown = (tsNextFiveMinuteInterval.Minutes * 60) + 
				tsNextFiveMinuteInterval.Seconds;
            }
            catch (Exception ex)
            {
                mstrErrorMessage += ex.Message.ToString();
            }

            return iSecondsCountDown;
        }

    #endregion

    #region "Private Methods"
            
        /// <summary>
        /// The Main() subroutine drives our five minute countdown and 
        /// updates Twitter.
        /// </summary>
        private void Main()
        {
            lblTime.Content = DateTime.Now.ToString();
            iFiveMinuteCountdown = iFiveMinuteCountdown - 1;
            lblFiveMinuteCountdown.Content = iFiveMinuteCountdown;
            
            if (iFiveMinuteCountdown == 0)
            {
                txtStatus.Text += DateTime.Now.TimeOfDay.ToString() + "\r\n";
                txtStatus.Text += "5 minute interval reached." + "\r\n";
                txtStatus.Text += 
			"----------------------------------------------" + "\r\n";
                    
                oTwittiBot = new Twittibot();
                Thread oTweetThread = new Thread(oTwittiBot.Tweet);

                oTweetThread.Start();

                iFiveMinuteCountdown = 300;
            }
        } 

        /// returns the nearest five minute increment in the hour
        private int GetNextFiveMinuteInterval(int Minutes)
        {
            int result;
            int i = 0;

            try
            {
                /* spin through a loop from the value of Minutes
                 * until we reach 60
                 * or we hit a multiple of five
                 * incrementing the variable by one */
                for (i = Minutes; i <= 60; i++ )
                {
                    //use the modulus operator with 5 as a divisor
                    result = i % 5;

                    //if we get zero as remainder, we have got our next 5 minute value
                    if (result == 0)
                    {
                        return i;
                    }
                }
            }

            catch (Exception ex)
            {
                mstrErrorMessage += ex.Message.ToString();
            }

            return i;
        }

        /// <summary>
        /// returns a TimeLineItems collection and binds to list box
        /// </summary>
        private void LoadTimeLine()
        {
            Twittibot oTwittiBot = new Twittibot();
            TimeLineItems oTimeLine = oTwittiBot.GetTimeLineItems();

            lstTimeLine.ItemsSource = oTimeLine;
        }
			
        /// <summary>
        /// instantiates, initializes, and starts timer
        /// initializes countdown
        /// </summary>
        private void StartTimer()
	    {
	        timer = new DispatcherTimer();
            timer.Interval = TimeSpan.FromSeconds(1);
            timer.Tick += new EventHandler(Timer_Tick);
            timer.Start();

            lblTime.Content = DateTime.Now;

            DateTime dtStart = new DateTime(DateTime.Now.Year, 
			DateTime.Now.Month, DateTime.Now.Day, 
			DateTime.Now.Hour, DateTime.Now.Minute, DateTime.Now.Second);
            iFiveMinuteCountdown = InitializeFiveMinuteCountdown(dtStart);
            txtStatus.Text += "Running...current session started at " + 
					DateTime.Now.ToString() + "\r\n";
        }

    #endregion

    #region "Private Events"
        private void btnRefresh_Click(object sender, System.Windows.RoutedEventArgs e)
        {
            LoadTimeLine();
        }

        private void btnStart_Click(object sender, System.Windows.RoutedEventArgs e)
        {           	
            StartTimer();
        }

        private void btnStop_Click(object sender, System.Windows.RoutedEventArgs e)
        {
            timer.Stop();
        } 

    #endregion
}

I won't bore you with my data access code, but it's there in the source if you want a look (The URLs and descriptions, you'll have to find for yourself!).

With the logic out of the way, I turned to the task of creating the GUI.

Windows Presentation Foundation: Hey, This Looks Vaguely Familiar...

TwittiBot Screenshot

At the beginning of the article, I mentioned being dragged, kicking and screaming, into learning a new development technology. That's because for the last five years, I've been heads down, nose-to-the-grindstone working exclusively in ASP.NET. Honestly, I was so enthralled by web development that I think I unconsciously determined never to set foot in Windows development again. Of course, I'd read Microsoft technology articles mentioning "Avalon" or, later, "Windows Presentation Foundation" but these terms were meaningless to me. After all, we programmers - perhaps as a survival mechanism -  have developed that cognitive skill known as "selective attention", the ability to screen out and ignore that stimuli which we believe is unimportant, and focus on only that stimuli that we believe really is important. Those new terms didn't even make it onto my radar screen, and I regret this in retrospect. 

However, once I serendipitously stumbled upon CodeProject's little Smart Client Competition, I immediately set out to educate myself about WPF. After reading some articles on the web, and having previously seen Windows Vista running on some demo laptops at the local Office Depot, I understood WPF's function: a graphics subsystem of the .NET Framework 3.x designed to provide a visually richer user experience.

Well, I was already working with Visual Studio 2008, so the next step was to download Microsoft Expressions Blend.

Tip: If you want to try before you buy, do yourself a favor and download the Preview of Expressions Blend 3; avoid version 2. Among other things, Blend 3 boasts the advantage of having Intellisense work when editing XAML (more on XAML later). Ever tried editing source code without Intellisense? Bad times.

The first thing that I noticed when I launched Blend was that it seemed vaguely familiar somehow. Before my career as a developer, I had spent some time in the early to mid '80's as a graphic artist/technical illustrator. Well, we all remember the arrival of the Apple Macintosh and the advent of desktop publishing; I could see the writing on the wall, and I knew my industry was heading in the digital direction. So, I hopped on the DTP bandwagon and dived headfirst into using the Macintosh, PageMaker, and Illustrator. As the years went by, I also got PhotoShop, Director, and a little 3D program named Poser under my belt.

I mention this because the design of the Blend interface reminds me of working in those graphics programs, kind of like rolling elements of Photoshop, Illustrator, Director, and Poser all into one.  I suppose, in a way, that this is intentional, since Blend's raison d'être is the creation of rich graphical interfaces for the web and Windows. If you've dabbled in multimedia production as I have, you may get a similar sense of déjà vu.

The scramble to find a crash course in learning Expressions Blend began. In the ensuing flurry of Google searches, I found a nifty little video from Microsoft's MIX07 presentation.  Celso Gomes' contribution to the presentation, in particular, provided the inspiration for creating the TwittiBot client interface. Rich Stern's blog entry on creating a simple Twitter client was also extremely helpful, especially with the ListBox element.

The Interface: No More Plain Jane

Having downloaded and installed Blend, and having run through a couple of tutorials, I now set myself to the task of making TwittiBot look pretty.

My main objective was clear: per the contest rules, my hybrid smart client had to "consume some kind of web-based data feed". Well, I figured a Twitter Timeline qualified as some sort of web-based feed, so I determined to incorporate a simple list box element to display the Timeline in my GUI design, patterned after the databound list box in Rich Stern's Twitter Client sample.

So, as in the image above, I wanted to display a user's latest tweet updates including image, screen name, tweet text, and relative date in a list of items. However, where Rich Stern's listbox example had employed Microsoft's WebClient and NetworkCredential classes to consume the XML for the Twitter timeline, I had gone with Andrew Le's TwitterLib class library, which completely abstracted the complexities of the XML from me. And, while TwitterLib's Tweet Collection gave me Tweet items with a Twitter ID, Screen Name, and Tweet Text, the property containing the URL for a  user's image was in another completely different class, User.

I decided on an adaptation of Rich's solution, where I would create my own custom Item class with all the necessary properties, a companion Collection class, and a method that would populate all the properties of each Tweet item and return a Tweet collection to be bound to the list. Following is the code for my custom classes:

public class TimeLineItem
{
    #region "Private Member Variables"
        protected int mintID;
        protected string mstrTweetText;
        protected string mstrScreenName;
        protected string mstrImageURL;
        protected string mstrRelativeTime;
    #endregion
     #region "Public Properties"
        public int ID
        {
            get
            {
                return mintID;
            }
             set
            {
                mintID = value;
            }
        }
        public string ImageURL
        {
            get
            {
                return mstrImageURL;
            }
             set
            {
                mstrImageURL = value;
            }
        }
        public string TweetText
        {
            get
                {
                    return mstrTweetText;
                }
             set
                {
                    mstrTweetText = value;
                }
        }
        public string ScreenName
        {
            get
            {
                return mstrScreenName;
            }
             set
            {
                mstrScreenName = value;
            }
        }
        public string RelativeTime
        {
            get
            {
                return mstrRelativeTime;
            }
             set
            {
                mstrRelativeTime = value;
            }
        }
     #endregion
}
 public class TimeLineItems : CollectionBase
{
    public TimeLineItem this[int index]
    {
        get
        {
            return ((TimeLineItem)List[index]);
        }
         set
        {
            List[index] = value;
        }
    }
     public int Add(TimeLineItem oTimeLineItem)
    {
        return (List.Add(oTimeLineItem));
    }
     public int IndexOf(TimeLineItem oTimeLineItem)
    {
        return (List.IndexOf(oTimeLineItem));
    }
     public void Insert(int index, TimeLineItem oTimeLineItem)
    {
        List.Insert(index, oTimeLineItem);
    }
     public void Remove(TimeLineItem oTimeLineItem)
    {
        List.Remove(oTimeLineItem);
    }
     public bool Contains(TimeLineItem oTimeLineItem)
    {
        return (List.Contains(oTimeLineItem));
    }
}

Sha-XAML!

As if you didn't need another reminder of just how all-pervasive XML is, wrap your head around this: Blend describes interface elements completely in a flavor of XML called XAML, or Extensible Application Markup Language. You simply define your controls between tags, press F5, and Blend compiles your XML into a neat little GUI package. Slick, huh?

So the source for your interface ends up looking like the classic XML tree, with elements representing controls nested within each other in a hierarchical fashion. If you've created web forms for ASP.NET development, this won't seem totally unfamiliar.

The list box itself was fairly simple, following Rich's tutorial. In the XAML below, the points of interest are the "{Binding}" properties, especially for the image and text block controls. If you compare the XAML below with the public properties of the TimeLineItem class in the code above, you'll notice that the class properties map exactly to XAML databinding elements. Here's the XAML for the listbox:

 <ListBox Background="Transparent" IsSynchronizedWithCurrentItem="True" 
	Margin="22.144,35.277,23.76,70" Grid.Row="1" x:Name="lstTimeLine">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <DockPanel MaxHeight="75" 
		MaxWidth="{Binding ElementName=lstTimeline, Path=ActualWidth}">
                <Border Margin="5" DockPanel.Dock="Left" 
		BorderBrush="White" BorderThickness="1" Height="48" 
		Width="48" HorizontalAlignment="Center">
                    <Image Source="{Binding ImageURL, IsAsync=True}" 
			Height="48" Width="48" VerticalAlignment="Top" />
                </Border>
                <StackPanel Margin="5" VerticalAlignment="Top" DockPanel.Dock="Right">
                    <TextBlock Foreground="White" Text="{Binding ScreenName}" 
			HorizontalAlignment="Left" FontFamily="Trebuchet" 
			FontWeight="Bold"></TextBlock>
                    <TextBlock Foreground="White" Text="{Binding TweetText}" 
			HorizontalAlignment="Left" FontFamily="Trebuchet" 
			FontSize="9" TextWrapping="WrapWithOverflow" Width="200">
			</TextBlock>
                    <TextBlock Foreground="White" Text="{Binding RelativeTime}" 
			HorizontalAlignment="Left" FontFamily="Trebuchet" 
			FontSize="9" FontStyle="Italic" 
			TextWrapping="WrapWithOverflow" Width="200"></TextBlock>
                </StackPanel>
            </DockPanel>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

And, the databinding code is ridiculously simple: set a collection as the ItemSource of your listbox:

/// <summary>
/// returns a TimeLineItems collection and binds to list box
/// </summary>
private void LoadTimeLine()
{
    Twittibot oTwittiBot = new Twittibot();
    TimeLineItems oTimeLine = oTwittiBot.GetTimeLineItems();

    lstTimeLine.ItemsSource = oTimeLine;
}

Conclusion

If you're like me and (until recently) haven't taken the plunge into Windows Presentation Foundation, do yourself a favor: dive in. WPF is a rollicking good time, it's relatively easy to get started using (hopefully my little primer will help!), and there's no doubt that a great deal of your .NET development experience and expertise will transfer easily to this exciting new frontier. If you're interested in dabbling in Twitter programming and want a class library to do the heavy lifting with XML and interacting with the Twitter API, I can heartily recommend TwitterLib (and now you know where to get it!)

From the length of my article, it may seem as if I learned a great deal; in reality, this experience has only served to prove to myself just how much I really don't know. Microsoft continues its never-ending pursuit of The Next Big Thing, and I've resigned myself to the reality that I will never dwell on the leading, blood-stained razor's edge of development technology.

But, that's okay; it is what it is. At the end of the day, I consider myself fortunate to possess a marketable skill and a job; so, in the final analysis, I'm still very grateful.

And if I ever get to meet him, Mr. Gates will still get his Man Hug.

History

  • 29th May, 2009: Initial version

License

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

Share

About the Author

Dwayne Macadangdang
Web Developer
United States United States
A former graphic artist, technical illustrator, and sequential illustrator (comic book artist), Dwayne currently develops and maintains web applications for a Fortune 50 health and wellness company. He and his wife and their two daughters reside in California, U. S. A.

Comments and Discussions

 
GeneralMy vote of 5 Pinmembermasterminduday3-Oct-10 5:49 
QuestionMissing DB Tables? Pinmemberksuvalk8-Jun-09 9:59 
AnswerRe: Missing DB Tables? Pinmembermbaocha25-Aug-09 11:47 
GeneralA promising start, but consider MVVM before you go much further. PinmvpPete O'Hanlon2-Jun-09 10:36 
GeneralRe: A promising start, but consider MVVM before you go much further. PinmemberDwayne Macadangdang2-Jun-09 17:19 
GeneralRe: A promising start, but consider MVVM before you go much further. PinmvpPete O'Hanlon21-Jun-09 10:55 
GeneralGood Stuff Pinmemberbinteractive1-Jun-09 13:05 
GeneralRe: Good Stuff PinmemberDwayne Macadangdang1-Jun-09 19:15 

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
Web04 | 2.8.140921.1 | Last Updated 1 Jun 2009
Article Copyright 2009 by Dwayne Macadangdang
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid