Code Project 论坛:新帖子监视器






4.90/5 (43投票s)
此应用程序可监控 Code Project 论坛的新帖子。
引言
我编写此应用程序是因为我想在特定论坛出现新帖子时收到通知。特别是,我关注的是 General Indian Topics 论坛,因为 Chris 要求我对其进行管理,并将其发展成一个面向新手印度 CP 用户的入门论坛。我发现我常常错过新帖子,或者当有人回复别人(因此我没有收到电子邮件)时。显然,每隔 3-4 分钟刷新一次页面会变得有些乏味。这时我才想到,写一个小应用程序,能够抓取该论坛的 HTML 并比较两次抓取之间的论坛计数,会更简单。当我开始着手时,我想到可以为所有论坛都这样做,而最简单的方法就是抓取主论坛列表的 HTML。
该应用程序是使用 VS 2010/.NET 4.0 编写的,因此您需要 .NET 4.0 运行时。我没有使用任何 .NET 4.0 的特性,所以理论上如果您用目标框架重新构建项目,或者降级到 VS 2008,可以在 3.5 上运行它。由于这是一个非常简单的项目,这应该很容易做到。该项目使用了 CodePlex 的 HTML Agility Pack v1.4.0,所以您也需要它。我已将所需的 DLL 包含在文章下载中,应该足够了。这是 HTML Agility Pack 的 URL,包括源代码下载和文档。
使用应用程序
该应用程序的用户界面相当简单。运行时,频率滑块默认设置为 30 秒。这意味着应用程序每 30 秒抓取一次 HTML。除非您期望实时回复,否则您可能想将其降低到 180 秒左右。您可以设置的最大值为 300 秒(5 分钟)。我认为,如果超过 5 分钟,您可能就不需要这个应用程序了。任何 5 分钟的时间段内,CP 几乎肯定都会有新帖子(因为它有来自世界各地的活跃用户,因此不同的时区)。有一个按钮可以执行手动抓取。但这不会影响正在运行的计时器。如果您要起身去拿咖啡,您可以手动抓取一次,这样当您回来时,您看到的就是您不在期间发布的内容。我个人更常用于调试,而不是其他任何事情。无论如何,每当一个论坛有新帖子时,它就会显示出来(见上文的图 1)。如果您将其设置为 30 秒,您会经常发现没有新帖子(在任何论坛中)。是的,即使是 CP 也不至于那么受欢迎 :-)
请记住,论坛计数是基于增量值的。所以如果您将其设置为每 1 分钟运行一次,而您休息了 3 分钟,您只会看到最近 1 分钟内的新帖子。您在返回时,之前 2 分钟内发布的任何新帖子都会在屏幕上闪过并消失。有时您实际上想知道错过了什么。这就是历史窗口的作用——见上文的图 2。它会显示触发的最近 500 次更新。
请注意,我尝试使用绿色和橙色来设计 UI,这是我刻意且费力地完成的,以赋予应用程序 CP 风格的主题。如果您不喜欢,那么我只能说您的视力很差,而且完全没有视觉品味。真是可怜!*笑*
实现细节
和所有在 2010 年编写 Windows 应用程序的人一样,我也使用了 WPF/MVVM。事实上,如果有人告诉我如今不能编写非 WPF 应用程序,我会非常震惊。好吧,我只是开玩笑 :-)
有一个 CodeProjectForum
类,代表一个论坛(以及它的增量计数)。
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
};
}
}
这是抓取 HTML、解析它并提取论坛详细信息的类。它使用后台工作线程来抓取和解析 HTML,并在完成后触发 FetchCompleted
事件(由 View Model 类在 UI 中处理)。HTML 解析并不复杂,并且通过使用 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(
"https://codeproject.org.cn/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()) });
}
}
}
主窗口和历史窗口都使用 ListBox
来显示论坛详细信息。它们只是样式差异很大。只有一个 View Model,对于历史窗口,我直接使用 ObservableCollection<>
作为它的 DataContext
。这里有一些 View Model 类的片段。您可以从附件的文章下载中浏览完整的源代码。
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();
}
下面的代码从 ShowHistory
方法开始,您可以在其中看到,如果 HistoryWindow
尚未显示,是如何创建它的,以及如何将其 DataContext
直接设置为之前添加的论坛对象集合。
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();
}
}
}
就是这样!一如既往,请随时发表您的评论和反馈,即使是很小的意见。也请随时要求添加任何功能,虽然我不能保证会实现它们,或者如果实现,也无法保证在特定的时间段内。
历史
- 2010/9/20 - 文章发布于 The Code Project。