Chapter 5. Observable Collections

Observable Collections "Don't call us. We'll call you."

Changes are always happening around us. However, we don't care for all of them. But we do for some and we would like to know whether something has changed in our area of interest. The same is the case with software development. Think of the following situation:

A new entry is added/deleted to a collection or some elements are moved inside the collection. All views that are attached (also known as binded) to that collection need to be notified that something has changed, so that they can adjust.

This process of sharing news that something has happened is called notification and notification can be achieved in two ways:

  1. We keep checking for the notification every once in a while. This is called Polling.

  2. The change itself knocks on your door. This is known as the Hollywood Principle . The quotation at the beginning of the chapter is known as "Hollywood Principle" in software. This means as soon as there is an event (read change), the event will notify every stakeholder. A stakeholder in software is something that has been registered for that event. There are several scenarios where notification on change of some value is vital for the proper functionality of the software system.

The classic example is weather gadgets. Gadgets get their data from some published web service and in most instances each gadget shows their data differently from each other. So, each of these gadgets' content will need to be updated when there is a change in the data.

This can become increasingly difficult if you have more data sources to monitor, and updating the views of the collections they are bound with get modified.

.NET Generics has a set of tools packaged in the System.Collections.ObjectModel and System.ComponenetModel namespaces.

In this chapter, we will learn how to monitor for a change using the ObservableCollection<T> class.

Before we dig deep, let's take a second closer look at change. There is more to the dictionary definition that can be extremely relevant for software engineering. How many types of change are possible?

There are basically three types of Change:

  1. Active change/Statistical change

  2. Passive change/Non-statistical change

  3. Data sensitive change

Active change/Statistical change

When some items are added or deleted from a collection, then the count of the collection changes. This is an active change that doesn't care about the existing data in the collection.

Example 1: As soon as you swipe your debit/credit card at a shopping mall, a transaction gets added to your bank account to which this debit/credit card is related.

Active change/Statistical change

Example 2: You discovered a transaction that can't be yours. It seems fraudulent. After careful examination, your Bank Manager is convinced and she deletes the transaction that you mentioned and reverts the cash back to your account. As soon as she deletes the transaction, it gets added to a list of deleted transactions. So, in this case, an active change takes place in the deleted transaction list due to another active transaction in your bank account list.

Active change/Statistical change

Passive change/Non-statistical change

Sometimes, the items from a collection are not deleted or added, instead they change positions or swap positions. This is a physical change in the collection; however, this doesn't change the statistics (such as length/count) of the collection.

For example, assume that you are sorting your bank account transactions according to the amount. Swapping transactions is a mandatory sub-operation, in this case, and results in a non-statistical positional change.

Data sensitive change

The preceding two changes don't care about the data already in the collection. So, if any of the elements in the collection changes, that's a data sensitive change. The physical layout of the collection doesn't change.

For example, assume that you want to monitor the weather for several locations. If any of the weather properties (such as temperature, humidity) changes in any of these locations, you want to update that detail. This is a typical scenario of a data sensitive operation.

Ok, so how do we handle these changes?

.NET 4.0 came up with an excellent way to deal with these types of changes. With .NET 4.0, a new type of generic collection, ObservableCollection, was introduced. This collection is a change-aware collection. So, whenever any change happens, this collection can report it.

The ObservableCollection<T> class implements two interfaces:

  1. INotifyCollectionChanged

  2. INotifyPropertyChanged

Both of these interfaces are very simple. Both of them only have one event. The INotifyCollectionChanged interface has an event called CollectionChanged, which is triggered when there is an active/statistical change. The INotifyPropertyChanged interface has an event, PropertyChanged, which can be triggered for any data sensitive change related to the Windows Presentation Foundation. I have decided to keep this interface out of discussion for this chapter. We will deal only with the CollectionChanged event and observable and read-only collections.

In this chapter, we will focus on the CollectionChanged event handler.

The ObservableCollection<T> class has a method called Move(). This method lets us swap two elements in an ObservableCollection.

Enough theory, now let's try our hands at this. The ObservableCollection<T> class can be used to monitor and help decide when to manipulate data for a source collection.

My nephew is 5 years old and he is getting used to even numbers, odd numbers, and so on. He also likes to do something on the computer. I wanted to keep him busy on the computer, so I designed a simple console that will ask him simple questions involving even and odd numbers, and will also monitor his responses. Also, I wanted to keep a track of the questions he answers, and the time he took for the response, so that I can monitor his progress.

Time for action – creating a simple math question monitor

Follow the given steps:

  1. Create a Windows application and call it NumberGame.

  2. Put a label on the form which will display the questions. Call it lblQuestion.

  3. Put a textbox on the form where kids will type their answers. Call it txtAnswer.

  4. Put a button to submit the answer. Call it btnSubmit.

  5. Put a button to generate a new question. Call it btnNewGame.

  6. Put a label to show the current time. This will be used to measure how much time the kid took to answer the question.

  7. Arrange everything as shown in the following screenshot. Add a good background image. This is a screenshot of the running app. So at design time, you can use any text to initialize the labels:

    Time for action – creating a simple math question monitor
  8. Put a timer control on the form. Leave the name as Timer1. Make sure it is enabled and the interval of the timer is set to 1000.

What just happened?

At this point, you can run the application but nothing will happen as we are yet to glue the buttons to the code.

Time for action – creating the collections to hold questions

Now that we have the skeleton and GUI, we need collections to hold questions and answers:

  1. Add the following using statement in Form1.cs:

    using System.Collections.ObjectModel;

    This namespace is the sweet home of our dear ObservableCollection<T> class.

  2. Add the following code snippets to the class Form1.cs:

    ObservableCollection<int> numbers = newObservableCollection<int>();
    
    ObservableCollection<KeyValuePair<string, TimeSpan>>questions = newObservableCollection<KeyValuePair<string, TimeSpan>>();
    
    List<int> evenNumbersInRange;
    DateTime startingTime;
    DateTime endingTime;
    
    int start;
    int end;
    
    bool isAllRight = true;
    

What just happened?

The program is not yet ready to run the way we want it to. However, let's check out the rationale behind the declaration of these variables.

We want to ask the kid questions, as shown in the previous screenshot.

The first line of the preceding code snippet, ObservableCollection<int> numbers, will be used to capture the response given by the kid.

The second line of the preceding code snippet, ObservableCollection<KeyValuePair<string, TimeSpan>>questions, which is a collection of KeyValuePair<string, TimeSpan>, for storing the questions asked and the response time (time taken to answer the question) of the kid to answer each question.

Following are the variables we just introduced:

  • evenNumbersInRange will be used to store the even numbers in the range and will be used to validate the answer given by the kid.

  • startingTime will mark the time when the question appears.

  • endingTime will mark the time when the kid gets the question answered absolutely right.

  • start will mark the starting number of the range. It will change with every question.

  • end will mark the ending number of the range. It will change with every question.

  • isAllRight will be useful to determine whether the question is fully answered or not.

Time for action – attaching the event to monitor the collections

Follow the given steps:

  1. Add the following line in the Form1_Load:

    numbers.CollectionChanged += new System.Collections.Specialized.NotifyCollectionChangedEventHandler(numbers_CollectionChanged);
  2. Add the following line in the Form1_Load:

    questions.CollectionChanged +=new System.Collections.Specialized.NotifyCollectionChangedEventHandler(questions_CollectionChanged);
  3. Add the following lines in the Form1_Load:

    //Starting the new game
    NewGame();
    //Starting the timer
    timer1.Start();

What just happened?

We just created the skeleton to be able to run the program. All the placeholders are ready and we just need to fill them. Let's see how these are connected.

We want to monitor the responses given by the kid and we also want to store the questions and response time of the kid. The ObservableCollection<T> class implements the INotifyCollectionChanged interface. This interface has only one event:

eventNotifyCollectionChangedEventHandlerCollectionChanged;

This event gets triggered whenever the collection gets changed. When a new item gets added to the collection, or an existing item gets deleted, or a couple of existing items swap their locations in the collection, this event is triggered.

We want to monitor the kid's response. The kid's response will be stored in the collection numbers. So, whenever that collection gets changed, we need to know. Thus, the event handler numbers_CollectionChanged is attached to the CollectionChanged event of the collection numbers by the following code listing:

numbers.CollectionChanged += new System.Collections.Specialized.NotifyCollectionChangedEventHandler(numbers_CollectionChanged);

So, every time a new entry is added or deleted or existing items move, this event will be called.

We also want to store the questions asked to the kid and the response time taken by the kid to answer those questions. In order to do that, I have attached an event handler for the questions collection by declaring an event handler as follows:

questions.CollectionChanged += new System.Collections.Specialized.NotifyCollectionChangedEventHandler(questions_CollectionChanged);

So, whenever a new question is added, the questions_CollectionChanged event handler will be called.

The method NewGame( ) loads a new question and displays it on the screen. And as soon as the new question starts, we must start the timer.

Time for action – dealing with the change as it happens

Follow the given steps:

  1. Add the following code to deal with the change in the collection numbers:

    void numbers_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
    {
      isAllRight = true;
    
      if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add)
      {
        if ((int)e.NewItems[0] % 2 != 0)
        {
          isAllRight = false;
          MessageBox.Show(String.Format("OOPS! You picked {0} which is not an even number",(int)e.NewItems[0]));
        }
        if ((int)e.NewItems[0] % 2 == 0 && !evenNumbersInRange.Contains((int)e.NewItems[0]))
        {
          sAllRight = false;
          MessageBox.Show(String.Format(@"OOPS! You picked {0} which is an even number but not in the range", (int)e.NewItems[0]));
        }
      }
    }
  2. Add the following code to deal with the change in the questions collection:

    void questions_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
    {
      if (e.Action == 
      System.Collections.Specialized.NotifyCollectionChangedAction.Add)
      {
        KeyValuePair<string,TimeSpan> record = (KeyValuePair<string,TimeSpan>)
        e.NewItems[0];
        StreamWriter questionWriter = new StreamWriter("C:\questions.txt", true);
        questionWriter.WriteLine(record.Key + " Time Taken " + record.Value);
        questionWriter.Close();
      }
    }

What just happened?

The System.Collections.Specialized.NotifyCollectionChangedEventArgs namespace has an enum called Action and it's of type NotifyCollectionChangedAction, which has the values as shown in the following screenshot:

What just happened?

When a new item is added to the collection, the Action is internally set to Add. When an item is removed from the collection, the Action is set to Remove. When two existing items in the collection swap their location, Action is set to Move. When an existing item is replaced with another element, Replace is set as the Action. If all the elements in the collection are deleted, then Action is set to Reset.

When new items are added to the collection, these items are stored in the collection called NewItems, which is of type IList.

The first event handler, that is:

e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add

will be true whenever a new element is being added to the numbers collection.

The (int)e.NewItems[0]() method returns the last integer added, as the NewItems collection returns IList<object> typecasting it to int is necessary.

If the last element added to this collection—which is actually entered by the kid—is not an even number, then it gets reported. If, however, the kid selects a number that is even, but not in the given range, then that is also a wrong answer and the application must be able to catch that. That's what happens in these two if-blocks in the preceding event handler.

When the kid finally answers the question correctly, we want to log the question asked and the response time taken by the kid to answer the question. That's what happens in the second event handler.

Note that the ObservableCollection<T> class is similar to any another collection, as it inherits from the Collection<T> class, so we can monitor any collection of any type.

Actually, there are two overloaded constructors to create an observable copy of an existing collection as follows:

List<int>firstHundredNaturalNumbers = Enumerable.Range(1, 100).ToList();
ObservableCollection<int>observableFirstHundredNaturalNumbers = new ObservableCollection<int>(firstHundredNaturalNumbers);

In this case, observableFirstHundredNaturalNumbers is an observable copy of the firstHundredNaturalNumbers collection. So if you make changes to the firstHundredNaturalNumbers collection, it will not be monitored unless you have specific home-grown event handlers dedicated for that job. However, firstHundredNaturalNumbers is ObservableCollection and so you can use the CollectionChanged event to monitor any change that happens to this copy.

There is another overloaded version to create an observable copy from an IEnumerable<T> interface. This helps to form a bridge between the non-generic collection and generic collection as IEnumerable<T> implements IEnumerable.

So, using this overloaded constructor, we can create an observable copy of a non-generic collection, such as Array or ArrayList.

Time for action – dealing with the change as it happens

Follow the given step:

  1. Add these methods to Form1.cs:

    privatevoidNewGame()
    {
      start = newRandom().Next(100);
      end = start + newRandom().Next(20);
      lblQuestion.Text = "Write the even numbers between " + start.ToString() + " and " + end.ToString()+ "
     separated by comma like (1,2,3)";
    
      evenNumbersInRange = Enumerable.Range(start, end - start + 1).Where(c => c % 2 == 0).ToList();
      startingTime = DateTime.Now;
    }
    
    private voidbtnNewGame_Click(object sender, EventArgs e)
    {
      txtAnswer.Clear();
      NewGame();
      txtAnswer.Focus();
      timer1.Start();
    }

What just happened?

We need some code to start a new game by loading a new question with a new range. Every time we need to do the same thing. So, putting that logic inside a method makes a lot of sense. The method NewGame() does just that.

When the Next Question button is clicked, the answer field must be cleared out so that the kid can enter a new answer there. Also, the timer has to be restarted so that we can capture the new response time.

Time for action – putting it all together

Follow the given step:

  1. Add the following event handler to deal with the submission of the answer:

    private void btnSubmit_Click(object sender, EventArgs e)
    {
      numbers.Clear();
      string[] toks = txtAnswer.Text.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
    
      foreach (string t in toks)
      numbers.Add(Convert.ToInt16(t));
    
      if (isAllRight && numbers.Count == evenNumbersInRange.Count)
      {
        MessageBox.Show("Congratulations! You got it all right");
        timer1.Stop();
        endingTime = DateTime.Now;
        questions.Add(new KeyValuePair<string, TimeSpan>(lblQuestion.Text, 
        endingTime.Subtract(startingTime)));
      }
      if (isAllRight && numbers.Count != evenNumbersInRange.Count)
      {
        MessageBox.Show("You are partially right. There are one or more even numbers left in the range.");
      }
    }

What just happened?

When the kid clicks on the Submit button, the program evaluates the answer. Only if it is absolutely correctly answered, the timer is stopped. At this point, the question gets added to the log along with the time taken to answer the question.

The call:

questions.Add(newKeyValuePair<string, TimeSpan>(lblQuestion.Text, endingTime.Subtract(startingTime)));

will in turn call the question_CollectionChanged event handler, where the question and the time the kid took to answer the question correctly is logged in a text file.

Later, teachers/parents can review this log to see how the kid is progressing. A large response time for an easy question will be a bad indication.

The following screenshot shows a few entries that are logged in the questions log file:

What just happened?

Time for action – creating a Twitter browser

Twitter is a remarkable application. It is great at getting real-time data from several people in different parts of the world. Most of us follow a lot of folks on Twitter. So, whenever they add a new tweet, we get notified.

However, people figured out long ago that seeing all the tweets from the people they follow in a single space is probably more helpful than visiting each one's page. Some applications exist to cater to this special need. I find that most of them are full of features that I don't use.

Moreover, I wanted to create a very easy application that even my granny can use. All we have to do is to put together a list of Twitter user IDs that we want to follow, and the application will get their latest tweets. If we don't want to follow a certain person for now, we can just remove her/his Twitter user ID from our list.

I guess you've understood the problem. Let's get down to business.

Time for action – creating the interface

Follow the given steps:

  1. Create a new Windows application and call it MyTweetDeck.

  2. Add a tab control to the Form1.cs.

  3. Add two tab pages in the tab control.

  4. Change the Text property of the tab pages to Names and Tweet View, as shown in the next screenshot.

  5. Add a textbox to the first tab called Names. Change the Dock property of the textbox to Fill so that it fills the entire tab page. Leave its name as textBox1:

    Time for action – creating the interface

We are done with designing the basic UI. Oh! Change the Text of the Form1.cs to Tweet@Glance. How's that name? Well, I leave it up to you to decide.

Time for action – creating the TweetViewer user control design

We want to show the tweet on a control where the user's profile photo will be visible. The username will be just below the profile photo and the latest tweet will appear to the right. Here is a sample of what I mean. You can take a look at the control in action at http://sudipta.posterous.com/twitview-control-for-net.

Time for action – creating the TweetViewer user control design

This tweet is from Jackie Chan. Who knew he was scared of needles! Twitter is amazing!

In this section, we are going to create a control that will help us lay out the information as shown in the preceding screenshot:

  1. Create a new project of Windows control library type and call it TwitterControl.

  2. Change the name of the UserControl1.cs to TweetViewer.cs. The control will be named as TweetViewer.

  3. Add the following controls to the Designer. Arrange them as shown in the following image. Separate the real estates of the Designer into two segments using a splitter control. Call it splitter1.

    Time for action – creating the TweetViewer user control design

As shown in the preceding screenshot, the left part has four controls: a PictureBox called profilePhoto, a label called lblTwitterUserName, and a couple of buttons btnPrev and btnNext to move to the next and the previous tweet. The right part has a web browser. Make sure that the web browser is docked to fill the entire right part of the splitter. You can download the entire project from the website for this book. Add a context menu to this control as follows:

Time for action – creating the TweetViewer user control design

Also, attach it to the splitter control, as shown in the following screenshot:

Time for action – creating the TweetViewer user control design

So, whenever users right-click on the left side, they will see this context menu to move a particular tweet to the top, or directly navigate to the first or last tweet. Take a look at this feature in action at http://sudipta.posterous.com/contextmenu-on-twitview-to-move-to-the-first.

Time for action – gluing the TweetViewer control

Now that we have all the controls in place, let's write some code to expose a few properties so that this control can be filled with Twitter data:

  1. Add the following code in the TweetViewer.cs file:

    public partial class TweetViewer : UserControl
    {
      private int newTweetCount;
      private bool newTweetAvailable;
    
      public int NewTweetCount
      {
        get { return newTweetCount;}
        set { newTweetCount = value;}
      }
    
      private Label _tweeterUserName;
      public Label TweeterUserName
      {
        get { return this.lblTwitterUserName; }
        set { this.lblTwitterUserName = value; }
      }
    
      private Image _tweeterProfilePic;
    
      public Image TweeterProfilePic
      {
        get { return profilePhoto.Image;}
        set { profilePhoto.Image = value;}
      }
      private string _latestTweet;
    
      public string LatestTweet
      {
        get {return this.tweetBrowser.DocumentText;}
        set {this.tweetBrowser.DocumentText = value;}
      }
    
      private ObservableCollection<string> tweets;
    
      public ObservableCollection<string> Tweets
      {
        get { return tweets;}
        set { tweets = value;}
      }
    
      private Color _tweeterPicBackColor;
    
      public Color TweeterPicBackColor
      {
        get { return splitter1.BackColor;}
        set { splitter1.BackColor = value;}
      }
    
      private int currentTweetIndex;
    
      public int CurrentTweetIndex
      {
        get { return currentTweetIndex;}
        set { currentTweetIndex = value;}
      }
    
      private List<string> movedTweets;
      public TweetViewer()
      {
        movedTweets = new List<string>();
        Tweets = new ObservableCollection<string>();
        InitializeComponent();
      }
      private string Quote(string x)
      {
        return """ + x + """;
      }
      private void SetTweet(int currentTweetIndex)
      {
        tweetBrowser.DocumentText = @"<HTML> <HEAD>" + "<metahttp-equiv="Page-Enter" content="RevealTrans(Duration=0.5, Transition=23)" /> "
                             + "<LINK href="
                             + Quote("http://dbaron.org/style/forest")
                             + "rel="
                             + Quote("stylesheet")
                             + "type="
                             + Quote("text/css")
                             + "></HEAD>"
                             + @"<body>"
                             + Tweets[currentTweetIndex];
      }
      private void btnNext_Click(object sender, EventArgs e)
      {
        if (this.NewTweetCount > 0 && currentTweetIndex >= 
          this.NewTweetCount)
        {
          TweeterPicBackColor = Color.Black;                
        }
        if (currentTweetIndex < Tweets.Count - 1)
        currentTweetIndex++;
        else
        currentTweetIndex = 0;
        SetTweet(currentTweetIndex);
      }
      private void btnPrev_Click(object sender, EventArgs e)
      {
        if (currentTweetIndex >0)
        currentTweetIndex--;
        else
        currentTweetIndex = Tweets.Count - 1;
        SetTweet(currentTweetIndex);            
      }
    
      private void firstTweetToolStripMenuItem_Click(object sender, EventArgs e)
      {
        SetTweet(0);
      }
    
      private void lastTweetToolStripMenuItem_Click(object sender, EventArgs e)
      {
        SetTweet(Tweets.Count - 1);
      }
    
      private void moveThisToTopToolStripMenuItem_Click(object sender, EventArgs e)
      {
        Tweets.CollectionChanged += new System.Collections.Specialized.NotifyCollectionChangedEventHandler (Tweets_CollectionChanged);
        this.Tweets.Move(currentTweetIndex, 0);
        for (int k = 0; k < movedTweets.Count; k++)
        this.Tweets.RemoveAt(k);
                
        movedTweets.ForEach(movedTweet => this.Tweets.Insert(0, movedTweet));
      }
      void Tweets_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
      {
        if (e.Action == 
        System.Collections.Specialized.NotifyCollectionChangedAction.Move)
        {
          if(!movedTweets.Contains("[MOVED] " + Tweets[e.NewStartingIndex]))
          movedTweets.Add("[MOVED] " + Tweets[e.NewStartingIndex]);
        }
      }
    }

We will use NTwitter for getting the tweets of the users. You can get NTwitter at http://ntwitter.codeplex.com/.

Time for action – putting everything together

Now that we have every piece of code, let's put these things together:

  1. Add the following variables in the Form1.cs:

    TableLayoutPanel tableLayoutPanel1;
    intCurrentRow = 0;
    ObservableCollection<string>tweeterUserNames = newObservableCollection<string>();
    List<string> names = newList<string>();
    List<BackgroundWorker> twitterDemons = newList<BackgroundWorker>();
    List<TweetViewer>twitViews = newList<TweetViewer>();

What just happened?

The variables mentioned in the previous code are going to be used for the following purposes:

  • tableLayoutPanel1 will be used to lay out the TweetViewer controls on the form.

  • CurrentRow will be used to keep track of the row we are at, for the tableLayoutPanel1.

  • tweeterUserNames will be used to store Twitter user IDs of the people who we want to follow.

  • names will be used to store the names. This will be used closely with the tweeterUserNames collection in order to achieve the functionality.

  • twitterDemons is a list of BackgroundWorker that will help us get the latest Twitter details.

  • twitViews is a list of TweetView controls. This will be populated with the controls.

Time for action – dealing with the change in the list of names in the first tab

Follow the given step:

  1. Add the following event handler in Form1.cs:

    private void textBox1_Leave(object sender, EventArgs e)
    {
      string[] toks = textBox1.Text.Split(new char[]{ '
    ', '
    ' },
      StringSplitOptions.RemoveEmptyEntries);
      foreach (string tok in toks)
      if(!tweeterUserNames.Contains(tok))
      tweeterUserNames.Add(tok);
      List<string> copyNames = tweeterUserNames.ToList();
      foreach (string tok in copyNames)
      {
        if (!toks.Contains(tok))//The name is deleted
        {
          tweeterUserNames.Remove(tok);
        }
      }
    }

What just happened?

We want to monitor the changes being made in the list of Twitter user IDs in the first tab. So, whenever the control leaves this textbox, we want to see whether there is any change.

The first loop in the preceding code snippet checks whether any of the names are new in the current list. If yes, then it will try to add that name to the tweeterUserNames collection. The call to the Add() method of the collection by the statement tweeterUserNames.Add(tok) will raise the CollectionChanged event handler and the action is of type add.

However, we are also interested to know whether some names have been deleted from the list. In order to do that, we maintain a copy of the tweeterUserNames collection. This copy is needed because we can't modify a collection while iterating over it. So, if a name is found that exists in the tweeterUserNames collection but is not present in the current list in the first tab, then we know for sure that the user doesn't want to follow this name any further and that gets deleted from the tweeterUserNames collection as well.

Time for action – a few things to beware of at the form load

Follow the given step:

  1. Add the following event handler in Form1.cs:

    privatevoid Form1_Load(object sender, EventArgs e)
    {
      tweeterUserNames.CollectionChanged += newSystem.Collections.Specialized.NotifyCollectionChangedEventHandler(tweeterUserNames_CollectionChanged);
    }

What just happened?

The first time the form loads, we want to make sure that the CollectionChanged event handler is properly attached. So, whenever a name gets added to the collection tweeterUserNames or gets deleted from it, we can monitor it.

Time for action – things to do when names get added or deleted

Follow the given step:

  1. Add the following event handler in Form1.cs:

    void tweeterUserNames_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
    {
      if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add)
      {
        for (int i = e.NewStartingIndex; i < tweeterUserNames.Count; i++)
        {
          names.Add(tweeterUserNames[i]);
          TweetViewer twitView = new TweetViewer();
          twitView.TweeterUserName.Text = tweeterUserNames[i];
          twitView.Font = new Font("Arial", 10);
          if (tableLayoutPanel1 == null)
          {
            tableLayoutPanel1 = new TableLayoutPanel();
            tableLayoutPanel1.Dock = DockStyle.Fill;
            tableLayoutPanel1.AutoScroll = true;
            this.tabPage2.Controls.Add(tableLayoutPanel1);
            this.tableLayoutPanel1.Controls.Add(twitView, 0, CurrentRow);
            CurrentRow++;
            twitViews.Add(twitView);
          }
          else
          {
            this.tableLayoutPanel1.Controls.Add(twitView, 0, CurrentRow);
            CurrentRow++;
            twitViews.Add(twitView);
          }
          BackgroundWorker twitterDemon = new BackgroundWorker();
          twitterDemon.WorkerSupportsCancellation = true;
          twitterDemon.WorkerReportsProgress = true;
          twitterDemon.DoWork += new DoWorkEventHandler(twitterDemon_DoWork);
          twitterDemon.RunWorkerCompleted += newRunWorkerCompletedEventHandler(twitterDemon_RunWorkerCompleted);
          twitterDemons.Add(twitterDemon);
        }
    
        for (int i = twitterDemons.Count - 1; i < names.Count; i++)
        {
          twitterDemons[i].RunWorkerAsync(i.ToString() + "-"+ names[i]);
        }
      }
    
      if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Remove)
      {
        int deletedIndex = e.OldStartingIndex;
        tableLayoutPanel1.Controls.RemoveAt(deletedIndex);
      }
    }

What just happened?

When a name gets added to the list of Twitter user IDs that we want to follow, the tweeterUserNames_CollectionsChanged() method gets called and e.Action will be of type Add.

e.NewStartingIndex returns the index of the newly added item in the list. If the tableLayoutPanel1 doesn't already exist, we will have to create that and add a newly created TweetViewer control in that. However, if it already exists, then we just have to add the newly created TweetViewer control and increase the current row count of the table layout panel by unity.

Once this TweetViewer control is added, a new BackgroundWorker object has to be created to get the details for this particular Twitter user ID that just got added to the list.

After that we have to run the newly created BackgroundWorker with the argument. Every time a new name gets added, it increases the count of names by unity. Thus, this last loop in the first if block in the preceding code, will run only once, every time.

When an existing username gets deleted from the list of names in the first tab, this event will be raised and e.Action will be of type Remove. e.OldStartingIndex returns the index of the item that gets deleted. So, once we know which one to delete by e.OldStartingIndex, the code just deletes that particular row of the tableLayoutPanel1.

Time for action – sharing the load and creating a task for each BackgroundWorker

Follow the given step:

  1. Add the following event handler in Form1.cs:

    void twitterDemon_DoWork(object sender, DoWorkEventArgs e)
    {
      string[] tokens = e.Argument.ToString().Split('-'),
      int index = Convert.ToInt16(tokens[0]);
      string name = tokens[1];
      twitViews[index].TweeterPicBackColor = System.Drawing.Color.Black;
      twitViews[index].TweeterUserName.BackColor = Color.Black;
      twitViews[index].TweeterUserName.ForeColor = Color.White;
    
      Stream webStream = new WebClient().OpenRead(GetProfilePicImage(name));
      twitViews[index].TweeterProfilePic = Image.FromStream(webStream);
    
      if (twitViews[index].Tweets.Count == 0 || twitViews[index].Tweets.Count == 50)
      {
        twitViews[index].Tweets = new ObservableCollection<string>(new Twitter().GetUserTimeline(name).Select(status => status.Text));
      }
      else
      {
        IEnumerable<string> tweetsSofar = twitViews[index].Tweets;
        IEnumerable<string> newTweets = new Twitter().GetUserTimeline(name).Select(status => status.Text);
    
        twitViews[index].NewTweetCount = 
        newTweets.ToList().IndexOf(tweetsSofar.ElementAt(0));
        for (int l = 0; l < twitViews[index].NewTweetCount; l++)
        {
          twitViews[index].Tweets.Insert(l, newTweets.ElementAt(l));
        }
    
    
    
      }
      if (twitViews[index].NewTweetCount > 0)
      {
        twitViews[index].TweeterPicBackColor = Color.DarkSlateBlue;
      }
      twitViews[index].CurrentTweetIndex = 0;
      twitViews[index].LatestTweet
                      = @"<HTML> <HEAD><LINK href="
                      + ""http://dbaron.org/style/forest""
                      + "rel="
                      + ""stylesheet""
                      + "type="
                      + ""text/css""
                      + "></HEAD>"
                      + @"<body>"
                      + twitViews[index].Tweets[twitViews[index].CurrentTweetIndex];
    }

The code for the GetProfilePicImage() method is omitted. You can get it from the book website.

What just happened?

If you want to get the details for all the Twitter user IDs sequentially, then it's going to take forever and the operation might leave the app screen frozen. To avoid this situation, we must share the workload evenly among many threads. BackgroundWorker classes are very handy in these type of situations.

The GetUserTimeline() method of the NTwitter API fetches the last 19 tweets, by default, of the user whose screen name is passed as an argument.

We don't want to stare at this twitView list all the time to check whether some of the users whom we are monitoring have tweeted recently or not. So, the Tweets property of the TwitView control is an ObservableCollection. So, whenever some new tweets come in, we want to be able to identify the person who tweeted recently.

That's why the NewTweetCount property has been introduced. The following code initializes the NewTweetCount property to the number of new tweets the person at index has created in the last monitoring frequency (which I have set to 20 minutes):

twitViews[index].NewTweetCount = newTweets.ToList().IndexOf(tweetsSofar.ElementAt(0));

tweetsSofar is the list of tweets acquired since the last time. And newTweets is the list of new tweets that we gathered just now.

If there is a new tweet available, we want to make sure that we identify the person who tweeted. So, we are changing the back color to some other color as follows:

if (twitViews[index].NewTweetCount > 0)
{
    twitViews[index].TweeterPicBackColor = Color.DarkSlateBlue;
}
What just happened?

So that we can easily identify the person who has tweeted recently, if we click on the next or previous button, we can go to the next or the previous tweet. As long as there is a new tweet available apart from those we have already seen in the past scan, the color will stay as DarkSlateBlue, as shown in the previous screenshot.

You can take a look at this behavior at http://sudipta.posterous.com/twitview-control-for-net-changing-backcolor-w.

Now that we have everything in place, let's query every now and then to check whether anything has changed. To do that, we need to add one timer to Form1 and add the following event handler:

private void timer1_Tick(object sender, EventArgs e)
{
  try
  {
    for (int i = twitterDemons.Count - 1; i >= 0; i--)
    {
      if (!twitterDemons[i].IsBusy)
      {
        twitterDemons[i].RunWorkerAsync(i.ToString() + "-" + names[i]);
      }
    }
  }
  catch
  {
    return;
  }
}

Time for action – a sample run of the application

Follow the given steps:

  1. Add the following Twitter user IDs in the first tab of the application:

    Time for action – a sample run of the application
  2. Click on the Tweet View tab. Within a few seconds, you will see something similar to the following:

    Time for action – a sample run of the application

You can take a look at this application in action at http://sudipta.posterous.com/ twitglance-demo-using-a-list-of-twitview-cont.

Have a go hero – archive tweets

The application offers a functionality to move a particular tweet of any person to the top. This way we will ensure that a tweet stays in our visibility for a long time because the last tweet gets deleted when new ones show up.

Remember that we added a context menu strip to directly go to the first and last tweet and to mark a tweet as "Moved" so that it can stay there for a longer period. The application stores only the last 19 tweets of the individual. After downloading the app from the website, try checking this functionality.

Can you explain how it works? Moreover, can you tweak it to monitor tweets from different people containing any of the keywords from a given list of keywords. Think of it as an exercise to monitor some Twitter trend.

Have a go hero – monitoring weather/stock

Using a similar structure, can you write a program to monitor weather at different locations? You can create a similar control to show a picture of the weather. You can download the startup code from the website for this book—search for WeatherDeck in the chapter's source.

You can use the Anima Online Weather API. It provides a nice wrapper against the Google weather API. You can download the API from http://awapi.codeplex.com/.

I have created a control similar to TweetViewer for weather monitoring called WeatherBlock. You can get it from the website for this book. With this control and the previously mentioned API, you can get and set weather conditions for any location as follows:

private void Form1_Load(object sender, EventArgs e)
{
  WeatherBlock wblock = new WeatherBlock();
  string place = "NYC";
  GoogleWeatherData nycWeather = GoogleWeatherAPI.GetWeather(Animaonline.Globals.LanguageCode.en_US,place);

  WebClient client = new WebClient();
  client.DownloadFile("http://www.google.com/" + nycWeather.CurrentConditions.Icon, "Temp.png");

  wblock.Location = place;
  wblock.ConditionTempCel = nycWeather.CurrentConditions.Temperature.Celsius.ToString() + "C";
  wblock.ConditionTempFer = nycWeather.CurrentConditions.Temperature.Fahrenheit.ToString()+ "F";
  wblock.WeatherImage = Image.FromFile("Temp.png");
  wblock.WeatherText.Text = nycWeather.CurrentConditions.Condition;
  wblock.WeatherHumidity = nycWeather.CurrentConditions.Humidity;
  wblock.WeatherWind = nycWeather.CurrentConditions.WindCondition;
  wblock.BackColor = Color.Black;
  this.Controls.Add(wblock);
}

It generated the following output. It shows the weather conditions in NYC (New York City, U.S.):

Have a go hero – monitoring weather/stock

Summary

In this chapter, we have learned how to deal with some physical changes of elements being added in Generic collections. In the next chapter, we will learn about several concurrent collections to see how to deal with concurrency at the data structure level.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.17.174.156