Visual Studio .NET 的博客阅读器插件






4.81/5 (37投票s)
2004年4月20日
26分钟阅读

227809

1553
一个集成到 Visual Studio 中的博客阅读器。它显示博客列表、博客条目以及你尚未阅读的条目。
引言
作为一名 C# 开发人员,我大部分时间都在 Visual Studio 中工作。因此,每当我发现某个日常任务需要我在其他工具中完成时,我通常会尝试通过创建插件的方式将其集成到 VS.NET 中。我每周喜欢做的一件事是阅读微软的优秀员工撰写的各种博客(您可以在 http://blogs.gotdotnet.com 找到微软博客的列表)。但是,随着我阅读的博客越来越多,手动查看每个博客以查看作者是否有新内容变得很麻烦。
所以我决定创建一个 VS.NET 插件来帮我完成这项工作。当插件加载时,它会下载我订阅的所有博客的最新 RSS 提要,并显示每个博客每个条目的标题和发布日期。该插件会存储您上次查看每个博客条目的日期,并用红色突出显示所有新条目。您可以通过双击条目查看单个博客条目,可以通过右键单击条目并点击“查看条目评论”查看该条目的所有评论,或者可以通过右键单击条目并点击“查看整个博客”打开博客主页。该插件的用户界面利用 Visual Studio .NET 窗口对象,因此我可以获得命令窗口或解决方案资源管理器窗口等其他窗口所具有的停靠和固定功能。
文章涵盖和未涵盖的内容
“如何创建插件”这个话题已经被彻底讨论过了,现在已经成了老生常谈,所以我不想在这篇文章中重点关注它。首先,我将介绍该工具的功能和设计。然后,我将简要概述 RSS 是什么以及 RSS 2.0 版本的 XML 格式。之后,我将讨论我在编写该工具时使用的一些 .NET 中更有趣的领域,包括 IsolatedStorage 命名空间、从互联网下载数据、调用多个异步委托(以及在 VS.NET 插件中同步它们的挑战),然后是一些更高级的 Visual Studio 插件功能,例如使用 Globals
对象存储应用程序设置,以及创建集成到 VS.NET 中的自定义窗口(例如监视或解决方案资源管理器窗口)。
博客阅读器设计
博客阅读器工具的设计相当简单。我创建了两个实体类,分别名为 Blog
和 BlogEntry
,它们之间存在一对多的关系。这两个类,不出所料,分别代表一个博客和博客中的一个条目。插件持有一个 ArrayList
的 Blog
对象,每个 Blog
对象都有一个 ArrayList
的 BlogEntry
对象。我还编写了一个名为 BlogRetriever
的辅助类,其唯一目的是连接到博客的 URL 并下载 RSS XML 内容。
应用程序启动过程首先从应用程序数据文件(通过 IsolatedStorage)读取每个博客 URL。然后,它连接到互联网并下载每个博客的 RSS 内容,并解析 XML,创建并填充 Blog
和 BlogEntry
对象,将它们添加到相应的 ArrayList
中。
当我第一次编写这个工具时,我向我的订阅博客列表中添加了 10 个不同的博客,但是当插件启动时,加载所有数据并创建 Blog
和 BlogEntry
对象花费了很长时间。所以我决定尝试为每个博客使用自己的线程来下载和填充数据。我通过使用异步委托成功地做到了这一点,这大大缩短了插件的加载时间(我稍后会详细介绍调用异步委托)。
那么 RSS 是什么?
RSS 代表“Really Simple Syndication”(真正简单的聚合),它是一种 XML 格式,最初主要用于聚合新闻网站的新闻内容,但现在已成为许多博客网站发布其博客内容的标准。我不会涵盖 RSS 的完整(或接近完整)2.0 规范。您可以自行在 http://blogs.law.harvard.edu/tech/rss 阅读。下面显示了 RSS 提要的示例。
RSS 的 XML 格式相当简单易懂。有一个根 <RSS> 元素,它包围着整个文档。紧接着这个元素下面是一个 <CHANNEL> 元素,其中包含描述整个文档主题的子元素,例如标题、链接和描述,以及其他可选的子元素。
在 <CHANNEL> 元素下,有多个 <ITEM> 元素,每个条目或“故事”对应一个。此元素将包含几个描述该单独故事的子元素,例如标题、发布日期、描述、作者和评论等。
博客阅读器工具只查找 <CHANNEL> 和 <ITEM> 元素中包含的必要子集,以显示所需信息。
来自 MSDN 的 RSS 块<rss version="2.0">
<channel>
<title>MSDN: .NET Framework and CLR</title>
<link>http://msdn.microsoft.com/netframework/</link>
<description>The latest information for developers on
the Microsoft .NET Framework and Common Language Runtime
(CLR).</description>
<language>en-us</language>
<ttl>1440</ttl>
<item>
<title>Creating a Product Search Application Using the
eBay SDK and Visual Basic .NET</title>
<pubDate>Fri, 26 Mar 2004 08:00:00 GMT</pubDate>
<description>Learn how to create a .NET Windows
Forms application that searches eBay's product database
using the eBay SDK.</description>
<link>http://msdn.microsoft.com/vbasic/default.aspx?pull
=/library/en-us/dv_vstechart/html/ebaySearchBar.asp</link>
</item>
<item>
<title>.NET Enterprise Services Performance</title>
<pubDate>Fri, 26 Mar 2004 08:00:00 GMT</pubDate>
<description>See the performance of native COM+ and
.NET Enterprise Services components applied to different activation and
calling patterns, and get guidelines to make .NET Enterprise Services
components execute just as quickly as C++ COM+ components.</description>
<link>http://msdn.microsoft.com/netframework/default.aspx
?pull=/library/en-us/dncomser/html/entsvcperf.asp</link>
</item>
</channel>
</rss>
下载 RSS 数据
使用 .NET 从 Internet 下载数据非常容易,只需 4 行代码即可完成,如下所示。
HttpWebRequest webreq = (HttpWebRequest)WebRequest.Create(blog.Link);
webreq.Credentials = new NetworkCredential(
Settings.UserName, Settings.Password);
HttpWebResponse webresp = (HttpWebResponse)webreq.GetResponse();
StreamReader rss = new StreamReader(
webresp.GetResponseStream(), Encoding.ASCII);
if (rss != null)
{
blog.ParseRSS(rss.BaseStream); //Close the stream from the server
rss.Close(); }
首先,通过调用静态的 WebRequest.Create()
方法来创建一个 HttpWebRequest
对象。该方法可以接受您要调用的 URL 字符串,或者一个 System.Uri
类的实例。如果您位于防火墙后面并且需要进行身份验证才能访问 Internet,您还需要创建一个 NetworkCredential
类的实例,并将其分配给 HttpWebRequest.Credentials
属性。接下来,您调用 HttpWebRequest.GetResponse()
方法,该方法将调用 URL 并返回一个 WebResponse
对象,您可以将其转换为 HttpWebResponse
。从该对象,您可以调用 HttpWebResponse.GetResponseStream()
方法以访问您请求的数据流。
我通常使用 StreamReader
,它是一个仅向前、只读的底层流读取器(尽管您可以通过 StreamReader.BaseStream
属性访问和完全操作底层流)。在这种情况下,我将流传递到每个博客的 Blog.ParseRSS()
方法中,该方法将从流中提取 XML 并解析 RSS 内容(我将在下一节讨论)。
一旦您完成了 HttpWebResponse
的流操作,您应该关闭它,因为如果不关闭,您的应用程序可能会耗尽 http 连接。为此,您可以调用 HttpWebResponse.Close()
或 StreamReader.Close()
。由于它们都使用相同的底层流实例,因此调用哪个都没有关系。调用两者也无妨,但并非必要。
如果您的服务器位于代理服务器后面,那么您可能还需要为 HttpWebRequest
对象提供代理信息。这可以通过以下三种方式之一完成。首先,如果您在 IE Internet 选项设置中存储代理信息,那么 .NET 默认情况下应该会获取并使用这些设置。它通过 machine.config 文件来实现这一点。在 machine.config 文件中有一个
如果您未在 IE Internet 选项中存储代理信息,则可以将“usesystemdefault”属性设置为 false,然后提供可选的“proxyaddress”属性并定义代理服务器的 Uri,如下所示。
<defaultProxy>
<proxy usesystemdefault="false" proxyaddress="
http://myproxyserver:80" bypassonlocal="true"/>
</defaultProxy>
定义代理设置的最后一种方法是在代码中进行。就在您调用 HttpWebRequest.GetResponse()
之前,您可以使用 WebProxy
对象的实例设置 HttpWebRequest.Proxy
属性,如下所示。
webreq.Proxy = new WebProxy("http://myproxyserver:80", true);
解析 RSS XML
一旦你有了 RSS XML 数据,你就需要能够解析它并提取你需要的内容数据。我尝试了几种不同的方法来解析 RSS 数据;通过 DOM (XMLDocument
) 遍历节点,使用 XPath 查询每个所需的元素,以及使用 XmlTextReader
在仅向前流中读取 XML。使用 DOM 绝对是最容易使用的,但绝对不是最快的。XPath 只是一个麻烦,当你需要读取 XML 文档中的大多数元素时,它也不是很快。我最终选择了 XmlTextReader
,因为它速度快且相对易用。XmlTextReader
是一个仅向前、只读、基于流的 XML 解析器。一旦 XML 加载到读取器中,你就可以通过调用 XmlTextReader.Read()
方法开始读取。如果成功读取了文档中的下一个元素,它将返回 true。这使得它非常适合用作 while()
循环的条件,如下所示。
public void ParseRSS(Stream rss)
{
XmlTextReader xml = null;
xml = new XmlTextReader(rss);
while (xml.Read())
{ if (xml.Depth == 2 && xml.Name == "title" &&
xml.IsStartElement("title"))
{
xml.Read();
this.title = xml.Value;
continue;
}
if (xml.Depth == 2 && xml.Name == "description" &&
xml.IsStartElement("description"))
{
xml.Read();
this.description = xml.Value;
continue;
}
.
.
.
}
}
XmlTextReader 用起来有点奇怪。每次调用 Read() 方法时,它都会向前移动到下一个元素,这听起来很简单。让我们以以下 XML 块为例
<one>
<two>something</two>
</one>
我想要做的是从开放和关闭的 <two> 标签之间获取值“something”。第一次调用 Read() 时,读取器移动到第一个元素 <one>。下次调用 Read()
时,活动元素是 <two>。你可能会认为现在 <two> 处于活动状态,你可以直接调用读取器的 Value 属性,但是如果你这样做,你只会得到一个空字符串。使用 XmlTextReader
时要记住的是,它会字面上移动到下一个元素。开放标签和关闭标签都是元素,但这两个标签之间的文本也是一个元素。因此,当 <two> 是活动元素时,你必须再调用 Read()
一次才能获取文本“something”。现在,当你再次调用 Read()
时,活动元素的标题又是 <two>。因此,在查找开放元素时,你还需要检查 XmlTextReader.IsStartElement()
方法。如果元素是关闭元素,则此方法将返回 false。你可以在上面的代码中,对于每个“if”语句看到这一点。
另一件需要记住的事情是,当您遍历一个 XML 文档(例如 RSS)时,其中包含多个同名元素,例如 <title>,但这些元素在 XML 文档中的层级不同。如果您查看文章开头的 RSS XML 示例,您会注意到“<channel>”和“<item>”元素都包含名为 <title> 和 <description> 的子元素。
XmlTextReader
不会为您提供当前元素的完全限定路径,但它会为您提供在 XML 文档中的深度。这是您在尝试解析 RSS 文档时需要检查的另一件事。否则,您可能会在错误的位置提取错误的信息。我在上面的代码示例中也对此进行了演示。
存储应用程序数据:隔离存储
大多数应用程序,至少是优秀的、用户友好的应用程序,都提供一种方法来存储数据和用户对应用程序所需状态的偏好。有许多方法可以做到这一点,但微软专门为此任务编写了一个鲜为人知的命名空间;它叫做 System.IO.IsolatedStorage
。
隔离存储是一种非常简单的方式,可以存储应用程序数据并使其与其他应用程序及其数据文件隔离。您无需担心文件存储在哪个目录中,因为 .NET 会为您处理所有这些麻烦的细节。隔离存储默认基于用户的身份和程序集名称。这意味着,如果同一个用户打开两个不同的应用程序,并且每个应用程序通过隔离存储读取和写入自己的数据文件,那么这两个数据文件彼此隔离,因为尽管这两个应用程序共享相同的用户,但它们具有不同的程序集名称。
目前,隔离存储基于 C:\Documents and Settings 下的文件夹结构,但每个操作系统版本略有不同。例如,在我的 Windows 2000 服务器上,根隔离存储文件夹位于
C:\Documents and Settings\ME\Local Settings\Application Data\IsolatedStorage
当您使用隔离存储创建文件时,将在此根文件夹下创建额外的文件夹,每个文件夹名称(可能)基于隔离存储范围(用户名、AppDomain、程序集名称等)的哈希值。例如,我为博客阅读器创建的数据文件的完整路径是
C:\Documents and Settings\ME\Local Settings\Application Data\IsolatedStorage\fsaap1nt.yj0\wpdgvfj1.plj\ Url.rqiojpb01nxfmst5zbyphw4tyzj0qs5w\ AssemFiles\BlogReader.dat
这些文件夹各自代表什么?我不知道,也不需要知道。这就是隔离存储的魅力所在;您无需担心细节。只需创建一个文件并写入其中,瞧,就这么简单!
那么,让我们从一个创建和写入隔离存储的示例开始。下面的代码展示了博客阅读器如何遍历每个保存的博客,并将博客 URL 和上次检查日期存储到隔离存储文件中。
using (IsolatedStorageFile file =
IsolatedStorageFile.GetUserStoreForAssembly())
{
using (StreamWriter stream = new StreamWriter(
new IsolatedStorageFileStream("BlogReader.dat",
FileMode.Create, file)))
{{
foreach (Blog blog in blogs)
{ stream.WriteLine(blog.Link + "|~|" + SerializeDateTime(blog.LastChecked));
}
stream.Flush();
stream.Close();
}}
}
我们首先创建一个 IsolatedStorageFile
对象。还记得我谈论过基于隔离文件范围的文件存储吗?嗯,您可以指定要创建文件所属的范围,或者只使用 IsolatedStorageFile.GetUserStoreForAssembly()
返回的程序集和用户的静态定义范围。
一旦我们有了文件对象,我们就会基于 IsolatedStorageFile
创建一个 IsolatedStorageFileStream
对象。我们给文件命名为“BlogReader.dat”,然后告诉隔离存储创建一个新文件。如果文件已经存在,那么隔离存储将直接用新文件覆盖它。然后我们创建一个 StreamWriter
,以便于向文件流写入数据。
一旦我们有了 StreamWriter
,我们就可以使用 StreamWriter.WriteLine()
方法写入博客 URL 和上次检查的日期。在每个博客 URL 都写入流之后,我们将流的内容刷新到文件中,然后关闭流。
从隔离存储中读取数据同样容易。下面是博客阅读器启动时读取博客 URL 的方式。
using (IsolatedStorageFile file =
IsolatedStorageFile.GetUserStoreForAssembly())
{
string[] files = file.GetFileNames("BlogReader.dat");
if (files.Length > 0 && files[0] == "BlogReader.dat"))
{
using (StreamReader stream = new StreamReader(
new IsolatedStorageFileStream("BlogReader.dat",
FileMode.Open, file)))
{ while (stream.Peek() > -1)
{
string line = stream.ReadLine();
int index = line.IndexOf("|~|");
Blog blog = new Blog(
line.Substring(0, index), UnSerializeDateTime(line.Substring(index + 3)));
blogs.Add(blog); }
}
}
}
首先,我们像保存数据时一样,创建一个 IsolatedStorageFile
的实例。现在,在打开数据文件之前,我们需要检查它是否存在。我认为这是隔离存储的一个小缺点。它没有 FileExists
方法或任何类似的方法。微软给我们提供的最接近的方法是 IsolatedStorageFile.GetFileNames()
方法。所以我们首先需要查询隔离存储中是否有与我们文件名匹配的文件。如果找到一个,那么我们基于文件名、FileMode.Open
的文件模式以及 IsolatedStorageFile
的实例创建一个 IsolatedStorageFileStream
的实例。然后我们基于 IsolatedStorageFileStream
对象创建一个 StreamReader
对象,并遍历数据文件中的每一行并创建一个 Blog
对象。
注意,我从未指定文件夹名称?这就是我如此喜欢隔离存储的原因,它为您处理文件路径,并将数据文件存储在一个相当偏僻的地方。现在请记住一件事。仅仅因为一个应用程序不能通过隔离存储 API 读取另一个应用程序的隔离存储文件,这并不意味着您不能通过 Windows 资源管理器导航到它并用记事本打开文件。因此,如果您要存储任何“敏感”信息,您仍然需要对其进行加密,就像您对任何其他数据存储一样。
通过 Globals 对象存储插件偏好设置
隔离存储很棒,但如果您需要存储小块数据,例如 Visual Studio 插件的用户偏好设置,那么 Visual Studio 自动化模型提供了一种内置的方式来处理这个问题:Globals
对象。Globals
对象提供了一种在 Visual Studio 会话之间,在项目、解决方案或 IDE 级别持久化用户信息的方法。
如果您编写的插件具有在每个独立项目或解决方案中不同的用户偏好设置,这将非常有用。Solution.Globals
属性公开了一个 Globals
对象,它会将数据存储在 .sln 文件中。而 Project.Globals
属性公开了一个 Globals
对象,它会将数据存储在 .csproj 文件中。
但是,如果您的插件要存储对所有项目和解决方案都全局的数据,那么 DTE.Globals
对象是首选。写入此对象的数据存储在一个名为 ExtGlobals.dat 的文件中,该文件可以使用记事本轻松读取,并且位于
C:\Documents and Settings\ME\Application Data\Microsoft\VisualStudio\7.1\ExtGlobals.dat
写入和读取 Globals
对象比隔离存储容易得多。最好将 Globals
对象视为一个哈希表。您通过其索引器读取和写入变量,使用与哈希表类相同的键/值模式。您必须记住的唯一一件事是,如果您在 Globals 对象中创建了一个新变量,您必须在后面跟着 Globals.set_VariablePersists(“变量名”, true)
方法。这将告诉 Visual Studio 将变量持久化到文件中。下面是使用 DTE.Globals
对象存储三个变量的示例。
//where vsObj is a DTE objecttt
vsObj.Globals["PW"] = Settings.Password;
vsObj.Globals["UN"] = Settings.UserName;
vsObj.Globals["OPEN"] = Settings.OpenBrowserInNewWindow.ToString();
vsObj.Globals.set_VariablePersists("PW", true);
vsObj.Globals.set_VariablePersists("UN", true);
vsObj.Globals.set_VariablePersists("OPEN", true);
从 Globals
对象读取同样容易。唯一需要记住的是,在实际尝试访问之前,请检查 Globals
对象是否包含您请求的变量(否则会抛出异常)。下面是读取 Globals
对象中三个变量的示例。
//where vsObj is a DTE object
if (vsObj.Globals.get_VariableExists("PW"))
Settings.Password = vsObj.Globals["PW"].ToString();
if (vsObj.Globals.get_VariableExists("UN"))
Settings.UserName = vsObj.Globals["UN"].ToString();
if (vsObj.Globals.get_VariableExists("OPEN"))
Settings.OpenBrowserInNewWindow =
bool.Parse(vsObj.Globals["OPEN"].ToString());
再次提醒安全注意事项。上面我展示的代码保存了用户名和密码,仅用于演示目的。但解决方案、项目和 ExtGlobals.dat 文件都易于读取和理解。在实际应用中,我不会在未加密的情况下存储任何如此有价值的信息。
在插件中使用异步委托
在文章开头,我提到为了获得较快的插件加载时间,我需要启动多个线程来异步下载每个 RSS 提要。如果您在一个过程中有几个必须完成的独立步骤才能继续程序,那么异步调用委托是一个很好的方法。下图演示了我的意思。
然而,需要记住的是,如果您的服务器只有一个处理器,那么创建后台线程以异步处理多个步骤并不会真正让您的应用程序运行得更快,因为您必须在程序继续之前同步。这是因为所有线程都必须共享同一个处理器,所以实际上,在程序可以继续之前,需要在处理器上完成相同数量的工作。而且,由于调用多个委托、创建新线程(如果需要,异步委托从 ThreadPool
获取线程)、线程之间进行的所有额外上下文切换以及同步的努力,程序很可能会运行得更慢。
我将假设您知道如何以同步方式使用委托的基础知识。异步调用委托同样容易。一旦您有了委托实例,就通过调用其 myDelegate.BeginInvoke()
方法来调用它。有几种方法可以启动您的异步委托,然后同步回来。如果您的程序需要“即发即弃”模型,那么只需调用 BeginInvoke()
并像往常一样继续。一个线程将从线程池中取出以执行委托,然后在委托调用的方法完成后返回到池中。
如果您需要从委托调用的方法中收集返回值,那么主要有两种方法。第一种方法是提供一个 AsyncCallback
委托实例作为 BeginInvoke()
方法的倒数第二个参数。当异步委托线程执行完成时,将调用此回调委托。您还需要将一个“状态”对象作为 BeginInvoke()
方法的最后一个参数传入。此“状态”对象将存储在从 BeginInvoke()
方法返回的 IAsyncResult
实例中。此“状态”对象也将由运行时存储,因此当您调用委托的 EndInvoke()
方法时,它可以返回正确的对象。
让我们举个例子。假设您的程序启动了 3 个不同的异步委托调用,每个调用都基于相同的委托类,并且返回类型为字符串。当第一个执行完成的委托完成并调用回调方法时,运行时将与调用 BeginInvoke()
时返回的 IAsyncResult
对象的相同实例传递给回调方法。然后,您调用委托的 EndInvoke()
方法,传入 IAsyncResult
实例。运行时使用存储在 IAsyncResult
中的状态对象来确定要返回哪个字符串。我将在下面演示这一点。
private delegate string DoStuffEventHandler(int one, int two);
DoStuffEventHandler do1, do2, do3;
private void DoAsyncDelegate()
{
//create 3 new delegate instances
do1 = new DoStuffEventHandler(DoStuff);
do2 = new DoStuffEventHandler(DoStuff);
do3 = new DoStuffEventHandler(DoStuff);
//call all 3 delegates asynchronously
IAsyncResult result1 = do1.BeginInvoke(1, 2,
new AsyncCallback(DoStuffCallback), "first");
IAsyncResult result2 = do2.BeginInvoke(3, 4,
new AsyncCallback(DoStuffCallback), "second");
IAsyncResult result3 = do3.BeginInvoke(5, 6,
new AsyncCallback(DoStuffCallback), "third");
}
//This is the method the async delegate calls
private string DoStuff(int one, int two)
{
return ((int)(one + two)).ToString();
}
//This is the callback that the async delegate will call
//when it is finished executing DoStuff
private void DoStuffCallback(IAsyncResult result)
{
//Use the AsyncState to figure out which
//textbox to put the return value of EndInvoke
switch (result.AsyncState.ToString())
{
case "first":
textBox1.Text = do1.EndInvoke(result);
break;
case "second":
textBox2.Text = do2.EndInvoke(result);
break;
case "third":
textBox3.Text = do3.EndInvoke(result);
break;
}
}
从异步委托调用获取返回值的第二种方法是使用从 BeginInvoke()
返回的 IAsyncResult.WaitHandle
属性,并对每个 IAsyncResult
使用实例方法 WaitHandle.WaitOne()
,或者对所有 IAsyncResult.WaitHandle
对象的数组使用静态方法 WaitHandle.Wait.All()
。然后,当进程从 WaitOne()
或 WaitAll()
方法调用返回时,您可以安全地调用委托的 EndInvoke
,遵循我上面概述的模式。
还有另一种方法可以获取异步委托调用的返回类型。您也可以在循环中检查 IAsyncResult.IsCompleted
属性,以查看异步调用是否已完成。一旦 IsCompleted
返回 true,您就可以调用 EndInvoke()
来获取返回值。通常不鼓励这样做,因为它会占用更多的处理器资源,并且被认为是糟糕的设计。
在 BlogReader 的例子中,我将 Blog
类的一个实例传递给委托,并且新线程所做的所有更改都作用于 Blog
实例。因为我传递给委托的只是对现有对象的引用,所以我不需要担心委托通过回调方法向我返回任何内容。Blog
实例的另一个引用存储在我的博客 ArrayList
中,这就是为什么我不需要回调来将已填充的 Blog
返回给主 UI 线程。
因为应用程序的下一步,在下载 RSS 数据并构建我的 Blog 对象图之后,是显示 Blog
对象的内容,所以我必须等到所有线程都完成工作。否则,我将没有任何东西可以显示给用户。还记得我谈到 BeginInvoke()
的返回类型是 IAsyncResult
类型的对象吗?嗯,为了实现我们想要的功能,我们需要从每个 BeginInvoke()
中收集每个 IAsyncResult.WaitHandle
对象,并将它们加载到一个数组中。一旦我们有了这个 WaitHandles 数组,通常我们可以调用静态的 WaitHandle.WaitAll(arrayOfWaitHandles)
,它会阻塞调用线程,直到所有异步委托线程都完成工作。但是 Visual Studio.NET 是一个 STA(单线程单元)线程模型,它不支持 WaitHandle.WaitAll()
功能。所以我不得不遍历数组中的每个 WaitHandle
对象并调用每个对象的 WaitHandle.WaitOne()
实例方法。数组中第一个调用 WaitOne()
的 WaitHandle
会导致主 UI 线程暂停一两秒钟,因为它仍在执行,但其余的 WaitOne()
方法调用会很快返回,因为到那时它们应该已经完成执行了。这如下所示。
WaitHandle[] handles = new WaitHandle[blogs.Count];
int count = 0;
foreach (Blog blog in blogs)
{
BlogRetriever blogGet = new BlogRetriever();
//Create a new delegate to be executed asynchronously
GetBlogDataEventHandler blogDelegate =
new GetBlogDataEventHandler(blogGet.GetBlogData);
//Invoke the delegate asynchronously
//and collect all the WaitHandle object in an array
handles[count++] = blogDelegate.BeginInvoke(
blog, null, null).AsyncWaitHandle;
}
//Loop through each handle and call wait one on each.
//this isnt too bad, but WaitAll would be better
foreach (WaitHandle handle in handles)
handle.WaitOne();
Shim 控件,用于在 Visual Studio .NET 中创建窗口
在插件中创建自己的窗体是一种简单的方法,可以创建集成到 Visual Studio 中的工具,但其外观并不“专业”。大多数专业的插件都使用 Visual Studio 窗口对象来承载其工具。窗口对象用于诸如类视图、解决方案资源管理器、监视窗口、断点窗口、任务列表等浮动/停靠的 Visual Studio 窗口。这些都是 Visual Studio 窗口对象。
Visual Studio Window 对象本身功能不多。它只是承载一个 ActiveX 控件,这正是您使用 VS 工具(如 Command 或 Class View 窗口)时所使用的。在 C# 中创建自己的工具窗口的问题在于,Window 对象只能承载 ActiveX 控件。据我所知,您无法使用 C# 或 VB.NET 创建 ActiveX 控件。但幸运的是,有人决定创建一个可以承载 .NET 用户控件的 C++ ActiveX 控件。有一个名为“shim”控件的免费控件,您可以从 Yahoo Visual Studio Add-In 新闻组(http://groups.yahoo.com/group/vsnetaddin/)下载。您必须注册该组,但进入后,请转到“Files / Visual Studio Shim Controls”部分。有两个 shim 控件供您选择,两者都不受支持。一个来自 Microsoft,另一个由 Xtreme Simplicity 编写。对于此工具和文章,我选择了 Xtreme Simplicity 的那个。
创建新工具窗口的 API 是方法 Windows.CreateToolWindow()
。参数如下所示。第一个参数是正在运行的插件实例。此对象被传递到插件的 OnConnection
方法中,您应该将其存储在类级别变量中,以便在此处使用。第二个参数是您正在使用的 ActiveX shim 控件的 ProgID。第三个参数是工具窗口的标题。第四个参数是您手动创建的 Guid,它成为 DTE.Windows
集合中新窗口的唯一标识符。第四个参数,通常,将是窗口将承载的 ActiveX 控件的实例。但是使用 shim 控件,您只需传入对 null 对象的引用。
public Window CreateToolWindow(
AddIn AddInInst,
string ProgID,
string Caption,
string GuidPosition,
ref object DocObj
);
我的假设是,如果第五个参数为 null,那么 Visual Studio 将创建一个 ActiveX 控件的新实例,因为在您将 null 对象传入 CreateToolWindow()
之后,它会突然成为 System.__ComObject
类型的对象,这是 .NET 告诉您大多数 COM VS 自动化对象是什么。
调用 CreateToolWindow()
后,您将对第五个参数(新创建的 ActiveX shim 控件实例)使用反射来调用其 HostUserControl()
方法。所有这些代码如下所示。请注意 InvokeMember
方法的最后一个参数。当您调用 HostUserControl
时,您传入一个仅包含您创建的 .NET 用户控件的对象数组。ActiveX shim 控件将获取此 .NET 用户控件,并在 ActiveX 控件内部显示该 .NET 用户控件。
object obj = null;
Window window = vsObj.Windows.CreateToolWindow(addIn,
"CSUserControlHost.CSUserControlHostCtl", caption, guid, ref obj);
window.Visible = true;
obj.GetType().InvokeMember("HostUserControl",
BindingFlags.InvokeMethod, null, obj, new object[]
{yourWindowToolDotNetControl});
这就是 Blog Reader 工具显示其用户界面的方式,通过 shim 控件承载我的用户控件。要查看所有执行此操作的代码,请查看源代码中的 Connect.CreateToolWindow()
。
窗口浮动时的成品
窗口停靠在底部时的成品
插件启动和关闭模式
正如我在文章开头所说,“如何创建插件”这篇文章已经被彻底讨论过了。所以我不想继续这个话题。但是,关于插件创建有一个方面经常被忽略。许多文章或示例插件在启动时会创建新的 CommandBar 和菜单项(Command 对象)。但是,当卸载插件时,它们不会执行任何清理操作。一个行为良好的插件在卸载时应该在图形上自行清理,否则你会得到一些无功能的孤立菜单项。正如你可以想象的那样,在你安装和卸载了第五个示例插件之后,这可能会变得相当烦人。
问题是 VS.NET 会保留你通过编程方式创建的任何新菜单,即使你关闭 VS 也是如此。所以,如果你的插件在卸载时没有手动从 VS 中删除 CommandBars 或菜单,那么你会留下一些没有任何功能的菜单项。
我将一种通用模式应用于所有插件的 Connect 类(我应该创建一个名为 IGoodBehavinAddIn
的接口,不是吗)。我不会明确地介绍代码,只是描述模式。您可以通读 Connect 类以查看我如何实现此模式。关键是我想让人们记住,插件的清理代码与设置代码同等重要。
当博客阅读器插件启动时,它首先检查 VS 中是否存在“博客阅读器”菜单项命令。如果它仍然存在,它将不会尝试创建一个新的,这将有助于加快插件的加载时间(以及防止 VS 拥有 10 个“博客阅读器”菜单项)。如果您的插件创建了多个 Command 和/或 CommandBar 对象,您只需检查一个对象。如果它仍然存在于 VS 中,那么您有 99% 的把握确定其余的也都存在。如果它们不存在,那么您必须以编程方式创建它们。
当 VS 关闭时,您的插件的 OnDisconnection
方法将被调用,并且会传入一个断开连接模式枚举。我喜欢做的是,只有当用户通过“插件管理器”手动卸载插件时,才将插件从 Visual Studio 中移除。您可以通过检查 ext_dm_UserClosed
的断开连接模式来确定这一点。如果 OnDisconnection
因任何其他原因被调用,我将保持插件加载不变。
在创建插件时,最后一点需要记住。默认情况下,当您使用 Visual Studio 的插件向导时为您创建的 MSI 项目,在每个 DLL 的属性窗口中,其多个 COM dll 依赖项都被标记为“Excluded = false”。这意味着 MSI 包在运行时将安装并注册 COM dll,并在卸载插件时取消注册并移除 COM dll。这可能非常糟糕!如果这些 DLL 从安装了 Visual Studio .NET 的服务器上被移除,Visual Studio .NET 将无法工作。这就是为什么您应该始终将向导自动添加到 MSI 包中的每个 DLL 的 Exclude 属性设置为 true。