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

Code Project Forums : New Posts Monitor

, 20 Sep 2010 CPOL
Rate this:
Please Sign up or sign in to vote.
This application monitors for new posts in the Code Project forums.

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 Smile | :)

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 Smile | :)

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)

Share

About the Author

Nish Sivakumar

United States United States
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.

Comments and Discussions

 
GeneralConcept is good PinmemberVenkatesh Mookkan18-Oct-10 16:34 

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 | Terms of Use | Mobile
Web03 | 2.8.141030.1 | Last Updated 20 Sep 2010
Article Copyright 2010 by Nish Sivakumar
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid