Click here to Skip to main content
15,867,330 members
Articles / Desktop Programming / WPF

Code Project Forums : New Posts Monitor

Rate me:
Please Sign up or sign in to vote.
4.90/5 (43 votes)
20 Sep 2010CPOL5 min read 675.2K   601   28   69
This application monitors for new posts in the Code Project forums.

Image 1

Fig. 1 - Main window

Image 2

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).

C#
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.

C#
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.

C#
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.

C#
    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)


Written By
United States United States
Nish Nishant is a technology enthusiast from Columbus, Ohio. He has over 20 years of software industry experience in various roles including Chief Technology Officer, Senior Solution Architect, Lead Software Architect, Principal Software Engineer, and Engineering/Architecture Team Leader. Nish is a 14-time recipient of the Microsoft Visual C++ MVP Award.

Nish authored C++/CLI in Action for Manning Publications in 2005, and co-authored Extending MFC Applications with the .NET Framework for Addison Wesley in 2003. In addition, he has over 140 published technology articles on CodeProject.com and another 250+ blog articles on his WordPress blog. Nish is experienced in technology leadership, solution architecture, software architecture, cloud development (AWS and Azure), REST services, software engineering best practices, CI/CD, mentoring, and directing all stages of software development.

Nish's Technology Blog : voidnish.wordpress.com

Comments and Discussions

 
GeneralRe: This is probably the uglies interface I've seen since the beginning of the new millenium Pin
Nish Nishant22-Sep-10 4:31
sitebuilderNish Nishant22-Sep-10 4:31 
GeneralRe: This is probably the uglies interface I've seen since the beginning of the new millenium Pin
Xmen Real 26-Sep-10 2:26
professional Xmen Real 26-Sep-10 2:26 
GeneralRe: This is probably the uglies interface I've seen since the beginning of the new millenium Pin
Nish Nishant26-Sep-10 5:12
sitebuilderNish Nishant26-Sep-10 5:12 
GeneralMy vote of 5 Pin
Praveen Nair (NinethSense)22-Sep-10 3:46
Praveen Nair (NinethSense)22-Sep-10 3:46 
GeneralRe: My vote of 5 Pin
Nish Nishant22-Sep-10 4:31
sitebuilderNish Nishant22-Sep-10 4:31 
QuestionA naive question Pin
bleedingfingers22-Sep-10 1:08
bleedingfingers22-Sep-10 1:08 
AnswerRe: A naive question Pin
Nish Nishant22-Sep-10 1:20
sitebuilderNish Nishant22-Sep-10 1:20 
GeneralRe: A naive question Pin
bleedingfingers23-Sep-10 3:35
bleedingfingers23-Sep-10 3:35 
Nishant Sivakumar wrote:
Unfortunately, there is no way to do that at the moment. So all it will show is that a specific forum (or forums) has X number of new posts. Invariably these new posts will be on the front page (though it is possible that the last post or posts were made in an old thread 10 pages deep).


Is that because you would have to dig into the database or something and don't have such information readily at hand? Would it be really that hard for you (guys) to get it working coz THAT would make this utility truly indispensable.

I ask this because I had to reply to one of the CWG threads in Indian Forum but that had moved to 10th page within 24hrs! I don't know if my response will be acknowledged readily. Also, providing what I request would make every thread always alive, in that, (interesting) discussions and even flame wars can go on for an extended period of time than what is currently possible through manual page surfing. On the flip side, not having it might help CP databases to not choke Big Grin | :-D
...byte till it megahertz...

GeneralRe: A naive question Pin
Nish Nishant23-Sep-10 8:59
sitebuilderNish Nishant23-Sep-10 8:59 
GeneralRe: A naive question Pin
Nish Nishant23-Sep-10 9:00
sitebuilderNish Nishant23-Sep-10 9:00 
GeneralRe: A naive question Pin
bleedingfingers23-Sep-10 23:09
bleedingfingers23-Sep-10 23:09 
GeneralMy vote of 5 Pin
Mustafa Ismail Mustafa21-Sep-10 23:07
Mustafa Ismail Mustafa21-Sep-10 23:07 
GeneralRe: My vote of 5 Pin
Nish Nishant22-Sep-10 1:21
sitebuilderNish Nishant22-Sep-10 1:21 
GeneralMy vote of 5 Pin
Mustafa Ismail Mustafa21-Sep-10 23:07
Mustafa Ismail Mustafa21-Sep-10 23:07 
GeneralRe: My vote of 5 Pin
Nish Nishant22-Sep-10 1:21
sitebuilderNish Nishant22-Sep-10 1:21 
GeneralMy vote of 5 Pin
thatraja21-Sep-10 20:10
professionalthatraja21-Sep-10 20:10 
GeneralRe: My vote of 5 Pin
Nish Nishant22-Sep-10 1:21
sitebuilderNish Nishant22-Sep-10 1:21 
GeneralMy vote of 5 Pin
Abhinav S21-Sep-10 20:01
Abhinav S21-Sep-10 20:01 
GeneralRe: My vote of 5 Pin
Nish Nishant22-Sep-10 1:20
sitebuilderNish Nishant22-Sep-10 1:20 
GeneralMy vote of 5 Pin
Sandesh M Patil20-Sep-10 22:57
Sandesh M Patil20-Sep-10 22:57 
GeneralRe: My vote of 5 Pin
Nish Nishant21-Sep-10 2:06
sitebuilderNish Nishant21-Sep-10 2:06 
GeneralMy vote of 5 Pin
mk1488220-Sep-10 20:33
mk1488220-Sep-10 20:33 
GeneralRe: My vote of 5 Pin
Nish Nishant21-Sep-10 2:05
sitebuilderNish Nishant21-Sep-10 2:05 
GeneralRe: My vote of 5 Pin
mk1488221-Sep-10 2:27
mk1488221-Sep-10 2:27 
GeneralMy vote of 5 Pin
Brij20-Sep-10 19:45
mentorBrij20-Sep-10 19:45 

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

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