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

Code Project Forums : New Posts Monitor

By , 20 Sep 2010
 

Fig. 1 - Main window

Fig. 2 - History window

Introduction

I wrote this application because I wanted to get notified whenever there was a new post in specific forums. In particular I was focusing on the General Indian Topics forum, because Chris had asked me to moderate it and help develop it into an orientation forum for newbie Indian CPians. I found that I missed out on new threads or when someone replied to someone else (and thus I didn't get an email). And obviously it became a little tedious to keep refreshing the page every 3-4 minutes. That's when I thought it'd be simpler to write a small app that would pull the HTML for that forum and compare the forum count with the previous pull. And as I started on it I thought I'd just do it for all forums, and the easiest way to do this was by pulling the HTML for the main forum listing.

A word of warning: The application fetches HTML from a specific URL. If the URL changes or if the HTML content undergoes a change, then the code that parses out the forum count will break. Until a time in future when CP offers a dependable web-service that will return information such as forum counts, the premise on which this application has been written will always be shaky. This is the same for similar applications written by folks like Luc Pattyn.

The application was written using VS 2010/.NET 4.0 so you'll need the .NET 4.0 runtime. I don't use any .NET 4.0 features so theoretically you can run it on 3.5 if you rebuild the project with that target framework or if you downgrade to VS 2008. It should be fairly trivial to do as it's a very simple project. The project uses the HTML Agility Pack v1.4.0 from CodePlex, so you'll need that too. I have included the required DLL in the article download, so that should suffice. Here's the URL for the HTML Agility Pack, including source downloads and documentation.

Using the application

The application has a fairly simple UI. When you run it, the frequency slider will default to 30 seconds. What it means is that the app will fetch the HTML every 30 seconds. Unless you are expecting to reply live, you might want to lower that to 180 seconds or so. The largest setting you can set is 300 seconds (5 minutes). I reckoned that for anything above 5 minutes, you don't really need this app. CP's sure to have had new posts in most forums in any 5 minute span of time (since it's got active members from all over the world and thus different time zones). There is a button that will perform a manual fetch. This will not affect the running timer though. If you are getting up to go get some coffee, you could do a manual fetch and that way you know that when you come back, whatever you see is what's been posted in your absence. I've personally used it more for debugging really than for anything else. Anyway, whenever a forum has new posts, it'll show up (see Figure 1 above). If you set it to 30 seconds, you will often find that there have been no new posts (in any of the forums). Yeah, even CP's not popular enough for that :-)

Remember that the forum counts are based on delta values. So if you set it to run every 1 minute, and you take a 3 minute break, you'll only see the new posts in the last 1 minute. Any new posts  that came up in the previous 2 minutes will have flashed on screen and gone away by the time you return. Sometimes you would actually want to know what you missed. That's what the history window is for - see Figure 2 above. It'll show you the last 500 updates that triggered.

Note how I've tried to use greenish and orange colors for the UI, this was intentionally and painstakingly done to give the app a CP like theme. If you don't like it, then all I can say is that you have appalling eyesight and absolutely no visual taste at all. Sucks to be you! *grin*

Implementation details

Like everyone else who writes Windows apps in the year 2010, I too used WPF/MVVM. In fact I would be terribly shocked if anyone told me that you can write non-WPF apps these days. Alright, I am only kidding here :-)

There is a CodeProjectForum class that represents a forum (along with its delta count).

class CodeProjectForum : INotifyPropertyChanged
{
    private string name;

    public string Name
    {
        get
        {
            return this.name;
        }

        set
        {
            if (this.name != value)
            {
                this.name = value;
                FirePropertyChanged("Name");
            }
        }
    }

    private string description;

    public string Description
    {
        get
        {
            return this.description;
        }

        set
        {
            if (this.description != value)
            {
                this.description = value;
                FirePropertyChanged("Description");
            }
        }
    }

    private int postCount = -1;

    public int PostCount
    {
        get
        {
            return this.postCount;
        }

        set
        {
            this.PreviousPostCount = this.postCount == -1 ? value : this.postCount;
            this.postCount = value;
            FirePropertyChanged("PostCount");
            FirePropertyChanged("Delta");
        }
    }

    public int Delta
    {
        get
        {
            return this.PostCount - this.PreviousPostCount;
        }
    }

    private int previousPostCount;

    public int PreviousPostCount
    {
        get
        {
            return this.previousPostCount;
        }

        private set
        {
            if (this.previousPostCount != value)
            {
                this.previousPostCount = value;
                FirePropertyChanged("PreviousPostCount");
            }
        }        
    }

    private DateTime lastChecked;

    public DateTime LastChecked
    {
        get
        {
            return this.lastChecked;
        }

        set
        {
            if (this.lastChecked != value)
            {
                this.lastChecked = value;
                FirePropertyChanged("LastChecked");
            }
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    protected void FirePropertyChanged(string propertyName)
    {
        PropertyChangedEventHandler handler = PropertyChanged;

        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    public CodeProjectForum Clone()
    {
        return new CodeProjectForum()
        {
            Name = this.Name,
            Description = this.Description,
            PostCount = this.PostCount,
            PreviousPostCount  = this.PreviousPostCount,
            LastChecked = this.LastChecked
        };
    }
}

And here's the class that fetches the HTML, parses it, and extracts the forum details. It uses a background worker to fetch and parse the HTML, and when it's done doing that it fires the FetchCompleted event (handled in the UI by the View Model class). The HTML parsing is nothing major, and is also further simplified by using the HtmlAgilityPack.

class CodeProjectForumCountFetcher
{
    private List<CodeProjectForum> forums = new List<CodeProjectForum>();

    public event EventHandler<CodeProjectForumCountFetcherEventArgs> FetchCompleted;

    public void Fetch()
    {
        BackgroundWorker worker = new BackgroundWorker();
        worker.DoWork += Worker_DoWork;
        worker.RunWorkerCompleted += Worker_RunWorkerCompleted;
        worker.RunWorkerAsync();
    }

    private void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        this.FireFetchCompleted();
    }

    private void Worker_DoWork(object sender, DoWorkEventArgs e)
    {
        string html = GetHttpPage(
          "http://www.codeproject.com/script/Forums/List.aspx", 10000);

        HtmlDocument document = new HtmlDocument();
        document.LoadHtml(html);

        foreach (HtmlNode trNode in 
          document.DocumentNode.SelectNodes("//table[@id='ForumListTable']/tr"))
        {
            var tdNodes = trNode.SelectNodes("td");

            if (tdNodes.Count != 4)
                continue;

            var forumName = tdNodes[0].InnerText.Trim();
            if (forumName.ToLowerInvariant().StartsWith("forum"))
                continue;

            int forumCount = -1;
            Int32.TryParse(tdNodes[3].InnerText.Replace(",", String.Empty).Trim(), 
                                                        out forumCount);

            var forumDescription = tdNodes[1].InnerText.Replace(" ", String.Empty).Trim();

            var forumMatches = forums.Where(forum => forum.Name == forumName);
            if (forumMatches.Count() == 0)
            {
                forums.Add(new CodeProjectForum() { Name = forumName, 
                  Description = forumDescription, 
                  PostCount = forumCount, LastChecked = DateTime.Now });
            }
            else
            {
                forumMatches.First().PostCount = forumCount;
                forumMatches.First().LastChecked = DateTime.Now;
            }
        }
    }

    private string GetHttpPage(string url, int timeout)
    {
        var request = WebRequest.Create(new Uri(url, UriKind.Absolute));
        request.Timeout = timeout;
        using (var response = request.GetResponse())
        {
            using (var responseStream = response.GetResponseStream())
            {
                using (var reader = new StreamReader(responseStream))
                {
                    return reader.ReadToEnd();
                }
            }
        }
    }

    public void FireFetchCompleted()
    {
        if (this.FetchCompleted != null)
        {
            this.FetchCompleted(this, new CodeProjectForumCountFetcherEventArgs() 
              { FetchedForums = Array.AsReadOnly(forums.Select(f => f.Clone()).ToArray()) });
        }
    }
}

Both the main window and the history window use a ListBox to display the forum details. They are just styled very differently. There's only one View Model, for the history window I directly use an ObservableCollection<> as its DataContext. Here're some snippets from the View Model class. You can browse the complete source code from the attached article download.

internal class MainWindowViewModel : ViewModelBase
{
    // . . .
    
    public int FetchFrequency
    {
        get
        {
            return this.fetchFrequency;
        }

        set
        {
            if (fetchFrequency != value)
            {
                fetchFrequency = value;
                FirePropertyChanged("FetchFrequency");
            }
        }            
    }

    private string statusText;

    public string StatusText
    {
        get
        {
            return this.statusText;
        }

        set
        {
            if (statusText != value)
            {
                statusText = value;
                FirePropertyChanged("StatusText");
            }
        }
    }

    public MainWindowViewModel()
    {
        this.Forums = new ObservableCollection<CodeProjectForum>();
        this.PropertyChanged += MainWindowViewModel_PropertyChanged;

        fetcher.FetchCompleted += Fetcher_FetchCompleted;
        Fetch();

        timer.Tick += Timer_Tick;
        StartTimer();
    }

    private void StartTimer()
    {
        timer.Interval = new TimeSpan(0, 0, this.FetchFrequency);
        timer.Start();
        ResetStatusText();
    }

    private void ResetStatusText()
    {
        this.StatusText = String.Format("Timer running at {0} seconds.", 
                                        this.FetchFrequency);
    }

    void MainWindowViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        if (e.PropertyName == "FetchFrequency")
        {
            timer.Stop();
            StartTimer();
        }
    }

    private void Fetch()
    {
        this.StatusText = String.Format("Executing a fetch!");
        fetcher.Fetch();
    }

The below code starts with the ShowHistory method where you can see how the HistoryWindow is created if it's not already shown, and how its DataContext is directly set to the collection of forum objects that were previously added.

    private void ShowHistory()
    {
        if(historyWindow == null)
        {
            historyWindow = new HistoryWindow() 
              { Owner = App.Current.MainWindow, DataContext = this.forumHistory };
            historyWindow.Closed += (s, e) => { historyWindow = null; };
            historyWindow.Show();
        }
    }

    void Timer_Tick(object sender, EventArgs e)
    {
        Fetch();
    }

    private void UpdateForumsCollection(IEnumerable<CodeProjectForum> forums)
    {
        this.Forums.Clear();

        foreach (var item in forums)
        {
            this.Forums.Add(item);
            AddToHistory(item);
        }
    }

    private void AddToHistory(CodeProjectForum item)
    {
        if(forumHistory.Count > 499)
        {
            forumHistory.RemoveAt(499);
        }

        forumHistory.Insert(0, item);
    }

    void Fetcher_FetchCompleted(object sender, CodeProjectForumCountFetcherEventArgs e)
    {
        var updatedForums = e.FetchedForums.Where(
          f => f.Delta > 0).OrderByDescending(item => item.Delta);
        UpdateForumsCollection(updatedForums);
        ResetStatusText();

        if (updatedForums.Count() > 0)
        {
            SystemSounds.Exclamation.Play();
        }
    }
}

That's it! As always, please throw in your comments and feedback, even if it's something really minor. Also feel free to ask for any features although I can't promise to implement them or if I do, then to do it in a specific timeframe.

History

  • 9/20/2010 - Article published on The Code Project.

License

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

About the Author

Nish Sivakumar
United States United States
Member
Nish is a real nice guy who has been writing code since 1990 when he first got his hands on an 8088 with 640 KB RAM. Originally from sunny Trivandrum in India, he has been living in various places over the past few years and often thinks it’s time he settled down somewhere.
 
Nish has been a Microsoft Visual C++ MVP since October, 2002 - awfully nice of Microsoft, he thinks. He maintains an MVP tips and tricks web site - www.voidnish.com where you can find a consolidated list of his articles, writings and ideas on VC++, MFC, .NET and C++/CLI. Oh, and you might want to check out his blog on C++/CLI, MFC, .NET and a lot of other stuff - blog.voidnish.com.
 
Nish loves reading Science Fiction, P G Wodehouse and Agatha Christie, and also fancies himself to be a decent writer of sorts. He has authored a romantic comedy Summer Love and Some more Cricket as well as a programming book – Extending MFC applications with the .NET Framework.
 
Nish's latest book C++/CLI in Action published by Manning Publications is now available for purchase. You can read more about the book on his blog.
 
Despite his wife's attempts to get him into cooking, his best effort so far has been a badly done omelette. Some day, he hopes to be a good cook, and to cook a tasty dinner for his wife.

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
Hint: For improved responsiveness ensure Javascript is enabled and choose 'Normal' from the Layout dropdown and hit 'Update'.
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
GeneralMy vote of 5memberT1000_21 Apr '11 - 2:12 
GeneralMy vote of 5memberNagy Vilmos2 Nov '10 - 6:35 
GeneralRe: My vote of 5mvpNishant Sivakumar5 Nov '10 - 4:55 
GeneralConcept is goodmemberVenkatesh Mookkan18 Oct '10 - 16:34 
GeneralMy vote of 5memberHimanshuJoshi15 Oct '10 - 9:30 
GeneralRe: My vote of 5mvpNishant Sivakumar16 Oct '10 - 3:57 
GeneralUseful articlememberCIDev12 Oct '10 - 10:19 
GeneralMy vote of 5memberPetr Pechovic6 Oct '10 - 4:57 
GeneralRe: My vote of 5mvpNishant Sivakumar7 Oct '10 - 15:24 
GeneralMy vote of 1member aphazel 死神30 Sep '10 - 22:45 
AnswerRe: My vote of 1mvpNishant Sivakumar1 Oct '10 - 9:30 
GeneralRe: My vote of 1member aphazel 死神5 Jun '11 - 3:15 
GeneralAnother vote for "this UI is ugly"memberDmitri Nesteruk25 Sep '10 - 7:09 
GeneralRe: Another vote for "this UI is ugly"mvpNishant Sivakumar25 Sep '10 - 10:56 
GeneralRe: Another vote for "this UI is ugly"memberCIDev12 Oct '10 - 10:15 
GeneralUse of RSS Feedsmembersenguptaamlan23 Sep '10 - 1:11 
GeneralRe: Use of RSS FeedsmvpNishant Sivakumar23 Sep '10 - 1:23 
GeneralRe: Use of RSS Feedsmembersenguptaamlan23 Sep '10 - 1:38 
GeneralRe: Use of RSS FeedsmvpNishant Sivakumar23 Sep '10 - 1:38 
GeneralThis is probably the uglies interface I've seen since the beginning of the new milleniummemberSimon Dufour22 Sep '10 - 4:26 
GeneralRe: This is probably the uglies interface I've seen since the beginning of the new milleniummvpNishant Sivakumar22 Sep '10 - 4:31 
GeneralRe: This is probably the uglies interface I've seen since the beginning of the new milleniummemberXmen W.K.26 Sep '10 - 2:26 
GeneralRe: This is probably the uglies interface I've seen since the beginning of the new milleniummvpNishant Sivakumar26 Sep '10 - 5:12 
GeneralMy vote of 5memberNinethSense22 Sep '10 - 3:46 
GeneralRe: My vote of 5mvpNishant Sivakumar22 Sep '10 - 4:31 
QuestionA naive questionmemberbleedingfingers22 Sep '10 - 1:08 
AnswerRe: A naive questionmvpNishant Sivakumar22 Sep '10 - 1:20 
GeneralRe: A naive questionmemberbleedingfingers23 Sep '10 - 3:35 
GeneralRe: A naive questionmvpNishant Sivakumar23 Sep '10 - 8:59 
GeneralRe: A naive questionmvpNishant Sivakumar23 Sep '10 - 9:00 
GeneralRe: A naive questionmemberbleedingfingers23 Sep '10 - 23:09 
GeneralMy vote of 5memberMustafa Ismail Mustafa21 Sep '10 - 23:07 
GeneralRe: My vote of 5mvpNishant Sivakumar22 Sep '10 - 1:21 
GeneralMy vote of 5memberMustafa Ismail Mustafa21 Sep '10 - 23:07 
GeneralRe: My vote of 5mvpNishant Sivakumar22 Sep '10 - 1:21 
GeneralMy vote of 5memberthatraja21 Sep '10 - 20:10 
GeneralRe: My vote of 5mvpNishant Sivakumar22 Sep '10 - 1:21 
GeneralMy vote of 5memberAbhinav S21 Sep '10 - 20:01 
GeneralRe: My vote of 5mvpNishant Sivakumar22 Sep '10 - 1:20 
GeneralMy vote of 5memberS M P20 Sep '10 - 22:57 
GeneralRe: My vote of 5mvpNishant Sivakumar21 Sep '10 - 2:06 
GeneralMy vote of 5membermk1488220 Sep '10 - 20:33 
GeneralRe: My vote of 5mvpNishant Sivakumar21 Sep '10 - 2:05 
GeneralRe: My vote of 5membermk1488221 Sep '10 - 2:27 
GeneralMy vote of 5mentorBrij20 Sep '10 - 19:45 
GeneralRe: My vote of 5mvpNishant Sivakumar21 Sep '10 - 2:04 
GeneralHelpfulmemberHiren Solanki20 Sep '10 - 19:34 
GeneralRe: HelpfulmvpAbhishek Sur20 Sep '10 - 22:27 
GeneralRe: HelpfulmvpNishant Sivakumar21 Sep '10 - 2:04 
GeneralMy vote of 5mvpAbhijit Jana20 Sep '10 - 16:59 

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

Permalink | Advertise | Privacy | Mobile
Web01 | 2.6.130516.1 | Last Updated 20 Sep 2010
Article Copyright 2010 by Nish Sivakumar
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid