Gadgets 的 .NET Interop – C# GMail 收件箱阅读器示例






4.84/5 (88投票s)
如何从 Vista 侧边栏小工具调用任何 .NET 代码
目录
介绍
自从编写我的侧边栏小工具介绍以来,我一直在努力解决小工具无法轻松完成 .NET 能做的事情的挫败感。我喜欢小工具的紧凑和简单,但我发现很难构建一个真正有用的小工具,仅仅是因为使用 JavaScript 没有真正的能力。不幸的是,对于 .NET 社区来说,小工具几乎纯粹依赖于 JavaScript。我很难将我对小工具的热情与它们的巨大局限性结合起来。
如果我能从小工具运行 .NET 代码就好了……
进入小工具 .NET Interop!
在本文中,我们将探讨如何构建小工具和 .NET 之间的互操作层,以便您可以从侧边栏小工具运行任何 .NET 代码。我们将通过构建一个 C# 项目来读取您的 GMail 收件箱来实现这一点。
COM 和 ActiveX
说小工具不能运行 .NET 代码是不公平的。事实是,从脚本语言创建 COM 对象实例非常容易。真正的问题是,必须注册所有代码以进行 COM 互操作非常不方便。这样做将要求您首先修改所有代码以使其与 COM 兼容。然后,您必须重新打包您的代码,并随每个小工具分发一个 MSI 文件,仅仅是为了安装和注册您的程序集(如果它将在小工具之间共享,可能还需要将其添加到 GAC)。这种变通方法不是一个现实的解决方案,特别是如果您已经有代码不想仅仅为了 COM 互操作而重写和打包。此外,您不能假设您的用户具有安装 COM 组件的知识或权限。
那么答案是什么?
无论如何,必须有一些 COM 组件到位;否则我们将永远无法克服 JavaScript 的局限性。一旦我们有了合适的 COM 层,我们将进入 GMail 部分(如果您熟悉 .NET 中的 COM,请参阅下文)。让我们从通过创建一个可用于加载任何 .NET 程序集的基本 COM 对象来创建解决方案的真正核心部分。有关 .NET COM 对象的更多详细信息,请参阅本文。
.NET COM 接口
这个想法很简单;创建一个小型、轻量级的 .NET COM 组件,它使用反射来加载任何程序集和类型。然后,可以直接从 JavaScript 调用该类型。让我们看看将完成大部分工作的“小工具适配器”的接口。
[ComVisible(true),
GuidAttribute("618ACBAF-B4BC-4165-8689-A0B7D7115B05"),
InterfaceType(ComInterfaceType.InterfaceIsDual)]
public interface IGadgetInterop
{
object LoadType(string assemblyFullPath, string className);
object LoadTypeWithParams(string assemblyFullPath, string className,
bool preserveParams);
void AddConstructorParam(object parameter);
void UnloadType(object typeToUnload);
}
实现的 Gadget Adapter 类只需要处理四种方法。需要注意的是,该接口有三个属性,允许我们将实现类公开为 COM 对象。我们需要四个方法来创建和调用托管代码中的任何类型。
小工具适配器
现在接口已定义,让我们看看这个接口的实际 Gadget Adapter 实现。我们将从类属性开始逐一分解。
[ComVisible(true),
GuidAttribute("89BB4535-5AE9-43a0-89C5-19B4697E5C5E"),
ProgId("GadgetInterop.GadgetAdapter"),
ClassInterface(ClassInterfaceType.None)]
public class GadgetAdapter : IGadgetInterop
{
...
}
这些属性与接口上的属性有一些不同。对于我们的目的而言,最重要的属性是“ProgId
”属性。此属性表示我们将用于通过 JavaScript 创建 ActiveX 对象的string
。现在GadgetAdapter
已正确修饰,下一步是加载程序集和创建类实例。AddConstructorParam
方法允许 JavaScript 代码添加将传递给类构造函数参数的值。这仅在需要使用带有一个或多个参数的构造函数加载 .NET 类型时才需要。
private ArrayList paramList = new ArrayList();
public void AddConstructorParam(object parameter)
{
paramList.Add(parameter);
}
下一个方法是所有神奇之处发生的地方。LoadTypeWithParams
方法有三个参数,允许加载任何 .NET 程序集。该方法接受程序集路径、要创建的类型以及用于处理构造函数参数处置的标志。
public object LoadTypeWithParams(string assemblyFullPath, string className,
bool preserveParams)
{
...
Assembly assembly = Assembly.LoadFile(assemblyFullPath);
object[] arguments = null;
if (paramList != null && paramList.Count > 0)
{
arguments = new object[paramList.Count];
paramList.CopyTo(arguments);
}
BindingFlags bindings = BindingFlags.CreateInstance |
BindingFlags.Instance |
BindingFlags.Public;
object loadedType = assembly.CreateInstance(className, false, bindings,
null, arguments, CultureInfo.InvariantCulture,
null);
...
return loadedType;
}
使用标准 .NET 反射,加载指定的程序集并创建输入类型的实例。该实例被返回,然后可以直接由 JavaScript 调用(稍后将详细介绍)。preserveParams
标志可防止在创建对象后清除构造函数参数。这仅在您使用相同的构造函数参数创建类的多个实例时才需要。
最后,由于我们处于 COM 世界中,我们必须小心自己进行对象处置。UnloadType
方法调用传入对象的处置,以实现优雅的清理。
public void UnloadType(object typeToUnload)
{
...
if (typeToUnload != null && typeToUnload is IDisposable)
{
(typeToUnload as IDisposable).Dispose();
typeToUnload = null;
}
catch { }
...
}
我选择的一个约定是,暴露给小工具的类必须实现IDisposable
,因此只有实现该接口的类型才能与示例代码一起使用。这就是互操作层的全部内容。它创建 .NET 对象并销毁 .NET 对象;不多不少。
运行时自动注册
现在我们有了一个可用的 COM 友好的 Gadget Adapter,但它如何注册呢?通常,您会依赖 MSI 安装程序来注册和 GAC 您的 COM 组件。请记住,这里的目标是在用户无需安装 MSI 的情况下在小工具中运行 .NET 代码。为了绕过 MIS(或RegAsm.exe),我们可以通过直接向注册表添加正确的值来“伪造”注册(感谢 Frederic Queudret 的这个想法)。GadgetInterop.js是一个 JavaScript 库,旨在促进 Gadget Adapter 注册(以及所有 COM 对象包装)。RegAsmInstall
JavaScript 方法获取有关 Gadget Adapter 互操作程序集的所有信息,并创建所有必要的注册表项来注册它。这一步的优点在于,任何小工具都可以在小工具首次执行时在运行时注册互操作层。
function RegAsmInstall(root, progId, cls, clsid, assembly, version, codebase)
{
var wshShell;
wshShell = new ActiveXObject("WScript.Shell");
wshShell.RegWrite(root + "\\Software\\Classes\\", progId);
wshShell.RegWrite(root + "\\Software\\Classes\\" + progId + "\\", cls);
wshShell.RegWrite(root + "\\Software\\Classes\\" + progId + \\CLSID\\,
clsid);
wshShell.RegWrite(root + "\\Software\\Classes\\CLSID\\" + clsid +
"\\", cls);
wshShell.RegWrite(root + "\\Software\\Classes\\CLSID\\" + clsid +
"\\InprocServer32\\", "mscoree.dll");
wshShell.RegWrite(root + "\\Software\\Classes\\CLSID\\" + clsid +
"\\InprocServer32\\ThreadingModel", "Both");
wshShell.RegWrite(root + "\\Software\\Classes\\CLSID\\" + clsid +
"\\InprocServer32\\Class", cls);
wshShell.RegWrite(root + "\\Software\\Classes\\CLSID\\" + clsid +
"\\InprocServer32\\Assembly", assembly);
wshShell.RegWrite(root + "\\Software\\Classes\\CLSID\\" + clsid +
"\\InprocServer32\\RuntimeVersion", "v2.0.50727");
wshShell.RegWrite(root + "\\Software\\Classes\\CLSID\\" + clsid +
"\\InprocServer32\\CodeBase", codebase);
wshShell.RegWrite(root + "\\Software\\Classes\\CLSID\\" + clsid +
"\\InprocServer32\\" + version + "\\Class", cls);
wshShell.RegWrite(root + "\\Software\\Classes\\CLSID\\" + clsid +
"\\InprocServer32\\" + version + "\\Assembly", assembly);
wshShell.RegWrite(root + "\\Software\\Classes\\CLSID\\" + clsid +
"\\InprocServer32\\" + version + \\RuntimeVersion,
"v2.0.50727");
wshShell.RegWrite(root + "\\Software\\Classes\\CLSID\\" + clsid +
"\\InprocServer32\\" + version + "\\CodeBase", codebase);
wshShell.RegWrite(root + "\\Software\\Classes\\CLSID\\" + clsid +
"\\ProgId\\", progId);
wshShell.RegWrite(root + "\\Software\\Classes\\CLSID\\" + clsid +
\\Implemented Categories\\{62C8FE65-4EBB-45E7-B440-6E39B2CDBF29}\\,
"");
}
没有太复杂,只是几个基本的注册表项。JavaScript 库的其余部分充当 Gadget Adapter 方法的包装器。您可以从自己的脚本中使用GadgetBuilder
的方法来加载或卸载任何 .NET 类型。
现在我们已经有了互操作层,让我们看看 GMail 示例(包含在源代码下载中),看看互操作层是如何实际使用的。
GMail 阅读器 - 运行 .NET 代码
读取 GMail 帐户收件箱就像读取 XML 提要一样简单。提要非常简单,实际上可以使用 JavaScript 进行解析。我故意向GmailReader
程序集添加了一些复杂性,以便在托管代码中实现一些我无法在 JavaScript 中完成的事情。此外,使用 .NET 代码可以提高速度和调试能力(这比您对 JavaScript 的要求要高得多)。为此,GMail 提要中的响应 XML 经过XslCompiledTransform
处理,将响应反序列化为我创建的类型的泛型列表(即强类型)。然后,该泛型列表中的类型可以直接暴露给 JavaScript。我在这里不会深入探讨 GMail 代码,因为它易于理解且注释良好。真正重要的是理解为了使代码对 JavaScript 友好所做的工作,以及如何从 JavaScript 调用该代码。有几个关键步骤是必需的。
[ComVisible(true)]
public class GmailClient : IDisposable
{
...
}
到目前为止,最重要的一步是将[ComVisible(true)]
属性添加到任何将被您的 JavaScript 使用的类。如果没有此属性,该类将无法被 JavaScript 调用。另一个约定是实现IDisposable
接口。这实际上只是预防性维护和良好实践,因为我们的对象暴露给 COM。
加载程序集
此时,让我们检查一下 Gmail.js 文件,看看如何从小工具使用 .NET 程序集。首先要做的是创建并初始化 GadgetInterop.js 文件中找到的GadgetBuilder
包装器的实例。我们将使用该包装器来加载和卸载 .NET 类型。
var builder = new GadgetBuilder();
builder.Initialize();
调用Initialize
方法会执行几个重要的任务。首先,它通过尝试创建 ActiveX 对象实例来检查 Gadget Adapter 是否已注册。如果失败,构建器会尝试运行上面列出的注册代码。这里的妙处在于,您无需手动注册 Gadget Adapter COM 对象。JavaScript 库会在它第一次运行时为您完成。
function Initialize()
{
if(InteropRegistered() == false)
{
RegisterGadgetInterop();
}
_builder = GetActiveXObject();
}
将所有内容整合在一起
下一步是加载GmailReader
程序集并创建客户端类型的实例。GmailClient
有两个构造函数参数:userName
和password
,这是创建实例所必需的。这两个参数的值都来自小工具的设置页面,并使用Gadget
API 存储,因此它们在小工具的生命周期内都存在。
builder.AddConstructorParam(userID);
builder.AddConstructorParam(password);
gmailClient = builder.LoadType(System.Gadget.path +
"\\bin\\GmailReader.dll", "GmailReader.GmailClient");
我们告诉构建器加载位于小工具 bin 目录中的 GmailReader.dll 程序集。无需将程序集放在“bin”目录中,甚至无需与小工具位于相同的文件夹结构中。我只是为了方便在这个示例中这样做了。
此时,gmailClient
JavaScript 变量持有一个完全加载的 .NET GmailClient
类型的引用。现在我们可以直接调用objects
方法,就像在托管代码中一样。为了获得足够的信息以在小工具 UI 上显示有意义的内容,我们可以调用以下代码
gmailClient.GetUnreadMail();
var count = gmailClient.UnreadMailCount;
var mailLink = document.getElementById('mailCountLink');
mailLink.innerText = count;
请注意,除了var
数据类型外,它与 .NET 等效项没有什么不同。换句话说,您现在可以完全访问您想在自己的对象中公开的任何方法或属性,并且无需再进行 COM 工作。这适用于任何程序集。有了 Gadget Adapter,您无需再进行任何 COM 工作。
尽管我们正在使用推断类型var
,但一旦我们从 .NET 代码中获得一个值,它就可以像任何其他 JavaScript 值一样使用。在这种情况下,未读邮件的数量会显示给用户,并且背景会根据没有邮件或新邮件而改变。
最后,因为gmailClient
保留在内存中,所以用户随时点击未读邮件计数链接时都可以显示邮件内容。换句话说,.NET 对象维护它(因此需要手动清理)。以下是详细信息在小工具中显示的方式。
总结
真的就是这样。一旦您的对象创建,您可以像从托管 .NET 代码调用它一样使用它。此外,由于互操作层在您第一次运行小工具后注册,因此它可以在所有小工具中重复使用。最棒的是,您可以将程序集、互操作程序集和互操作 JavaScript 库与您的小工具一起打包,Vista 将像处理任何其他小工具一样处理整个安装过程。
不幸的是,微软将托管代码排除在小工具框架之外,尤其是当他们在许多其他领域都支持它时。尽管如此,事实是托管代码仍然可以轻松使用,因此对于一些真正有用的小工具开发仍有希望。
尽情享用!