RSS Feed聚合器和博客智能客户端






4.91/5 (79投票s)
RSS Feed 聚合器和博客智能客户端,使用了 Enterprise Library、Updater Application Block、大量的 XML 技巧和桌面技巧。关于智能客户端开发的现实生活中的挑战的全面指南。
- 从 RSS Feeder 网站下载 RSS Feeder .NET ver 3.2 [SourceForge] - (4 MB) (更新于 2005 年 8 月 16 日)
- 下载源代码 - 0.99 MB (更新于 2005 年 8 月 16 日)
目录
- 引言
- 功能演练
- 智能客户端 – MSDN 定义和要求
- RSS Feeder 如何成为智能客户端
- 涵盖的主题
- Enterprise Library
- 连接性
- 使用 Updater Application Block 2.0 自动更新
- 决定是否用新文件覆盖旧文件
- 轻松使用 Enterprise Library
- RSS Feeder 对象模型
- RSS 2.0 XML 格式
- Atom 0.3 格式
- 日期处理
- 为 Atom、RDF 和 RSS Feed 创建通用解析器
- 将 Atom 0.3 转换为 RSS 2.0
- 介绍 EXSLT 项目
- XML 序列化/反序列化
- 嵌入式资源
- 将数据存储在应用程序数据文件夹中
- OPML
- 数据库优化
- MS Outlook® 集成
- 使用 HTTP 下载 Feed
- RSS 自动发现
- RSS Feed 的 XSL 转换
- 应用程序的单一实例
- 使用定时器制作响应式 UI
- 减少 .NET 应用程序的内存占用
- 注册应用程序在启动时运行
- 通知托盘图标
- 错误报告
- 博客功能
- UI
- 结论
引言
RSS Feeder.NET 是一个免费的开源桌面 RSS Feed 聚合器,它从 Web 源下载 Feed 并将其本地存储以供离线查看、搜索和处理。它也是一个丰富的博客工具,可用于博客 WordPress、B2Evolution、.Text、Community Server 等各种博客引擎。您可以完全依赖 MS Outlook®,也可以完全独立运行。您也可以同时使用两者,以您觉得舒适的方式工作。它不会增加 Outlook 的加载时间,也不会使 Outlook 变慢或阻止其正常关闭。它是一个智能客户端,可以充分利用本地资源和分布式 Web 信息源。
更新历史
- 2005 年 8 月 9 日 - 更新了源代码,Web Log Manager 进行了多项修复。
- 2005 年 8 月 8 日 - 当您首次加载项目并生成时,会在 *WebLogManager.resx* 中发现一个错误。解决方案是简单地加载窗体,做一些操作使其变脏,然后保存窗体。
特点
- 报纸模式。您可以以更具可读性的报纸模式阅读 Feed,称为“Blogpaper”。
- 自动发现。拖动任何超链接,我都会找出该页面中是否有任何 RSS Feed。
- Outlook 集成。您可以将 Feed 存储在 Outlook 文件夹中。
- 博客。它提供了一个 Outlook 2003 风格的便捷工作区来管理您的博客帐户并撰写丰富的帖子。
- 从 Outlook 博客。您可以为 Weblog 帐户指定一个 Outlook 文件夹。该文件夹中的所有帖子将在同步过程中自动发布到 Weblog。您可以使用 Word 编辑器以 HTML 格式撰写帖子。帖子内容(HTML 标记)在发布到 Weblog 之前会经过严格清理。
- Outlook 视图。它使用自定义视图在 Outlook 文件夹中呈现更易读的 Feed 列表。标准的帖子视图不容易快速浏览。该视图将主题放在首位并加粗,在主题下方显示帖子的摘录。
- 优化启动。您可以安全地将 RSS Feeder 设置为启动时运行,它不会使 Windows® 启动变慢。一个巧妙的延迟加载过程在启动时不会对 Windows® 造成任何负担,而是等到 Windows® 在漫长的启动过程后完全恢复精力时才启动应用程序。
- Newsgator 导入。Newsgator 用户可以使用 RSS Feeder 导入所有订阅,并无缝替换 Newsgator,而无需修改 Outlook 文件夹位置。
功能演练
让我们对 RSS Feeder .NET 的所有功能进行一次简短的演练
Outlook 风格视图
Outlook 的三窗格视图是一项用户界面工程的奇迹。它使信息浏览快速而轻松。在 RSS Feeder 中,我遵循了相同的概念。最左边的窗格是频道列表。单击频道时,您会在中间列表中看到 Feed 列表。您可以使用中间列表底部的搜索框即时搜索 Feed。单击 Feed 时,Feed 内容会显示在右侧的查看器中。
您可以直接从左下角的属性网格中编辑频道属性。单击标题“Properties”以显示属性网格。.NET 内置的属性网格非常方便地公开对象并允许用户方便地修改对象。我已经实现了属性网格的几个扩展,我将在后面进行解释。
每日 Blogpaper – 报纸风格阅读
博客最便捷的阅读视图是报纸风格视图,我称之为“The Daily Blogpapper”。您只需打开 Blogpaper,即可阅读从所有频道收集到的最新帖子(可选择排除),然后单击“Mark as Read”。就是这么简单。
之前的 Outlook 风格视图对于处理 Feed 和频道很有用。但它不是一个舒适的阅读环境。Blogpaper 为日常阅读提供了真正舒适的阅读环境。
丰富的博客功能
RSS Feeder 不仅仅是一个 RSS 聚合器;它也是一个功能同样强大的博客工具。
Outlook 2003 风格视图是使用您可以在 Divelements Ltd 找到的令人惊叹的免费 UI 组件创建的。在左侧,您可以看到 Weblog 帐户以及您在该帐户中的帖子。右侧是编辑环境。
RSS Feeder 目前支持 *MetaWebLogAPI* 支持的博客引擎,如 WordPress、B2Evolution、Drupal 等,以及一些基于 XML Web 服务的博客引擎,如 .Text 和 Community Server。我们将在后面详细介绍如何实现这些。
Outlook 集成
如果您不喜欢我的应用程序,您可以继续使用 MS Outlook。安装应用程序时,它会询问您是否需要 Outlook 集成。您只需指定一个包含所有子频道文件夹的基文件夹,RSS Feeder 就会将所有 Feed 提供给 Outlook。
Feed 列表的便捷视图使得快速浏览大量帖子非常容易。自动预览还提供了帖子内容的预览,无需打开每个帖子,我们就可以决定保留还是删除它。
从 Outlook 博客
这是我最喜欢的,您可以在 Outlook 文件夹中创建帖子,然后创建一个映射到该文件夹的 Weblog 帐户。该文件夹中的所有帖子都会发布到 Weblog。您可以从其他地方拖动帖子,也可以在该文件夹中撰写新帖子。每当 RSS Feeder 运行其周期性同步(每 5 分钟)时,它都会从文件夹读取帖子,然后将帖子发送到相应的博客站点。
信息丰富的进度条
RSS Feeder 的发送/接收窗口为您提供有关 Feed 同步的详细统计信息。您可以查看从服务器收到的错误,可以看到您的互联网连接速度以及发送到 Outlook 的 Feed 数量。
在发布博客时,它还会为您提供服务器生成的帖子 ID 或发布时收到的详细错误消息。
Newsgator 导入
RSS Feeder 将在首次运行时自动导入 Newsgator 设置,包括所有订阅和 Outlook 文件夹。这为您从 Newsgator 迁移到 RSS Feeder 提供了零成本。
智能客户端 – MSDN 定义和要求
要使一个应用程序符合智能客户端的条件,根据 MSDN 上对智能客户端的定义,该应用程序需要满足以下要求。
本地资源和用户体验
所有智能客户端应用程序都能够利用本地资源,如硬件进行存储、处理或数据捕获,例如 CF 存储卡、CPU 和扫描仪。智能客户端解决方案通过充分利用 Microsoft® Windows® 平台提供的所有功能,提供高保真度的最终用户体验。一些著名的智能客户端应用程序包括 Word、Excel、MS Money,甚至像 Half-Life 2 这样的 PC 游戏。与 Amazon.Com 或 eBay.com 等“基于浏览器”的应用程序不同,智能客户端应用程序安装在您的 PC、笔记本电脑、Tablet PC 或智能设备上。
已连接
智能客户端应用程序能够轻松连接到企业或互联网系统并与之交换数据。Web 服务允许智能客户端解决方案利用 XML、HTTP 和 SOAP 等行业标准协议与任何类型的远程系统交换信息。
离线可用
智能客户端应用程序无论是否连接到 Internet 都可以工作。Microsoft® Money 和 Microsoft® Outlook 是两个很好的例子。智能客户端可以利用本地缓存和处理,在没有网络连接或网络连接不稳定的情况下运行。离线功能不仅用于移动场景,也用于桌面解决方案,在这些解决方案中,它们可以利用离线架构在后台线程中更新后端系统,从而保持用户界面的响应性并改善整体最终用户体验。这种架构还可以提供成本和性能优势,因为用户界面不必从服务器传输到智能客户端。由于智能客户端可以在后台与系统交换所需的数据,因此可以减少与系统交换的数据量(即使在有线客户端系统中,这种带宽减少也能带来巨大好处)。这反过来提高了用户界面 (UI) 的响应性,因为 UI 不是由远程系统渲染的。
智能部署和更新
过去,传统的客户端应用程序很难部署和更新。安装一个应用程序然后发现它破坏了另一个应用程序是很常见的。诸如“DLL Hell”之类的问题使得安装和维护客户端应用程序变得困难且令人沮丧。patterns and practices 团队的 .NET Updater Application Block 为那些希望创建跨多个桌面部署的自更新 .NET Framework 应用程序的开发人员提供了指导。Visual Studio 2005 和 .NET Framework 2.0 的发布将随着 ClickOnce 这一新的部署和更新技术(用于部署和更新 .NET Framework 应用程序)的发布,开启一个简化的智能客户端部署和更新的新时代。
(以上文本摘录并缩短自 MSDN 网站。)
RSS Feeder 如何成为智能客户端
让我们来看看 RSS Feeder .NET 如何成为一个智能客户端应用程序
本地资源和用户体验
下载应用程序后,它会在本地运行,分别将所有 Feed 和您的个人博客存储在 MS Access 数据库和 XML 存储中。它还提供了一个像 Outlook 2003 一样的丰富用户界面供您使用。因此,您可以获得桌面便利性的全部好处,同时您处理的所有信息都来自某个 Web 源。
已连接
应用程序使用 HTTP 连接到 RSS Feed 源并将 Feed 下载到本地存储。它使用 XML RPC 与支持 XMLRPC 的博客引擎通信以发布 Weblog。一些著名的 XMLRPC 支持的博客引擎包括 WordPress、B2Evolution、Drupal 等。它还使用 Web Service 与 .Text 和 CommunityServer 等博客引擎通信。
离线可用
该应用程序完全支持离线使用。当它未连接时,您可以从已下载的本地存储中读取 Feed。但当它连接时,它会在后台自动下载最新的 Feed 并无缝更新视图。
智能部署和更新
该应用程序使用 Updater Application Block 2.0 提供自动更新功能。每当我发布新版本或将一些 bug 修复部署到中央服务器时,该应用程序的所有用户都会在后台自动获得更新。这可以节省每个用户每次发布新内容时都去网站下载新版本的时间。它还允许我非常快地向所有人传递 bug 修复。
错误报告
这是我个人对智能客户端的要求。由于智能客户端运行在远程计算机上,与网站不同,您不知道用户是否遇到任何问题,也不知道应用程序是否生成了任何异常。我们需要提供某种错误报告功能,该功能会自动捕获错误并将错误传输到某个中央服务器,以便可以修复该错误。您在 Windows XP 或 Office 2003 应用程序中已经看到了这个错误报告功能。每当发生错误时,它都会将错误报告发送给 Microsoft。同样,该应用程序也会在后台捕获错误并将异常跟踪发送到 Sourceforge 上的跟踪系统。
多线程
使客户端真正智能的另一个我喜欢的要求是使应用程序完全多线程。智能客户端应始终保持响应。它在下载或上传数据时绝不能卡住。用户将继续工作,而不知道后台正在进行一些大型操作。例如,当用户撰写博客时,应用程序应在后台下载 Feed,而不会妨碍用户的创作环境。
防崩溃
当智能客户端在用户面前崩溃并显示令人讨厌的“Continue”或“Quit”对话框时,它就变成了哑客户端。为了使您的应用程序真正智能,您需要捕获任何未处理的错误并安全地发布该错误。在此应用程序中,我将向您展示如何做到这一点。
涵盖的主题
RSS Feeder 是一个完整的应用程序,涉及 XML、XSLT、HTML、HTTP GET/POST、配置管理、加密、日志记录、自动更新、丰富 UI、RSS/RDF/ATOM Feed 处理、XML RPC、Web Service、博客、Outlook 自动化、多线程等等。详细涵盖所有这些需要一本书的篇幅。因此,在本文中,我将只介绍我在所有这些领域使用的技巧和窍门。到您阅读完本文时,您将 fully equipped 拥有制作丰富的已连接的智能客户端应用程序的实际经验,实施您在网上能找到的最佳实践和精妙技巧。
Enterprise Library
Enterprise Library 是 Microsoft patterns and practices 应用程序块的重大新版本。应用程序块是可重用软件组件,旨在协助开发人员应对常见的企业开发挑战。Enterprise Library 将最常用的应用程序块的新版本整合到一个单独的集成下载中。
Enterprise Library 的总体目标如下:
- 一致性:所有 Enterprise Library 应用程序块都具有一致的设计模式和实现方法。
- 可扩展性:所有应用程序块都包含定义好的可扩展点,允许开发人员通过添加自己的代码来定制应用程序块的行为。
- 易用性:Enterprise Library 提供了许多可用性改进,包括图形化配置工具、更简单的安装过程以及更清晰、更完整的文档和示例。
- 集成:Enterprise Library 应用程序块旨在协同工作,并经过测试以确保它们能够正常工作。也可以单独使用应用程序块(除非块相互依赖,例如 Configuration Application Block)。
应用程序块有助于解决开发人员在每个项目中面临的常见问题。它们旨在封装 Microsoft 推荐的 .NET 应用程序最佳实践。它们可以快速轻松地添加到 .NET 应用程序中。例如,Data Access Application Block 提供对 ADO.NET 最常用功能的访问,并通过易于使用的类来提高开发人员的生产力。它还解决了类库本身不直接支持的场景。(不同的应用程序有不同的需求,您会发现并非每个应用程序块都适用于您构建的每个应用程序。在使用应用程序块之前,您应该充分了解您的应用程序需求以及该应用程序块旨在解决的场景。)
RSS Feeder 使用以下应用程序块:
- Caching Application Block:缓存经常访问的数据,例如每次单击 RSS Feed 时用于将 RSS Feed 渲染为 HTML 的 XSLT 文件。
- Configuration Application Block。静态和动态配置都由此块处理。例如,全局配置,如是否需要 Outlook 集成、Feed 下载间隔等。
- Cryptography Application Block。您的 Weblog 帐户密码使用 TripleDES 算法进行加密。
- Exception Handling Application Block。整个应用程序使用异常处理块来处理所有已处理或未处理的异常。
- Logging and Instrumentation Application Block。通过此块将信息性、警告、调试级别日志记录以及错误日志记录发送到文本文件。
- Updater Application Block。提供从中央 Web 服务器自动更新。
连接性
RSS Feeder 有三种连接方式:
- 从更新服务器下载最新更新。
- 从 Feed 源下载 Feed。
- 发布到博客引擎。
使用 Updater Application Block 2.0 自动更新
Updater Application block 的实现确实很麻烦。在尝试了几周后,我终于放弃了它默认的 *BITSDownloader*,因为它在我的情况下根本不起作用。我开始使用 *HTTPDownloader*,它没有问题。当我使用 *BITSDownloader* 时,我总是收到一个错误,即响应头不包含 *Content-Length*。但如果我捕获流量,我可以看到 HTTP 响应头中确实存在 *Content-Length*。因此,我改用了 Kent Boogaart 为 Katersoft 创建的 *HTTPDowloader*。*HTTPDowloader* 只使用了内置的 *HttpWebRequest* 从服务器下载文件。它既可以同步运行也可以异步运行,适合所有更新场景。
更新程序的工作方式如下:
- 首先,它获取清单 XML 的 URL。准备一个清单并将其存储在服务器上,该清单描述了需要下载的文件。
- 下载清单后,它首先根据清单找出需要下载的文件。然后开始下载它们。
- 下载完成后,它会生成一个控制台应用程序,该应用程序会等待 RSS Feeder 关闭。
- 当 RSS Feeder 关闭时,控制台应用程序执行更新并退出。
下载清单时遇到的一大问题是代理会缓存清单。通常,您的互联网服务提供商会使用代理来缓存 Web 内容。由于 XML 是 Web 内容,代理服务器现在会缓存 XML 文件。结果,即使我频繁更新服务器上的清单 XML,代理也不会相应地更新它们的缓存,而是返回旧版本的清单文件。这会阻止自动更新。
为了解决这个问题,我在清单 URI 的末尾注入了一个滴答计数。这始终会产生一个唯一的 URL,从而防止代理返回旧内容。
/// Get the default configuration
// provider for updater block
Microsoft.ApplicationBlocks.Updater.
Configuration.UpdaterConfigurationView view =
new Microsoft.ApplicationBlocks.Updater.
Configuration.UpdaterConfigurationView();
// Get the default manifest uri in order to add a unique
// timestamp at its end
// in order to avoid manifest file caching at proxies
Uri manifestUri = view.DefaultManifestUriLocation;
string uri = manifestUri.ToString();
uri += "?" + DateTime.Now.Ticks.ToString();
manifests = updater.CheckForUpdates(new Uri( uri ));
决定是否用新文件覆盖旧文件
当您在应用程序中实现自动更新时,您会遇到一个困境:是用新文件覆盖旧文件,还是不覆盖?例如,我的 RSS Feeder 有几个 XSLT 文件,用于将 RSS Feed 渲染为 HTML。现在,用户可以自由更改这些文件。所以,如果我发布一个新版本的文件,我不能在不知道用户是否更改了它们的情况下覆盖这些文件。但我需要更新它,因为我可能在某些文件中修复了一些问题。现在您可能会想,嗯!这是一个简单的解决方案。只需检查文件日期是否与 EXE 的日期相等。如果相等,则用户未更改文件。这并不总是正确的,因为我经常发布更新的 EXE。所以,我不能与 EXE 的日期进行比较。唯一可以确保我们不会意外覆盖文件的方法是保存每个文件的 MD5 哈希,并在复制之前确保哈希是否匹配。我不确定 Updater Application block 是否内置了此功能,但如果该功能可以自动检查 MD5 是否匹配,然后再覆盖文件,那将是很好的。
轻松使用 Enterprise Library
Enterprise Library 是一个伟大的作品。它确实可以为您开发的任何类型的应用程序节省大量的框架开发负担。通常,当我们开始开发一个新项目时,我们从一个提供配置管理、安全、加密、数据库访问等的框架开始。所有这些都需要时间来集成和测试。Enterprise Library 使您无需开发此类框架,因为它提供了从成功的项目多年的经验中收集到的最佳实践。
但是,直接从应用程序的所有层使用 Enterprise Library 类存在问题。很快,您就会需要一个包装类。另外,由于它是一个通用库,您首先需要对其进行一些定制才能开始使用。这里我将介绍一些额外的调整,您可以使用它们来开始使用 Enterprise Library。
让我们来看一个包含 Caching、Cryptography、Exception 和 Logging 应用程序块的典型配置文件。
现在看圈出的部分。这些是使用 EL 类时需要记住的事情。例如,如果要使用日志记录块,则必须提供 Category 名称。
Logger.Write( "Log message", "Debug" );
同样,如果要使用 Cache Manager,您必须记住名称。
CacheManager manager = CacheFactory.GetCacheManager("XSL Cache");
如果要使用 Security Block 类,则需要指定安全提供程序名称,例如 *MD5CryptoServiceProvider*。
Cryptographer.CreateHash( "MD5CryptoServiceProvider", “hash me” );
因此,很快,您的代码就会充满了 EL 类,当任何类的签名发生变化或发布新版本时(例如,从 Pattern and Practices Application Blocks 到新的 Enterprise Library),您都需要在整个项目中执行搜索和替换来升级您的代码。这确实很痛苦。所以,我在这里所做的是,我创建了一个名为 *EntLibHelper* 的 Enterprise Library 助手类,您可以使用它,如下所示。
EntLibHelper.Info( “Information loggin”); // Log block
//Caching block
string hashedValue = EntLibHelper.Hash( “Hash me” );
// Exception block
EntLibHelper.Exception( “Some error occurred”, x );
因此,下次,当 Microsoft 发布名为 Universal Library 的产品时,您只需要更改 *EntLibHelper* 内部的代码。此外,*EntLibHelper* 负责处理所有 Enterprise Library 块的初始化问题,确保正确使用,并提供一个方便的接口,让开发人员无需记住 EL 接口。
请查看 *EntLibHelper* 的源代码。请记住,它与 RSSFeeder 的 *app.config* 兼容。如果您更改了任何名称,例如,重命名 Logging Category 或将 MD5 哈希更改为 SHA 哈希,您都需要更改类顶部常量中指定的名称。
private const string EXCEPTION_POLICY_NAME = "General Policy";
private const string SYMMETRIC_INSTANCE = "DPAPI";
private const string HASH_INSTANCE = "MD5CryptoServiceProvider";
public const string XSL_CACHE_MANAGER = "XSL Cache";
RSS Feeder 对象模型
RSS Feeder 有一个微小的对象模型,如下所示。
Channels 是 *Channel* 的集合,它代表一个 RSS Feed 源。*Channel* 是 *RSSFeed* 对象的集合。*RSSFeed* 对象包含有关帖子的基本信息,如标题、发布日期和唯一标识特定项的 GUID。每个项的整个 XML 都存储在 XML 字段中。
其中一些项是从原始 XML 中提取到 RSS 对象中以加快访问速度。例如,当我们在 *Listview* 中渲染 Feed 列表时,我们需要显示 *title* 和发布日期。此外,我们还需要按发布日期排序。这就是为什么一些字段在从实际 XML 中提取的 *RSSFeed* 对象中重复的原因。
通常,RSS 和 Atom XML 都包含帖子的标题、作者姓名、唯一 ID、帖子链接、发布日期和详细正文。
RSS 2.0 XML 格式
让我们看一下 RSS 2.0 格式的示例 XML。
<?xml version="1.0"?>
<rss version="2.0" xmlns:dc=http://purl.org/dc/elements/1.1/
xmlns:admin=http://webns.net/mvcb/
xmlns:rdf=http://www.w3.org/1999/02/22-rdf-syntax-ns#
xmlns:content="http://purl.org/rss/1.0/modules/content/">
<channel>
<title>.NET Community Blog</title>
<link>https:///b2evolution/index.php</link>
<description>.NET Community Blog</description>
<language>en-US</language>
<docs>http://backend.userland.com/rss</docs>
<ttl>60</ttl>
<item>
<title>Important information</title>
<link>https:///b2evolution/index.php?...</link>
<pubDate>Fri, 17 Jun 2005 10:05:52 +0000</pubDate>
<category domain="external">Announcements [A]</category>
<category domain="alt">Announcements </category>
<category domain="main">b2evolution Tips</category>
<guid isPermaLink="false">21@https:///b2evolution</guid>
<description>Blog B contains a few posts in the
'b2evolution Tips' category. </description>
<content:encoded>
<![CDATA[ <p>Blog B contains a few posts in the 'b2evolution Tips'
category.</p>]]>
</content:encoded>
<comments>https:///b2evolution/...</comments>
</item>
根节点是 *<rss>*,它包含一个或多个 *<channel>* 节点。*<item>* 节点代表一个帖子。帖子正文可在 *<description>* 节点中找到。一些 RSS Feed 生成器在 *<description>* 中写入 HTML 和纯文本内容。但是,一些高级生成器在 *<description>* 中写入纯文本版本,而在 *<content:encoded>* 节点中写入所有格式的实际 HTML 版本。
唯一标识一个项很麻烦,因为并非所有站点都生成 *guid* 节点。例如,CodeProject RSS Feed 不包含 *guid* 节点。因此,您唯一标识 Feed 的唯一选择是生成整个内容的 MD5/SHA 哈希并使用该哈希值作为标识符,或者使用 *link* 节点。RSS Feeder 首先检查是否存在任何 *guid* 节点,如果没有,则使用 *link* 节点作为唯一标识符。(致 CodeProject:如果这不正确,请告知我。)
并非所有站点都遵循 RSS 2.0 格式。一些站点仍在使用 RSS 0.9 格式。即使是那些遵循的站点,也并不总是正确生成所有节点。例如,CodeProject RSS Feed,您可以看到 *guid* 节点丢失了。解析 RSS 时需要小心。
Atom 0.3 格式
<?xml version="1.0" encoding="utf-8"?>
<feed version="0.3" xml:lang="en-US"
xmlns="http://purl.org/atom/ns#">
<title>.NET Community Blog</title>
<link rel="alternate" type="text/html"
href="https:///b2evolution/index.php" />
<tagline>.NET Community Blog</tagline>
<generator url="http://b2evolution.net/"
version="0.9.0.10">b2evolution</generator>
<modified>1970-01-01T00:00:00Z</modified>
<entry>
<title type="text/plain"
mode="xml">Important information</title>
<link rel="alternate" type="text/html"
href=https:///b2evolution/index.php... />
<author>
<name>admin</name>
</author>
<id>https:///b2evolution/index.php?...</id>
<issued>2005-06-17T10:05:52Z</issued>
<modified>1970-01-01T00:00:00Z</modified>
<content type="text/html" mode="escaped">
<![CDATA[ <p>Blog B contains a few posts in the
'b2evolution Tips' category.</p>
<p>All these entries are designed to help you so,
as EdB would say:
"<em>read them all before you start
hacking away!</em>" ...]]></content>
</entry>
Atom 类型也类似,但比 RSS 2.0 格式看起来稍好。我注意到的更好的地方是,*content* 节点有一个不错的 *type* 属性,定义了内容的类型。*mode* 属性有助于识别是否需要 HTML 解码。它还有一个不错的 *id* 属性,唯一标识一个 *entry*。节点很明显,最重要的是;当它们生成 Atom Feed 时,每个人都产生一致的输出,而 RSS 有几种仍然广泛使用的版本。
日期处理
当您制作 RSS 聚合器时,您很快就会意识到人们不遵循一致的日期格式。有些使用 .NET 的 *DateTime* 格式,有些使用 PHP 的日期格式,有些使用 Java 的日期格式,有些甚至使用 RFC822 或 RFC1123 日期格式。现在人们使用的日期格式太多了,您找不到任何可以解析所有这些格式的代码。经过大量挣扎,我终于找到了这个函数,它试图消化几种可能的日期格式。
private DateTime FormatDate( string date )
{
string RFC822 = "ddd, dd MMM yyyy HH:mm:ss zzz";
//string RFC1123 = "yyyyMMddTHHmmss";
//string RFCUnknown = "yyyy-MM-ddTHH:mm:ssZ";
int indexOfPlus = date.LastIndexOf('+');
if( indexOfPlus > 0 )
date = date.Substring( 0, indexOfPlus-1 );
string [] formats = new string[] { "r", "S", "U" };
try
{
// Parse the dates using the standard
// universal date format
return DateTime.Parse(date,
CultureInfo.InvariantCulture,
DateTimeStyles.AdjustToUniversal);
}
catch
{
try
{
// Standard formats failed, try the "r" "S"
// and "U" formats
return DateTime.ParseExact( date, formats,
DateTimeFormatInfo.InvariantInfo,
DateTimeStyles.AdjustToUniversal);
}
catch
{
try
{
// All the standards formats have failed,
//try the dreaded RFC822 format
return DateTime.ParseExact( date, RFC822,
DateTimeFormatInfo.InvariantInfo,
DateTimeStyles.AdjustToUniversal);
}
catch
{
// All failed! The RSS Feed source
// should be sued
return DateTime.Now;
}
}
}
}
为 Atom、RDF 和 RSS Feed 创建通用解析器
各种被广泛接受的格式使开发人员的生活变得困难,因为他们必须在应用程序中支持所有广泛使用的规范。Feed 聚合器需要同时支持 RSS、Atom 和 RDF,因为所有这些都被广泛使用。这导致了设计复杂性,因为您需要使您的对象模型和解析过程通用,以解析和存储三种不同格式的 Feed。
经过大量搜索,我终于意识到每个人都为 RSS 和 Atom Feed 产生了不同的对象模型。但是,拥有不同的对象模型意味着您需要在数据库中创建不同的表结构,这很难维护。您还需要在所有地方编写代码,首先检查它是处理 Atom 还是 RSS Feed。这使得您的应用程序很复杂。XML 可以解决此类问题。在 XML 中,您可以存储不同类型的数据,但完全结构化。使用 XSL,您可以轻松地以相同的方式渲染不同的 XML 结构。因此,无论您的 XML 中是 Atom 还是 RSS 内容,XSL 都可以轻松检查类型并以相同的 HTML 输出进行查看。
首先,让我们看看我制作的通用 Feed 解析器。*FeedProcessor.cs* 是通用 Feed 解析器,它可以以相同的方式解析 RSS/Atom/RDF,并以一种通用格式生成 *Channel* 和 *RSSFeed* 对象。
解析时,它首先检查 XML 是否包含 RSS、Atom 或 RDF。
public IList Parse( XmlReader reader )
{
IList channels = new ArrayList();
while( reader.Read() )
{
if( reader.NodeType == XmlNodeType.Element )
{
string name = reader.Name.ToLower();
switch( name )
{
case "atom:feed": // We have Atom Feed
case "feed": // We have Atom Feed
channels.Add( this.ProcessAtomFeed(reader));
break;
case "rdf:rdf": // We have rdf feed
case "rdf": // We have rdf feed
case "rss:rss": // We have rss feed
case "rss": // We have rss feed
channels.Add( this.ProcessRssFeed(reader));
break;
}
}
}
return channels;
}
对于 Atom Feed,它调用 *ProcessAtomFeed* 来解析频道属性。
private RssChannel ProcessAtomFeed( XmlReader reader )
{
RssChannel channel = new RssChannel();
channel.Type = RssTypeEnum.Atom;
channel.Feeds = new ArrayList();
while( reader.Read() )
{
if( reader.NodeType == XmlNodeType.Element )
{
string name = reader.Name;
switch( name )
{
case "title": // title for channel
channel.Title = ReadString( reader );
break;
case "link": // link to website
reader.MoveToAttribute("href");
if( reader.ReadAttributeValue() )
{
channel.Link = reader.Value;
}
break;
case "tagline": // description of the channel
channel.Description = ReadString( reader );
break;
case "description": // Same
channel.Description = ReadString( reader );
break;
case "entry": // Aha! an entry
channel.Feeds.Add(this.ProcessAtomEntry(reader));
break;
}
}
else if( reader.NodeType == XmlNodeType.EndElement )
{
if( reader.Name == "feed" )
break;
}
}
return channel;
}
同样,*ProcessRssFeed* 函数也做同样的事情。
复杂的部分是解析实际包含 XML 内容的 *entry* 或 *item* 节点。我们需要以两种方式解析它:
- 我们需要根据我们正在解析的内容(Arom/RSS)发现一些基本属性,如发布日期、标题和 GUID。
- 我们需要将我们正在读取的所有内容存储在缓冲区中,因为 *XmlReader* 是一次性读取器,一旦向前移动就无法返回。
因此,我们不仅需要使用 *XmlReader* 来读取,还需要使用 *XmlWriter* 来写入我们正在临时缓冲区中读取的相同内容。
下一个设计复杂性是编写一个通用函数,以相同的方式解析 *entry* 和 *item* 节点。虽然我们可以为解析 *entry* 和 *item* 节点创建两个不同的函数,但这两个函数将有 90% 的代码重复;唯一的区别是一些节点的名称和一些结构上的差异。所以,这是解析所有这些的函数。
private RssFeed ProcessFeedNode( XmlReader reader,
string itemNodeName, string titleNodeName,
string guidNodeName, string linkNodeName,
string pubDateNodeName )
{
RssFeed feed = new RssFeed();
// Build a buffer which stores the
// entire XML content of the entry
StringBuilder buffer = new StringBuilder(1024);
XmlTextWriter writer =
new XmlTextWriter(new StringWriter(buffer));
writer.Namespaces = false;
writer.Indentation = 1;
writer.IndentChar = '\t';
writer.Formatting = Formatting.Indented;
writer.WriteStartElement(itemNodeName);
string lastNode = reader.Name;
while( (reader.NodeType == XmlNodeType.Element
&& lastNode != reader.Name) || reader.Read() )
{
if( reader.NodeType == XmlNodeType.Element )
{
lastNode = reader.Name;
writer.WriteStartElement( reader.Name );
writer.WriteAttributes( reader, true );
if( reader.Name == titleNodeName )
{
feed.Title = ReadString( reader );
writer.WriteString(feed.Title);
}
else if( reader.Name == guidNodeName )
{
feed.Guid = ReadString( reader );
writer.WriteString(feed.Guid);
}
else if( reader.Name == linkNodeName )
{
// Atom feed contains the link as "href" attribute
string link = reader.GetAttribute("href", "");
if( null == link )
{
// but Rss feed has the link as value
link = ReadString( reader );
writer.WriteString( link );
}
if( feed.Guid == null )
{
feed.Guid = link;
}
}
else if( reader.Name == pubDateNodeName )
{
string date = ReadString( reader );
feed.PublishDate = this.FormatDate( date );
writer.WriteString(date);
}
else
{
writer.WriteRaw( reader.ReadInnerXml() );
}
// Close the element started
writer.WriteEndElement();
// For empty elements, ReadEndElement fails
if( reader.NodeType == XmlNodeType.EndElement )
{
if( reader.Name == itemNodeName ) break;
reader.ReadEndElement();
}
}
if( reader.NodeType == XmlNodeType.EndElement )
{
if( reader.Name == itemNodeName )
break;
}
}
writer.WriteEndElement();
writer.Close();
feed.XML = buffer.ToString();
return feed;
}
虽然此函数不是最优的,但我们可以通过多种方式对其进行优化。但它能很好地完成工作。它在不到一秒的时间内解析了 200 KB 的 Feed,CPU 占用率甚至不到 5%。
技巧 1. ReadString( reader ) 或 reader.ReadString()
您会在上面的代码中看到,我使用了一个自定义函数 *ReadString*,而不是使用 *XmlReader* 的 *ReadString* 方法。文档说 *ReadString* 方法应该读取字符串的内容。它不应该跳过结束标签。但在实践中,它确实会越过结束标签并在下一个开始标签处停止。所以,如果您正在读取 *<title>* 节点,并调用 *ReadString*,您将获得的下一个节点是 *<pubDate>* 节点,而不是 *</title>*。但我们需要知道标签何时关闭,以便我们也可以在 *XmlWriter* 中关闭标签。这就是我制作自定义 *ReadString* 方法的原因。
private string ReadString( XmlReader reader )
{
/// Reuse existing buffer in order to prevent
/// frequent StringBuffer allocation
buffer.Length = 0;
/// Empty elements have no content
if( reader.IsEmptyElement ) return string.Empty;
/// Skip the begin tag and all white spaces before
/// the first character of content is found
while(!reader.EOF
&& ( reader.NodeType == XmlNodeType.Element
|| reader.NodeType == XmlNodeType.Whitespace ) )
reader.Read();
/// Read and store in buffer when we are getting text
/// and CDATA sections.
/// But stop immediately
/// whenever we read the end element.
while( reader.NodeType == XmlNodeType.CDATA
|| reader.NodeType == XmlNodeType.Text
&& reader.NodeType != XmlNodeType.EndElement )
{
buffer.Append( reader.Value );
reader.Read();
}
/// Now the read is poting to the EndElement. Return
/// the content of the buffer
/// we have prepared for this node
return buffer.ToString();
}
将 Atom 0.3 转换为 RSS 2.0
使此应用程序简单并只考虑 RSS 的最佳方法是,在从 Web 源下载内容后立即将 Atom XML 转换为 RSS XML。这样,整个应用程序就可以处理 RSS Feed,而无需担心所有其他格式。因此,将来,如果另一种格式变得流行,我所要做的就是编写另一个转换器,将该格式转换为 RSS 格式。应用程序无需进行重大更改。
在源代码中,您将找到 *atomtorss2.xslt*,它将 Atom 0.3 XML 转换为 RSS 2.0 XML。这是执行转换的 XSLT 的一小部分摘录。
<xsl:template name="items">
<xsl:for-each select="atom:entry">
<item>
<title><xsl:value-of select="atom:title"/></title>
<link>
<xsl:value-of select="atom:link[@rel='alternate']/@href"/>
</link>
<guid><xsl:value-of select="atom:id" /></guid>
<description>
<xsl:value-of select="atom:content" />
</description>
<pubDate>
<xsl:choose>
<xsl:when test='atom:issued'>
<xsl:value-of select="date:format-date(atom:issued,'EEE,
dd MMM yyyy hh:mm:ss z')"/>
</xsl:when>
<xsl:when test='atom:modified'>
<xsl:value-of select="date:format-date(atom:modified,'EEE,
dd MMM yyyy hh:mm:ss z')"/>
</xsl:when>
</xsl:choose>
</pubDate>
</item>
</xsl:for-each>
</xsl:template>
现在,您会注意到,在 *select* 属性中有一个函数 *format-date*。此函数在 XSLT 处理器中不可用。那么我们该怎么做呢?
介绍 EXSLT 项目
EXSLT 将 XSLT 处理提升到一个新的水平。它支持丰富的函数集合,您可以在 XSLT 脚本中使用它们,这使得 XSL 成为真实世界 XML 转换的强大脚本。*XsltTransformer* 的优点在于,它允许您编写纯 .NET 函数,这些函数在从 XSLT 脚本调用时会被调用。使用此功能,您可以进行复杂的 XML 转换,从而充分利用 .NET 平台的全部功能。您甚至可以编写调用数据库并获取动态值并将该值放入生成的 XML 中的函数。
使用 *EXSLT* 非常简单。以下代码显示了如何将 Atom 0.3 XML 转换为 RSS 2.0 XML。
void EXSLT()
{
ExsltTransform xslt = new ExsltTransform();
xslt.SupportedFunctions = ExsltFunctionNamespace.All;
xslt.MultiOutput = false;
xslt.Load("atomtorss2.xslt");
xslt.Transform("atom.xml", "rss.xml");
}
XML 序列化/反序列化
虽然我们都知道,但我总是忘记正确地序列化包含自定义对象的 *ArrayList*。所以,我制作了一个方便的 *SerializationHelper* 类,它公开了 *Serialize* 或 *Deserialize* 函数的变体,您可以在应用程序中使用它们。
public static XmlWriter Serialize( Stream stream, object o )
{
XmlTextWriter writer = new XmlTextWriter( stream,
System.Text.Encoding.UTF8 );
XmlSerializer serializer =
new XmlSerializer( o.GetType() );
serializer.Serialize( writer, o );
return writer;
}
public static XmlWriter Serialize( Stream stream,
ArrayList array, Type type )
{
XmlTextWriter writer = new XmlTextWriter( stream,
System.Text.Encoding.UTF8 );
XmlSerializer serializer = new
XmlSerializer(typeof(ArrayList), new Type[] {type});
serializer.Serialize( writer, array );
return writer;
}
public static object Deserialize( Stream stream, Type t )
{
XmlTextReader reader = new XmlTextReader( stream );
XmlSerializer serializer = new XmlSerializer( t );
object o = serializer.Deserialize( reader );
return o;
}
public static ArrayList DeserializeArraylist(Stream stream,
Type t)
{
XmlTextReader reader = new XmlTextReader( stream );
XmlSerializer serializer = new
XmlSerializer( typeof( ArrayList ), new Type [] { t } );
ArrayList list =
(ArrayList) serializer.Deserialize( reader );
return list;
}
要序列化包含 *RSSFeed* 类型对象的 *ArrayList*,请发出以下命令。
Serialize( stream, arrayList, typeof( RSSFeed ) );
技巧 2. 序列化类型为数组的对象
如果您有一个 *ArrayList* 类型的属性,序列化的 XML 会非常难看,并且不像我们所说的强类型(如果这适用于 XML)。为了自定义 *ArrayList* 的序列化方式,您可以尝试使用 *XmlArray* 和 *XmlArrayItem* 两个属性。
private ArrayList _WebLogs = new ArrayList();
[XmlArray("weblogs"), XmlArrayItem("weblog", typeof(WebLog)) ]
public ArrayList WebLogs
{
get { return _WebLogs; }
set { _WebLogs = value; }
}
这样您将获得漂亮的 XML 输出。
嵌入式资源
将附加文件随项目一起携带的最简单方法不是将它们作为外部文件携带,而是将它们放入程序集中。Visual Studio 为此目的提供了一个名为 Build Action 的好属性。
如果将属性设置为 Embedded Resource,则该文件链接到程序集中。这意味着文件的全部内容作为内嵌资源嵌入到程序集中。因此,您可以制作一个包含所有必需文件的程序集。您可以直接将文件作为 *Stream* 从程序集中读取,而不是作为文件打开。每个文件都按照以下命名约定嵌入到程序集中。
NameSpace.FileName.Extention
因此,对于文件 *atomtorss2.xslt*,嵌入式资源的完整名称是。
RSSFeederResources.atomtorss2.xslt
嵌入式资源最棒的一点是它们不在文件系统中作为单独的物理文件存在。所以,您永远不需要担心文件的路径。您可以直接将文件内容作为 *Stream* 读取。但是,嵌入式资源是只读的。如果您想修改内容,那么据我所知,目前还没有办法做到这一点。在这种情况下,您必须从嵌入式资源创建一个文件,并始终使用该文件。
*SerializationHelper* 包含一些方便的函数来处理嵌入式程序集。例如:
public static Assembly GetResourceAssembly()
{
return Assembly.LoadFrom("RSSFeederResources.dll");
}
public static Stream GetStream( string name )
{
return GetResourceAssembly().GetManifestResourceStream(
RESOURCE_ASSEMBLY_PREFIX + name);
}
*GetStream* 以 *Stream* 的形式返回嵌入式资源。
public static System.Drawing.Icon GetIcon( string name )
{
using( Stream stream = GetStream( name ) )
{
return new System.Drawing.Icon( stream );
}
}
您直接从嵌入式图标文件获取 *Icon* 对象。这对于携带应用程序使用的所有图标非常方便。您无需维护单独的图标文件并担心它们的路径。
public static void WriteEmbeddedFile( string name, string fileName )
{
using( Stream stream = GetStream( name ) )
{
FileInfo file = new FileInfo( fileName );
using( FileStream fileStream = file.Create() )
{
byte [] buf = new byte[ 1024 ];
int size;
while( (size = stream.Read( buf, 0, 1024 )) > 0 )
{
fileStream.Write( buf, 0, size );
}
}
}
}
函数 *WriteEmbeddedFile* 从嵌入式资源的内容生成一个文件。
将数据存储在应用程序数据文件夹中
存储应用程序特定数据的最佳位置不在您的程序安装的文件夹中,而是在 Windows® 为每个用户创建的“应用程序数据”文件夹中。实际上有两个应用程序数据文件夹。一个直接位于用户文件夹下,另一个位于隐藏的“Local Settings”文件夹中。所以,如果您的用户名是 Omar AL Zabir,这些文件夹的路径将是:
C:\Documents and Settings\Omar Al Zabir\Application Data
而秘密的是:
C:\Documents and Settings\Omar Al Zabir\Local Settings\Application Data
它们之间最大的区别是第一个是可见的。您可以通过 Explorer 浏览到该文件夹。但第二个默认是隐藏的。您需要从 Explorer 选项中打开“显示隐藏文件”才能看到该文件夹。
另一个区别是,当您在 Windows® 中拥有漫游配置文件时,第一个可见文件夹会同步到网络存储。所以,您存储在该文件夹中的任何内容,当您从域中的另一台计算机登录时都可以轻松访问。此位置比应用程序 *exe* 所在的位置更适合存储文件,因为该位置不会漫游。请记住,第二个文件夹是特定于计算机的,并且在您从一台计算机移到另一台计算机时不会同步。
RSS Feeder 将所有数据存储在第二个文件夹中。
您可以使用 *Environment.GetFolderPath* 函数获取这些特殊文件夹的路径。它需要一个枚举,如以下代码片段所示。
enum System.Environment.SpecialFolder
{
ApplicationData
CommonApplicationData
LocalApplicationData
...
}
还有许多其他有用的文件夹,如 Desktop、Program Files、My Documents 等。所有这些都可以从这个 enum
中获得。这是如何使用它的:
// Prepare the path where all application specific
// settings will be stored
string appDataPath = Environment.GetFolderPath(
Environment.SpecialFolder.LocalApplicationData);
ApplicationSettings.ApplicationDataPath =
Path.Combine( appDataPath, "RSS Feeder" );
// Check if all these paths exist
if( !Directory.Exists(ApplicationSettings.ApplicationDataPath))
{
Directory.CreateDirectory(ApplicationSettings.ApplicationDataPath);
}
这样,您就可以设置自己的文件夹来存储所有应用程序特定的文件。
OPML
OPML 是一种基于 XML 的格式,允许在不同操作系统和环境中运行的应用程序之间交换大纲结构化信息。OPML 用于存储有关 RSS Feed 源的信息。例如,一个博客网站使用 OPML 来存储它包含的所有博客标题和 Feed 位置。如果您访问 blogs.msdn.com,您将获得所有 Microsoft 博主 Feed URL 和标题的 OPML。
<opml>
<body>
<outline text="Microsoft Bloggers">
<outline title="Alex Lowe's .NET Blog"
htmlUrl=http://blogs.msdn.com/alowe/default.aspx
xmlUrl="http://blogs.msdn.com/alowe/rss.aspx" />
<outline title="Michał Cierniak"
htmlUrl=http://blogs.msdn.com/michaljc/default.aspx
xmlUrl="http://blogs.msdn.com/michaljc/rss.aspx" />
...
...
上面的 XML 是 blogs.msdn.com 的 OPML 摘录。
RSS 聚合器使用 OPML 在其他聚合器之间交换订阅信息。例如,您可以从 Newsgator 导出所有订阅为 OPML,然后将 OPML 导入我的 RSS Feeder。事实上,您根本不需要这样做。每次运行它时,它都会导入 Newsgator 设置。请注意,Newsgator 的 XML 包含“xmlurl”,而所有其他 OPML 使用“xmlUrl”。区别在于“U”的大小写。
Newsgator 存储其订阅信息在一个 OPML 中。它的 OPML 有点不同。
<?xml version="1.0" encoding="utf-8"?>
<opml xmlns:ng="http://newsgator.com/schema/opml">
<body>
<outline title="NewsGator News and Updates"
description="NewsGator News and Updates"
lastItemMD5="eFY8dWcOAPZBbpG7Ha1l5g==,XqCFJLx/DJigD7YRFnD3OA==,..."
xmlurl=http://www.newsgator.com/news/rss.aspx
htmlurl="http://www.newsgator.com"
ng:folderName="" ng:folderType="auto"
ng:useDefaultCredentials="false"
ng:username="" ng:passwordenc=""
ng:domain="" ng:useGuid="false"
ng:interval="0" ng:nntpMostRecent="-1"
ng:newsPage="true" ng:renderTransform=""
ng:downloadAttachments="false" />
</body>
</opml>
它使用 *ng:folderName* 和 *ng:folderType* 等附加属性来描述订阅映射到的 Outlook 文件夹位置。RSS Feeder 提供 Newsgator 导入功能。它使用此信息来映射到与 Newsgator 相同的文件夹。
源代码中的 *OpmlHelper* 类提供了 OPML 解析和生成功能。
数据库优化
由于 RSS Feeder 使用 MS Access 数据库,它在多个客户端连接数据库方面没有太多问题,也没有在 SQL Server 中需要考虑的额外设计问题。但是,我们需要优化连接的打开和关闭,因为 MS Access 打开和关闭连接需要很长时间。
所以,我在这里做的是在应用程序启动时打开一个静态连接,并在整个应用程序中使用该连接。当应用程序关闭时,它会关闭打开的连接。这比每次访问数据库时打开和关闭连接提供了显著的性能提升。
但是,静态连接对象会导致多线程问题。有时两个线程可以同时尝试在同一个连接上执行命令。例如,想象一下 Feed Downloader 正在后台下载 Feed,而您正在读取 Feed。现在,您和 Feed Downloader 都试图同时读/写数据库中的 Feed。至少有一个会失败,因为连接将处于 *Executing* 状态而不是 *Open* 状态。为了防止这种情况,我在看到连接处于 *Executing* 状态时实现了一个 *Thread.Sleep*。
private static OleDbConnection __Connection = null;
private static OleDbConnection _Connection
{
get
{
if( null == __Connection )
{
string connectionString = string.Format
( "Provider=Microsoft.Jet.OLEDB.4.0;Data Source={0};",
ApplicationSettings.DatabaseFilePath );
__Connection = new OleDbConnection( connectionString );
__Connection.Open();
}
else
{
while( ConnectionState.Executing == __Connection.State
|| ConnectionState.Fetching == __Connection.State )
{
System.Threading.Thread.Sleep( 50 );
}
if( ConnectionState.Open != __Connection.State )
__Connection.Open();
}
return __Connection;
}
}
public static void Close()
{
if( null != __Connection )
if( ConnectionState.Closed != __Connection.State )
{
__Connection.Close();
__Connection.Dispose();
}
}
因此,当一个线程尝试在连接正在执行时获取一个连接对象时,该线程会进入 50 毫秒的睡眠。这足以让执行完成。
在普通的单处理器计算机上,您很少会遇到这个问题。但如果您拥有模拟两个处理器的 P4 超线程处理器,或者您自己家里的六核 Xeon 处理器的 HP6000,那么您将经常遇到这个问题。
MS Outlook® 集成
这是最复杂的部分。在自动化 Outlook 时面临的第一个问题是释放代码中使用的所有对象引用。如果您没有正确释放引用,Outlook 即使从屏幕上消失也不会关闭。第二个问题是创建与版本无关的解决方案。如果您从 Visual Studio 中“Add Reference...”到 Outlook COM 库并设置对特定版本 DLL 的引用,生成的互操作程序集将成为特定版本程序集。因此,它将无法与旧版本的 Outlook 正常工作。使其与版本无关的唯一方法是针对您想要支持的最旧版本的 Outlook 进行开发,并从该版本进行构建。
另一个使 Outlook 自动化真正与版本无关的解决方案是使用后期绑定。您可以使用 .NET 框架中的 *Activator* 类通过 ProgID 实例化任何 COM 对象。因此,您可以使用后期绑定启动 Outlook,如下所示。
public static void StartOutlook( ref object app, ref object name )
{
try
{
// Try to get existing outlook instance
app = Marshal.GetActiveObject("Outlook.Application");
name = GetProperty( app, "Session" );
}
catch
{
// Create new instance of outlook
app = GetObject("Outlook.Application");
name = GetProperty( app, "Session" );
}
}
public static object GetObject( string typeName )
{
Type objectType = Type.GetTypeFromProgID(typeName);
return Activator.CreateInstance( objectType );
}
public static object GetProperty( object obj,
string propertyName )
{
return obj.GetType().InvokeMember( propertyName,
BindingFlags.GetProperty, null, obj, null );
}
虽然这使得编码成为一场噩梦,但您可以创建辅助函数来处理对象和属性。源代码中提供的 *OutlookHelper* 类为您提供了许多用于自动化 Outlook 的辅助函数。但是,您仍然需要在编写使用后期绑定方法进行编码之前先记住 Outlook 的对象模型。为了获得安全的后期绑定 COM 互操作方法以及与版本无关但强类型的 Outlook 自动化库,请查看我的文章 SafeCOMWrapper。如果您使用它,您就不必编写繁琐的代码,也无需记住对象模型。
以下是显示 Outlook 文件夹选择器对话框的后期绑定方法。
public static string SelectFolderPath()
{
// Show outlook folder picker
object app = null, name = null;
StartOutlook( ref app, ref name );
// Ensure at least one explorer is visible
// otherwise we cannot show the folder
// picker
bool isOutlookInvisible = EnsureExplorer( app, name );
object folder = CallMethod( name, "PickFolder" );
string path = null;
// Get the selected folder name
if( null != folder )
path = GetProperty( folder, "FolderPath" ) as string;
if( null != folder )
Marshal.ReleaseComObject( folder );
folder = null;
Marshal.ReleaseComObject( name );
name = null;
if( isOutlookInvisible )
CloseOutlook(app);
Marshal.ReleaseComObject( app );
app = null;
// Outlook still hangs around in the task
// manager until we call this
GC.Collect();
return path;
}
从 RSS Feed 创建 Outlook 帖子
每当应用程序下载 RSS Feed 时,它也会将它们发送到映射的 Outlook 文件夹。首先,它使用本文所述的 XSLT 转换将条目的 XML 转换为 HTML。然后,它使用 *UserProperty* 来存储实际的 XML 相对于帖子。这样,您以后就可以编写宏来处理原始条目的 XML。
foreach( RssFeed item in rssItems )
{
// Create outlook item
object post = OutlookHelper.CallMethod( folderItems,
"Add", 6 );
//Outlook.OlItemType.olPostItem = 6
//post.CreationTime = item.PublishDate;
OutlookHelper.SetProperty( post, "Subject",
item.Title );
// Clear stream
stream.Position = 0;
stream.SetLength(0);
// Transform RSS item XML to HTML
XMLHelper.TransformXml( xsltFileName,
item.XML, stream );
// Read the content from the stream
stream.Position = 0;
string html = reader.ReadToEnd();
OutlookHelper.SetProperty( post,
"HTMLBody", html );
OutlookHelper.SetProperty( post,
"BodyFormat", 2 );
// Outlook.OlBodyFormat.olFormatHTML = 2
// Store the actual item XML in an user property
object userProperties =
OutlookHelper.GetProperty( post, "UserProperties" );
object missing = System.Reflection.Missing.Value;
object xmlProperty =
OutlookHelper.CallMethod( userProperties, "Add", "XML",
1, missing, missing );
// Outlook.OlUserPropertyType.olText = 1
Marshal.ReleaseComObject( userProperties );
OutlookHelper.SetProperty( xmlProperty,
"Value", item.XML );
OutlookHelper.SetProperty( post, "UnRead", 1 );
OutlookHelper.CallMethod( post, "Post" );
Marshal.ReleaseComObject( post );
itemsAdded ++;
}
从 Outlook 文件夹读取帖子并发送到博客站点
RSS Feeder 提供直接从 Outlook 博客。首先,您创建一个“Mail and Post”类型的文件夹。然后,您在该文件夹中创建帖子。您还可以将帖子从其他文件夹拖到此文件夹。RSS Feeder 会拾取文件夹中可用的帖子,然后将其发布到 Weblog 站点。
以下代码显示了如何以困难的方式完成此操作,即后期绑定。
object folderItems = OutlookHelper.GetProperty( folder, "Items" );
int index = 1;
while(index <=
(int)OutlookHelper.GetProperty(folderItems,
"Count"))
{
object item =
OutlookHelper.GetItem( folderItems, index );
string messageClass =
(string)OutlookHelper.GetProperty( item,
"MessageClass" );
if( "IPM.Post" == messageClass )
{
string subject =
(string)OutlookHelper.GetProperty(item,
"Subject");
string categories =
(string)OutlookHelper.GetProperty( item,
"Categories", "" );
string html;
try
{
html =
(string)OutlookHelper.GetProperty( item,
"HTMLBody");
}
catch
{
...
MessageBox.Show( this,
"You did not allow me to read" +
"the post from Outlook." +
"Please allow it next time.",
"Outlook Error", MessageBoxButtons.OK,
MessageBoxIcon.Exclamation );
...
...
...
continue;
}
// Contruct a new post
Post p = new Post();
p.Title = subject;
p.Text = html;
p.Date = DateTime.Now;
p.Categories =
webLog.GetCategories( categories );
// Post to web log, note this goes over web
try
{
WebLogProvider.Instance.PostBlog(webLog, p);
// After successful post, move
// the post to "Sent" folder
OutlookHelper.CallMethod( item,
"Move", sentFolder );
}
catch( Exception x )
{
...
}
}
Marshal.ReleaseComObject( item );
}
使用 HTTP 下载 Feed
虽然这个领域没有什么特别之处,但我可以给您提供一些方便的技巧。例如,我制作了一个方便的函数,可以从 URL 下载内容并提供下载过程中的进度更新。因此,您可以创建一个进度对话框,在下载继续时显示已下载内容的大小和连接速度。
技巧 3. 使用 MemoryStream 和进度更新下载内容
以下函数处理带代理支持的常见下载场景。
private static void DownloadContent( Uri uri,
string proxyName, int port,
string proxyUserName, string proxyPassword,
ProgressEventHandler progressHandler,
MemoryStream memoryStream )
{
WebRequest webRequest = HttpWebRequest.Create(uri);
// Set proxy settings to the web request
if( null != proxyName && proxyName.Length > 0 )
{
webRequest.Proxy = new System.Net.WebProxy( proxyName, port );
if( proxyUserName.Length > 0 )
{
// Decrypt the encrypted password
string password = RSSCommon.PropertyEditor.PasswordEditor.
PasswordProvider.Decrypt(proxyPassword);
// Set proxy credentials
webRequest.Proxy.Credentials =
new System.Net.NetworkCredential( proxyUserName, password );
}
}
else
webRequest.Proxy = System.Net.WebProxy.GetDefaultProxy();
DateTime startTime = DateTime.Now;
using(System.Net.WebResponse webResponse =
webRequest.GetResponse())
{
using(System.IO.Stream stream =
webResponse.GetResponseStream())
{
byte [] buffer = new byte[ 1024 * 2 ]; // 2 KB buffer
int size;
while( (size = stream.Read( buffer, 0,
buffer.Length) ) > 0 )
{
memoryStream.Write( buffer, 0, size );
// Perform some speed calculation
// Total time elapsed
TimeSpan duration = DateTime.Now - startTime;
// Downloaded kilobytes
double kb = (memoryStream.Length / 1024.0);
double speed = kb / duration.TotalSeconds;
string message = string.Format( "{0} kb, {1} kbps",
kb.ToString("n2"), speed.ToString("n2") );
// notify download progress
progressHandler( null,
new ProgressEventArgs( message , 0 ) );
}
}
}
}
上述函数接受一个类型为 *ProgressEventHandler* 的委托,该委托定义如下:
public delegate void ProgressEventHandler( object sender, ProgressEventArgs e );
*ProgressEventArgs* 扩展了 .NET 框架的 *EventArgs*。
[Serializable]
public class ProgressEventArgs : EventArgs
{
public string Message;
public int Value;
public ProgressEventArgs(string message, int value)
{
this.Message = message;
this.Value = value;
}
}
这是制作 Events 的标准方法。我见过人们制作自定义委托并将所有所需信息作为参数。这不是规定的方法。建议的方法是制作类似于 *EventHandler* 委托的委托,并制作一个扩展 *EventArgs* 的自定义类,该类包含所有事件参数。这使得设计一致,其他开发人员可以轻松掌握概念,因为它更接近他们已经使用的东西。
技巧 4. 以简单安全的方式从非 UI 线程更新 UI
我们都会犯的一个常见错误是,我们尝试从后台线程或自创建的线程更新 UI,而不是使用主 UI 线程,从而导致应用程序频繁崩溃。在设计多线程应用程序时,您需要确保您绝不会从主线程以外的任何其他线程使用 UI 的任何资源。通常我们都会犯的常见错误是,我们将另一个线程的代码分解成不同的函数,而在这些函数中,我们忘记了它正在另一个线程中运行的事实。结果,当我们尝试访问 UI 元素时,应用程序会频繁崩溃。
为了从另一个线程调用或正在另一个线程中运行的函数更新 UI,您需要使用 *Control* 或 *Form* 的 *Invoke* 或 *BeginInvoke* 方法。这是我最喜欢的方式。
private void UpdateWebLogProgress( object sender,
ProgressEventArgs e )
{
if( base.Disposing ) return;
if( base.InvokeRequired )
{
this.Invoke( new ProgressEventHandler(
this.UpdateWebLogProgress ),
new object[] { sender, e } );
return
}
// Access UI elements here, no problemo.
}
访问 UI 元素的函数本身会检查是否需要调用。如果需要调用,它会通过 *delegate* 使用 *Invoke* 方法调用自身。如果不需要调用,它会执行处理 UI 元素的代码。因此,它保证可以防止从另一个线程访问 UI 元素的问题。它还节省了代码,因为您不需要从所有地方显式地调用它。
this.Invoke( new ProgressEventHandler(this.UpdateWebLogProgress),
new object[] {this, new ProgressEventArgs(“Complete”, 100)});
您只需要从任何线程执行以下操作:
UpdateWebLogProgress( this, new ProgressEventArgs( “Complete”, 100 ) );
因此,它使调用者不必记住处理 UI 元素的功能。
RSS 自动发现
RSS 聚合器的一个有趣功能是从任何 URL 检测 RSS Feed 或 RSS Feed 位置的存在。在实现自动发现时,您需要考虑不同的场景。
- URL 本身可能是一个 RSS Feed,而不是 HTML。
- URL 可以是一个具有 *<link rel="alternate" type="application/rss+xml" …>* 的 HTML 页面,该页面指向一个 RSS 源。
- HTML 页面可能包含指向 RSS Feed 源的超链接。
RSS Feeder 可以智能地识别 URL 包含的内容并采取相应措施。例如,如果您提供一个 HTML 页面的 URL,它会查找 HTML 中指定的所有 RSS Feed 源。所以,如果您提供 URL msdn.microsoft.com,它会自动检测该页面中指定的所有 RSS 频道。
另一方面,如果您提供一个包含某种类型 Feed 的 URL,它将自动检测 Feed 的类型。
HTML 处理由著名的 *SgmlReader* 完成。以下是发现从 URL 下载数据后找到的内容类型的代码。
private static RssTypeEnum Discover( MemoryStream memoryStream,
ref IList channelSources, ProgressEventHandler progressHandler )
{
// Parse the content and find out whether the content is HTML or XML
Sgml.SgmlReader reader = new Sgml.SgmlReader();
bool isHtml = false;
RssTypeEnum feedType = RssTypeEnum.Unknown;
memoryStream.Position = 0;
/// Parse the response using a SGML parser in order to identify whether
/// the response is HTML or XML.
reader.InputStream = new StreamReader( memoryStream );
try
{
while( reader.Read() )
{
if( null != reader.Name )
{
string name = reader.Name.Trim().ToLower();
/// If we get the html tag in the document,
/// then it is definitely a HTML page
if( name == "html" || name == "body" )
{
isHtml = true;
feedType = RssTypeEnum.Unknown;
}
// RSS XML starts from <rss> node
else if( (name == "rss" || name == "rdf:rdf")
&& !isHtml )
{
feedType = RssTypeEnum.RSS;
// We need to parse in a different
// way for RSS stream
break;
}
// Atom XML starts from <feed> node
else if( name == "feed" && !isHtml )
{
feedType = RssTypeEnum.Atom;
// We need to parse in a different
// way for RSS stream
break;
}
else if( isHtml && name == "link" )
{
// Let's see if this link indicats a RSS feed
string rel = reader.GetAttribute("rel");
string type = reader.GetAttribute("type");
string title = reader.GetAttribute("title");
string href = reader.GetAttribute("href");
if( rel.ToLower() == "alternate"
&& type.ToLower() == "application/rss+xml")
{
channelSources.Add(new string [] { title, href });
}
if( rel.ToLower() == "alternate"
&& type.ToLower() == "application/atom+xml")
{
channelSources.Add(new string [] { title, href });
}
}
}
} // while( reader.Read() )
}
catch( Exception x )
{
EntLibHelper.Exception( "RSS Discovery", x );
// RSS discovery failed. SgmlReader can't parse it.
// May be this really is an RSS feed which .NET's framework
// can parse better.
feedType = RssTypeEnum.RSS;
}
return feedType;
}
代码逐个读取节点,直到发现 *html*、*rss*、*rdf* 或 *feed* 节点。如果到达 *html* 节点,它会假定这是一个 HTML 文档,并查找指定 RSS 或 Atom Feed 源引用的 *link* 节点。如果找到此类源,它会创建一个源集合并返回该集合。
RSS Feed 的 XSL 转换
此程序中有两种 XSL 转换 RSS XML 的方式。一种是渲染单个帖子,另一种是渲染包含多个频道和多个 Feed 的报纸。
渲染单个帖子
信不信由你,如果您无法正确地使用 XSL 将 XML 转换为 HTML,如果 XML 节点中包含嵌入式 HTML 内容。.NET 框架内置的 XSL 转换器无法解码节点中的 HTML 内容。尽管 XSL 规范说明您可以这样做。
<xsl:value-of disable-output-escaping="yes" select="description" />
这用于防止 XSL 转换器转义 description 节点的内容并按原样渲染,但它不起作用。无论如何,XSL 转换器都会编码 HTML 内容。结果,您看到的是 HTML 代码,而不是格式精美的 HTML 输出。
由于这个问题,我创建了 *HtmlReader* 和 *HtmlWriter* 类。有关详细信息,请参阅我的 HTML Cleaner 文章。
*HtmlWriter* 扩展了 *XmlWriter* 并防止转义标签内的文本内容。它会覆盖 *WriteString* 方法并从中调用 *WriteRaw*。
public override void WriteString(string text)
{
// Change all non-breaking space to normal space
text = text.Replace( " ", " " );
/// When you are reading RSS feed and writing
/// Html, this line helps remove
/// those CDATA tags
text = text.Replace("<![CDATA[","");
text = text.Replace("]]>", "");
if( this.FilterOutput )
{
// We want to replace consecutive spaces to
// one space in order to save horizontal
// width
if(this.ReduceConsecutiveSpace) text =
text.Replace(" ", " ");
if(this.RemoveNewlines) text =
text.Replace(Environment.NewLine, " ");
base.WriteRaw( text );
}
else
{
base.WriteRaw( text );
}
}
此类消除了对 *disable-output-escaping* 属性设置的需求。
渲染报纸 – 来自多个频道的多个帖子
这比渲染单个帖子要棘手,因为您需要在一个 HTML 中渲染多个频道。过程如下:
- 将所有频道合并到一个 XML 中,该 XML 包含一个 RSS 节点但包含多个 channel 节点。
- 使用 XSLT 转换合并后的 XML 以渲染 HTML。
我遇到的第一个问题是使用 *XmlWriter* 合并多个 XML。*XmlWriter* 的 *WriteRaw* 方法应该写入我给它的任何内容而无需进行任何编码。但它并没有这样做,而是编码了作为节点值的任何 XML。结果,我无法使用 *XmlWriter* 写入 RSS 条目的 XML。为了做到这一点,我需要先创建一个 *StreamWriter*,然后用 *XmlTextWriter* 包装它。常规 XML 构建由 *XmlTextWriter* 完成,但每当需要写入原始 XML 时,我就会调用 *StreamWriter* 的 *WriteLine* 方法。
因此,生成一个频道的代码如下所示:
// Write the channel header
writer.WriteStartElement("channel");
writer.WriteElementString( "title",
channel.Title );
writer.WriteElementString( "link",
channel.FeedURL.ToString() );
IList items =
DatabaseHelper.GetTopRssItems(channel.Id,
(int)itemCountUpDown.Value, false);
foreach( RssFeed feed in items )
{
streamWriter.WriteLine( feed.XML );
}
writer.WriteEndElement();
应用程序的单一实例
这是许多桌面应用程序的常见要求,其中您只需要允许运行一个应用程序实例。
这样做需要一些 Win32 级别的知识。当创建一个应用程序时,它会注册一个 *Mutex*。但在创建它之前,它会首先检查是否已经注册了同名的 *Mutex*。如果已注册,则表示应用程序的另一个实例已在运行,因此我们退出。
public static bool IsAlreadyRunning()
{
string strLoc =
Assembly.GetExecutingAssembly().Location;
FileSystemInfo fileInfo = new FileInfo(strLoc);
string sExeName = fileInfo.Name;
bool bCreatedNew;
mutex = new Mutex(true,
"Global\\"+sExeName, out bCreatedNew);
if (bCreatedNew)
mutex.ReleaseMutex();
return !bCreatedNew;
}
您可以在 这篇文章 中找到解决方案。
使用定时器制作响应式 UI
假设您有一个项目列表。每次用户选择一个项目时,您都需要执行一些数据库操作并生成 HTML,这需要花费时间。您已将此代码写入列表的 *SelectedIndexChanged* 事件中。因此,如果用户通过单击来选择一个项目,然后按住向下箭头一段时间,则事件将反复触发,导致频繁的数据库调用和 HTML 渲染。由于这些操作需要时间,因此选择指针不会平滑地向下移动,而是会在每个项目上停留一段时间,直到所有方法执行完成,整个 UI 才会变得无响应。
另一种情况是,如果用户先单击一个项目,然后使用鼠标向下滚动并按住 SHIFT 并单击另一个项目以进行多选,则列表的 *SelectedIndexChanged* 事件将为选择范围内的每个项目触发。结果,用户将卡住一段时间,直到所有事件都触发并且所有事件的代码执行完成。
为了解决这些问题,我们将使用一个计时器来调用执行实际工作的方法,而不是直接调用它。过程如下:
- 在列表的 *SelectedIndexChanged* 事件中,我们首先停止计时器以初始化其状态。然后我们用一个间隔启动计时器。
- 我们将一个委托指向需要调用的方法。
- 在 *Elapsed* 事件中,我们调用委托,停止计时器并清除委托。
结果是,无论列表的 *SelectedIndexChanged* 事件触发多少次,它都只会设置一个计时器,该计时器又会排队调用一个方法。由于我们使用委托指向一个方法而不是事件,因此该委托只会被调用一次,该委托又会调用实际方法一次。
以下是设置计时器的方法:
this._CallbackMethod =
new MethodInvoker( this.AutoSelectChannel );
this.callbackTimer.Interval =
STARTUP_SHOW_CHANNEL_DURATION;
this.callbackTimer.Start();
在 *Elapsed* 事件中,我们有这段代码:
private void callbackTimer_Elapsed(object sender,
System.Timers.ElapsedEventArgs e)
{
callbackTimer.Stop();
if( null != this._CallbackMethod )
this._CallbackMethod();
this._CallbackMethod = null;
}
减少 .NET 应用程序的内存占用
.NET Winforms 应用程序是内存密集型应用程序。一个简单的只有一个按钮、一个窗体的应用程序加载时大约需要 15 到 20 MB 内存,而其 VB 6 版本仅需要 15 到 20 KB。
但是,如果您在 RSS Feeder 位于托盘中且没有可见窗口时打开任务管理器,您会注意到它只占用大约 3 到 5 MB 内存。
这是通过减少应用程序进程的 *WorkingSet* 来完成的。*WorkingSet* 是 *Process* 类的一个属性,它包含进程正在消耗的内存量。有趣的是,它不是一个只读变量;您可以增加或减少它。如果您尝试减少它,您会发现内存使用量显着下降。虽然它并不总是有效,但我们可以随时尝试。
public static void ReduceMemory()
{
try
{
System.Diagnostics.Process loProcess =
System.Diagnostics.Process.GetCurrentProcess();
if(m_TipAction == true)
{
loProcess.MaxWorkingSet =
(IntPtr)((int)loProcess.MaxWorkingSet - 1);
loProcess.MinWorkingSet =
(IntPtr)((int)loProcess.MinWorkingSet - 1);
}
else
{
loProcess.MaxWorkingSet =
(IntPtr)((int)loProcess.MaxWorkingSet + 1);
loProcess.MinWorkingSet =
(IntPtr)((int)loProcess.MinWorkingSet + 1);
}
m_TipAction = !m_TipAction;
}
catch( Exception x )
{
System.Diagnostics.Debug.WriteLine( x );
}
}
在源代码中,您会找到一个 *MemoryHelper* 类来完成这项工作。当您的应用程序被禁用或最小化时,您可以调用 *ReduceMemory* 方法,为其他人留下一些内存。
注册应用程序在启动时运行
我们都知道如何做到这一点,在注册表中添加一个键,仅此而已。但将一个应用程序添加到启动项,尤其是 .NET 应用程序,对于 Windows 来说是一件很麻烦的事。通常,如果您尝试第一次加载 Winforms 应用程序,您会看到它有多慢以及它消耗多少硬盘活动。在 Windows 正在执行许多其他任务时将此类应用程序添加到启动项将使启动过程慢得多,并使用户失去耐心。
这就是为什么我用 VB 6 制作了一个应用程序加载程序,它只占用 20 KB 并且眨眼间就能加载。当用户将 RSS Feeder 设置为启动时运行,它实际上会将加载程序放在启动项中,但有 5 分钟的延迟。加载程序加载后会立即休眠 5 分钟,让 Windows 有时间自由呼吸,并在繁重的启动推送后擦干汗水。当一切都好起来时,它会启动庞大的 .NET 应用程序。
这是加载程序的 VB 6 代码。
Private Declare Sub Sleep Lib "kernel32" (_
ByVal dwMilliseconds As Long)
Sub Main()
On Error GoTo ExitDoor
' The first command line argument
' specifies the delay and the
' second argument specifies the exe to launch
Dim strArguments() As String
strArguments = Split(Command$, " ")
Dim lngDelay As Long
lngDelay = Val(strArguments(0))
Dim strFile As String
strFile = Trim(strArguments(1))
' Pause for the amount of time
Call Sleep(lngDelay * 1000)
' Run the program
Shell strFile
ExitDoor:
End Sub
通知托盘图标
RSS Feeder 通常以图标的形式保留在系统托盘中。所以,我们需要使用 Windows 2000/XP/2003 的气球通知功能来提供其活动信息。
这通过 CodeProject 中的 精彩文章 来实现。
错误报告
由于 RSS Feeder 在全世界运行而我却一无所知,我需要一种方法来了解我的用户是否愉快地使用它。因此,我实现了一个静默的崩溃报告模块,它捕获任何未处理的异常并将它们通过 Source Forge 跟踪系统报告给我。
错误报告的工作方式如下:
- 每当发生未处理的异常时,错误报告就会排队到 Error Queue。
- Error Reporting Engine 会定期监视队列。每当它从队列中提取一个错误时,它就会启动一个后台线程。
- 后台线程会准备一个错误报告,收集一些上下文信息,例如用户的当前登录名,这有助于识别同一用户的重复报告。
- 消息将作为 HTTP POST 发布到 Source Forge Tracker System。
错误报告引擎使用计时器定期检查队列。
private void errorReportTimer_Tick(object sender,
System.EventArgs e)
{
if( base.Disposing ) return;
// If no reports in queue, close
if( 0 == _ErrorReports.Count )
{
this.errorReportTimer.Stop();
_Instance = null;
base.Close();
}
else
{
if( !this._IsErrorBeingSent )
{
this._IsErrorBeingSent = true;
base.Show();
base.Refresh();
// Get the first error report item
ErrorReportItem item =
_ErrorReports[0] as ErrorReportItem;
_Instance.errorReportTextBox.Text = item.Details;
ThreadPool.QueueUserWorkItem( new
WaitCallback( SendQueuedReport ), item );
}
}
}
错误报告从后台线程发送,因此 UI 保持响应。
private void SendQueuedReport(object state)
{
if( base.Disposing ) return;
ErrorReportItem item = state as ErrorReportItem;
try
{
// Send the report
ErrorReportHelper.PostTrackerItem(item.Summary,
item.Details);
}
catch
{
// Who cares
}
lock( _ErrorReports )
{
_ErrorReports.Remove( state );
}
this._IsErrorBeingSent = false;
}
真正的奇迹在于 *ErrorReportHelper* 类,它负责执行将 HTTP POST 到 Source forge Tracker System 的任务。
public static void HttpPost( string url,
string referer, params string [] formVariables )
{
HttpWebRequest req = null;
try
{
req = (HttpWebRequest)
HttpWebRequest.Create(new Uri(url));
}
catch
{
return;
}
CookieContainer CookieCont= new CookieContainer();
req.AllowAutoRedirect = true;
req.UserAgent = "Mozilla/4.0 (compatible; " +
"MSIE 6.0; Windows NT 5.0; .NET CLR 1.1.4322)";
req.CookieContainer = CookieCont;
req.Referer = referer;
req.Accept = "image/gif, image/x-xbitmap,
image/jpeg, image/pjpeg,
application/msword,
application/vnd.ms-powerpoint,
application/vnd.ms-excel, */*";
req.Method = "POST";
req.ContentType =
"application/x-www-form-urlencoded";
现在我们已经准备好一个 *HttpWebRequest* 对象来执行 HTTP POST。接下来我们需要准备包含需要发布的内容的请求正文。
StringBuilder postData = new StringBuilder();
for( int i = 0; i < formVariables.Length; i += 2 )
{
postData.AppendFormat("{0}={1}&",
HttpUtility.UrlEncode(formVariables[i]),
HttpUtility.UrlEncode(formVariables[i+1]));
}
postData.Remove(postData.Length - 1, 1);
byte[] postDataBytes =
Encoding.UTF8.GetBytes(postData.ToString());
req.ContentLength = postDataBytes.Length;
我们的下一个任务是连接到实际的跟踪系统 *index.php* 并传输数据。
Stream postDataStream = req.GetRequestStream();
postDataStream.Write(postDataBytes, 0,
postDataBytes.Length);
postDataStream.Close();
为了进行成功的 HTTP 对话,我们确实需要获取一些响应,但不是全部响应。Source forge 的页面非常沉重。不要考虑下载整个页面。此外,返回的页面包含所有已发布的项目,并且可能有成千上万的错误报告!
// Get the response stream in order
// to complete a full HTTP POST
HttpWebResponse resp = null;
try
{
resp = (HttpWebResponse) req.GetResponse();
}
catch
{
return;
}
// We are not interested to read anything
Stream rcvStream = resp.GetResponseStream();
resp.Close();
rcvStream.Close();
}
技巧 5. 集中式错误处理
有一种非常方便的方法可以捕获有人错误地将其忽略在try catch
块之外的任何异常。Application
类提供了一个名为ThreadException
的事件,该事件可以捕获任何未被任何catch
块捕获的未处理异常。当您忘记捕获任何异常时,您会看到令人尴尬的“继续”或“退出”对话框,其中包含详细的异常跟踪,暴露了您所有的错误。如果您捕获了ThreadException
,则可以捕获任何泄露的异常,并且可以保持沉默或将其静默写入日志文件,以便稍后进行处理。
RSS Feeder 通过订阅两个事件来隐藏其不足之处。
AppDomain.CurrentDomain.UnhandledException +=
new UnhandledExceptionEventHandler(
CurrentDomain_UnhandledException);
Application.ThreadException += new
System.Threading.ThreadExceptionEventHandler(
Application_ThreadException);
这些异常由我的EntLibHelper
静默处理,该助手公开了一个很好的方法来处理未处理的异常。
private static void Application_ThreadException(object sender,
System.Threading.ThreadExceptionEventArgs e)
{
EntLibHelper.UnhandledException(e.Exception);
}
private static void CurrentDomain_UnhandledException(object sender,
UnhandledExceptionEventArgs e)
{
if( e.ExceptionObject is Exception )
EntLibHelper.UnhandledException(e.ExceptionObject as Exception);
}
EntLibHelper
会查询配置文件以决定如何处理未处理的异常。您可以在配置文件中指定如何处理未处理异常类别中的异常。如果您指定重新抛出它,应用程序将关闭。但如果您指定捕获它,它会将异常发送到错误报告引擎,然后发送到跟踪系统。因此,您将看到错误报告窗口弹出,通知我有关此错误的信息。这对您和我来说都是零麻烦。
internal static void UnhandledException(Exception x)
{
// An unhandled exception occured
// somewhere in our application. Let
// the 'Global Policy' handler have
// a try at handling it.
try
{
bool rethrow = ExceptionPolicy.HandleException(x,
"Unhandled Exception");
if (rethrow)
{
// Something has gone very
// wrong - exit the application.
System.Windows.Forms.Application.Exit();
}
else
{
ErrorReportForm.QueueErrorReport(
new ErrorReportItem( x.Message, x.ToString() ) );
}
}
...
...
博客功能
RSS Feeder 不仅是 RSS 聚合器,而且还是一个同样强大的博客工具。它为您提供了一个漂亮的 Outlook 2003 风格的用户界面,可以处理多个博客。
您可以创建一到多个博客账户,撰写博文并保存以备日后发布。当您发送一篇博文进行发布时,它会进入一个发送队列,该队列由发送/接收模块收集。如果发布博文时出现任何问题,您可以返回并打开该博文查看错误报告。
XMLRPC
大多数常用的博客引擎都使用 XML RPC,特别是那些使用 PHP 开发的。其中一些是 WordPress、B2Evolution、Drupal 等。XML RPC 类似于 SOAP,其中方法调用和参数被序列化为 XML。您可以通过 Google 搜索 XMLRPC 或访问此网站了解更多关于 XML RPC 的信息。
大多数博客引擎都支持一个定义明确的 API,称为 MetaWeblogAPI
。该 API 使用 XMLRPC 调用一组固定的 Web 方法。尽管 API 是固定的,但方法名称在不同的博客引擎之间有所不同。因此,您无法真正编写一个适用于所有支持此 API 的博客引擎的通用 MetaWeblogAPI
代码。我认为迫切需要标准化一个被普遍接受的方法和参数列表,简而言之,就是为所有博客引擎提供一个固定的接口。例如,如果您正在为 B2Evolution 写博客,您需要使用以下方法:
b2.getCategories
:获取博客的分类。b2.newPost
:创建新博文。
另一方面,对于支持相同 MetaWeblogAPI
的 WordPress,方法名称是:
metaWeblog.getCategories
metaWeblog.newPost
这使得实现一个可以通用适用于所有博客的博客库变得困难。然而,RSS Feeder 在这方面做得相当好。目前它支持几乎所有基于 PHP 的博客引擎以及 .Text 和 Community Server 等著名的基于 .NET 的博客引擎。
编辑环境
编辑环境已尽可能丰富,同时保持博客引擎对格式的限制。并非所有 HTML 格式都得到博客引擎的支持。大多数博客引擎会剥离 style
属性和样式表。有些不允许使用 <font>
标签。有些则禁止在 <p>
标签内使用 <div>
。因此,我创建的编辑器或多或少地包含足够多的格式工具,可以满足所有需求。
技巧 6. 便利的工具栏处理
当您有一个包含 30 到 40 个按钮的大型工具栏时,为处理每个按钮的点击事件编写代码会变得很困难。最终,您要么为每个按钮编写点击处理程序,要么编写一个工具栏级别的处理程序,其中包含一个大型的 if else
或 switch case
来确定哪个按钮被点击。
与此不同,我尝试了一种便捷的方法。每个按钮在其标签中都包含一个方法名。例如,保存按钮的标签值设置为 $SavePost
。用户控件有一个名为 SavePost
的 public
方法。在工具栏的点击处理程序内部,我使用了以下代码:
private void DesignSandBar_ButtonClick(object sender,
TD.SandBar.ToolBarItemEventArgs e)
{
// Get the tag which contains the
// function name to be invoked
string name = e.Item.Tag as string;
if( null == name || 0 == name.Length ) return;
// if self call, call the function on self
if( name.StartsWith("$") )
{
MethodInfo method =
this.GetType().GetMethod(name.TrimStart('$'));
method.Invoke( this, null );
}
else
{
// The function is on the editor component
MethodInfo [] methods =
this._Editor.GetType().GetMethods( );
// Call the method which has no parameter
foreach( MethodInfo method in methods )
if( method.Name == name &&
0 == method.GetParameters().Length )
{
try
{
method.Invoke( this._Editor, null );
}
catch( Exception x )
{
Debug.WriteLine( x );
}
}
}
}
它使用反射来查找用户控件上与标签中指定的名称匹配的方法,并调用该方法。因此,您无需编写点击处理程序或 switch
块来确定哪个按钮被点击以及需要调用哪个方法。
此外,如果标签不以 $ 符号开头,则表示需要调用 DHTML 编辑器控件上的方法,用户在该控件中编写博文的正文。DHTML 编辑器控件公开了许多格式化方法,因此它们直接映射到工具栏按钮。
DHTML 编辑器控件
您看到的编辑器是 Internet Explorer 随附的 DHTML 编辑器控件。这是 Microsoft 在其大多数产品中使用的控件,包括 Visual Studio 的 HTML 设计器窗口。您可以在此路径找到该控件:
C:\Program Files\Common Files\Microsoft Shared\Triedit\dhtmled.ocx
使用此控件相当困难,因为其提供的几乎所有功能都通过一个名为 ExecCommand
的方法实现。您可以在 MSDN 上找到该方法的参考。此函数允许您控制编辑器的行为,更改格式样式,显示常用对话框,如查找、链接、图像等。例如,您可以使用以下命令将编辑器设置为粗体模式:
Object nullArg = null;
editorControl.ExecCommand( DHTMLEDITCMDID.DECMD_BOLD,
OLECMDEXECOPT.OLECMDEXECOPT_DODEFAULT, ref nullArg );
由于使用此控件需要您记住所有函数名称和参数,因此需要一个方便的包装器。这就是源代码中提供的 HtmlEditor
控件的作用。它将 DHTML 编辑器的几乎所有功能公开为方便的公共方法和属性,例如 SetBold
、SelectedText
、OrderList
等。由于该包装器是一个 .NET 类,它扩展了 DHTML 编辑器控件,您可以轻松地创建它的新实例并将其放置在容器中。以下代码示例演示了如何创建新编辑器并将其放置在 Panel
中:
private void CreateEditor()
{
_Editor = new HtmlEditor();
_Editor.Dock = DockStyle.Fill;
_Editor.HandleCreated +=
new EventHandler(_Editor_HandleCreated);
editorPanel.Controls.Add( _Editor );
}
您需要注意的一个重要问题是,只有在句柄创建后才能使用此组件,也就是说,IsHandleCreated
属性为 true
。如果您想在控件或窗体的 Load
事件中使用此组件,则会失败。编辑器在 Load
事件触发时无法正常加载。由于某种原因,它需要很长时间才能正确初始化。因此,您需要在控件的 HandleCreated
事件上执行任何需要在加载时执行的任务。同时,请记住在使用此控件之前始终检查 IsHandleCreated
属性。例如:
if( this._Editor.IsHandleCreated )
{
this._Editor.NewDocument();
}
UI
您可能想知道我是如何创建用户界面的。它是使用在 Divelements Ltd 找到的极好的免费控件开发的。我是他们工具的忠实粉丝,并且在我另一个项目 Smart UML 中也使用了它们。
结论
我试图通过实现我从网络上收集到的所有最佳实践,使 RSS Feeder 成为一个理想的 Smart Client 应用程序。这个应用程序是 Enterprise Library 在实际应用中使用的良好范例。它还使用了 Application Updater Block 2.0,该模块的实现非常痛苦。希望您觉得这篇文章是桌面应用程序开发基础知识的良好来源。虽然这个应用程序的开发和发布只有一个月的时间,但我希望它已经相当稳定且功能丰富,可以成为您日常生活的一部分。我期待收到您的反馈,这将指导我在未来改进它,并使其成为提要聚合和博客的最佳工具。