如果你的 Windows 运行时组件在后台线程(工作线程)上引发了一个用户定义的委托类型的事件,而你希望 JavaScript 能够收到该事件,则可以通过下列方式之一实现和/或引发该事件:
(选项 1)通过 CoreDispatcher 引发事件以便将事件封送到 JavaScript 线程上下文。 虽然这通常是最佳选项,但在某些情况下,它并不能提供最快的性能。
(选项 2)使用 Windows.Foundation.EventHandler<Object>(但会丢失事件类型信息)。 如果选项 1 不可行或其性能不够快,而且可以接受类型信息丢失,则这是一个不错的第二选择。
(选项 3)创建自己的代理和存根 COM 对象(针对组件)。 此选项的实现难度最大,但它会保留类型信息,但在要求严苛的情况下可能比选项 1 提供更好的性能。
如果只在后台线程上引发事件,而没有使用上述选项之一,JavaScript 客户端将不会收到该事件。
背景
就本质而言,所有 Windows 运行时组件都是 COM 对象,而无论你使用何种语言来创建它们。 在 Windows API 中,大多数组件都是敏捷的 COM 对象,可与后台线程和 UI 线程上的对象进行同样有效的通信。 如果 COM 对象无法设置为敏捷对象,则需要称为代理和存根的 Helper 对象才能跨 UI 线程-后台线程边界与其他 COM 对象通信。(在 COM 术语中,这称为线程单元之间的通信。)
Windows API 中的大多数对象都是敏捷对象或内置了代理和存根。 但是,不能为 Windows.Foundation.TypedEventHandler<TSender, TResult> 等泛型类型创建代理和存根,因为如果不提供类型参数,它们就不是完整的类型。 只有 JavaScript 客户端会在缺乏代理或存根时出现问题,但如果要让组件能够在 JavaScript 以及 C++ 或 .NET 语言中都可用,则必须使用下列选项之一。
(选项 1)通过 CoreDispatcher 引发事件
可使用 Windows.UI.Core.CoreDispatcher 发送任何用户定义的委托类型的事件,这时 JavaScript 将能够收到它们。 如果不确定要使用哪个选项,可先尝试此选项。 如果事件激发和事件处理之间的延迟会带来问题,请尝试其他选项之一。
下面的示例演示如何使用 CoreDispatcher 引发强类型的事件。 请注意,类型参数是 Toast,而不是 Object。
public event EventHandler<Toast> ToastCompletedEvent;
private void OnToastCompleted(Toast args)
{
var completedEvent = ToastCompletedEvent;
if (completedEvent != null)
{
completedEvent(this, args);
}
}
public void MakeToastWithDispatcher(string message)
{
Toast toast = new Toast(message);
// Assume you have a CoreDispatcher at class scope.
// Initialize it here, then use it from the background thread.
var window = Windows.UI.Core.CoreWindow.GetForCurrentThread();
m_dispatcher = window.Dispatcher;
Task.Run( () =>
{
if (ToastCompletedEvent != null)
{
m_dispatcher.RunAsync(CoreDispatcherPriority.Normal,
new DispatchedHandler(() =>
{
this.OnToastCompleted(toast);
})); // end m_dispatcher.RunAsync
}
}); // end Task.Run
}
(选项 2)使用 EventHandler<Object>(但会丢失类型信息)
另一种从后台线程发送事件的方式是使用 Windows.Foundation.EventHandler<Object> 作为事件的类型。 Windows 对泛型类型提供此具体实例化,并为其提供了代理和存根。 其缺点在于,事件参数和发送方的类型信息会丢失。 C++ 和 .NET 客户端必须通过文档识别在收到事件时要强制转换回哪个类型。 JavaScript 客户端不需要原始类型信息。 它们可基于其在元数据中的名称找到参数属性。
下面的示例演示如何在 C# 中使用 Windows.Foundation.EventHandler<Object>:
public sealed Class1
{
// Declare the event
public event EventHandler<Object> ToastCompletedEvent;
// Raise the event
public async void MakeToast(string message)
{
Toast toast = new Toast(message);
// Fire the event from a background thread to allow this thread to continue
Task.Run(() =>
{
if (ToastCompletedEvent != null)
{
OnToastCompleted(toast);
}
});
}
private void OnToastCompleted(Toast args)
{
var completedEvent = ToastCompletedEvent;
if (completedEvent != null)
{
completedEvent(this, args);
}
}
}
在 JavaScript 端使用此事件的方式如下:
toastCompletedEventHandler: function (event) {
var toastType = event.toast.toastType;
document.getElementById("toasterOutput").innerHTML = "<p>Made " + toastType + " toast</p>";
},
(选项 3)创建自己的代理和存根
若要对完全保留了类型信息的用户定义的事件类型获得潜在的性能提升,就必须创建自己的代理和存根对象,并将其嵌入在应用程序包中。 通常,只有在其他两个选项都不能满足要求的情况下才能使用此选项,但这类情况并不多见。 此外,无法保证此选项将比其他两个选项提供更高的性能。 实际性能取决于多种因素。 请使用 Visual Studio 探查器或其他探查工具测量应用程序的实际性能,并确定该事件是否确实为瓶颈。
本文的剩余部分将说明如何使用 C# 创建基本 Windows 运行时组件,再使用 C++ 为代理和存根创建 DLL,以便 JavaScript 使用组件在异步操作中引发的 Windows.Foundation.TypedEventHandler<TSender, TResult> 事件。(你还可以使用 C++ 或 Visual Basic 创建该组件。 有关创建代理和存根的步骤相同。)本演练基于 Creating a Windows Runtime in-process component sample (C++/CX)(创建 Windows 运行时进程内组件示例 (C++/CX))并帮助说明其用途。
本演练分为三个部分:
创建 Windows 运行时组件:在此部分中,将创建两个基本 Windows 运行时类。 一个类公开 Windows.Foundation.TypedEventHandler<TSender, TResult> 类型的事件,另一个类是作为 TValue 的参数返回给 JavaScript 的类型。 在完成后续步骤之前,这两个类不能与 JavaScript 通信。
编写 JavaScript 应用程序:此应用程序将激活主类对象、调用方法,并处理由 Windows 运行时组件引发的事件。
为组件的接口生成 GUID:用于生成代理和存根类的工具需要这些 GUID。
为组件生成 IDL 文件:随后使用 IDL 文件生成代理和存根的 C 源代码。
将代理和存根代码编译为 DLL
注册和使用代理存根 DLL:注册代理存根对象以便 COM 运行时可以找到它们,并在应用程序项目中引用该代理存根 DLL。
创建 Windows 运行时组件
在 Visual Studio 的菜单栏上,选择**“文件”>“新建项目”。 在“新建项目”对话框中,展开“JavaScript”>“Windows 应用商店”,然后选择“空白应用程序”。 将项目命名为 ToasterApplication,然后选择“确定”**按钮。
向解决方案中添加一个 C# Windows 运行时组件:在**“解决方案资源管理器”中,打开解决方案的快捷菜单,然后选择“添加”>“新建项目”。 展开“Visual C#”>“Windows 应用商店”,然后选择“Windows 运行时组件”。 将项目命名为 ToasterComponent,然后选择“确定”**按钮。 ToasterComponent 将是你在后续步骤中创建的组件的根命名空间。
在**“解决方案资源管理器”中,打开解决方案的快捷菜单,然后选择“属性”。 在“属性页”对话框中,选择左窗格中的“配置属性”,然后在对话框顶部将“配置”设置为“调试”,并将“平台”设置为“x86”、“x64”或“ARM”。 选择“确定”**按钮。
重要
将“平台”设置为“任意 CPU”不起作用,因为这对于稍后添加到解决方案中的本机代码 Win32 DLL 无效。
在**“解决方案资源管理器”**中,将 class1.cs 重命名为 ToasterComponent.cs,以使其与项目的名称匹配。 Visual Studio 会自动重命名该文件中的类以便与新文件名匹配。
在 .cs 文件中,添加一条针对 Windows.Foundation 命名空间的指令,以便将 TypedEventHandler 纳入范围。
当需要代理和存根时,你的组件必须使用接口公开其公共成员。 在 ToasterComponent.cs 中,为 toaster 定义一个接口,并为 toaster 生成的 Toast 定义另一个接口。
备注
在 C# 中,可以跳过此步骤,并改为先创建类,再打开其快捷菜单,然后选择“重构”>“提取接口”。在生成的代码中,请手动为接口提供公共可访问性。
public interface IToaster { void MakeToast(String message); event TypedEventHandler<Toaster, Toast> ToastCompletedEvent; } public interface IToast { String ToastType { get; } }
IToast 接口有一个字符串,可检索此字符串来描述 toast 的类型。 IToaster 接口有一个方法来执行 toast,另有一个事件来指示 toast 已经完成。 由于此类型会返回特定的某块(即类型)toast,因为被称为类型事件。
接下来,我们需要用于实现这些接口的类,这些类必须是公开的密封类,以便从稍后编写的 JavaScript 应用程序中访问它们。
public sealed class Toast : IToast { private string _toastType; public string ToastType { get { return _toastType; } } internal Toast(String toastType) { _toastType = toastType; } } public sealed class Toaster : IToaster { public event TypedEventHandler<Toaster, Toast> ToastCompletedEvent; private void OnToastCompleted(Toast args) { var completedEvent = ToastCompletedEvent; if (completedEvent != null) { completedEvent(this, args); } } public void MakeToast(string message) { Toast toast = new Toast(message); // Fire the event from a thread-pool thread to enable this thread to continue Windows.System.Threading.ThreadPool.RunAsync( (IAsyncAction action) => { if (ToastCompletedEvent != null) { OnToastCompleted(toast); } }); } }
在上面的代码中,我们创建了 toast,然后启动了一个线程池工作项来激发通知。 虽然 IDE 可能会建议你对异步调用应用 await 关键字,但在此示例中并不需要如此,因为该方法的任何作业都不依赖于操作的结果。
重要
上面代码中的异步调用使用 ThreadPool.RunAsync,这只是为了演示在后台线程上激发事件的一种简单方法。你可以按照下面的示例编写此特定方法而且效果不错,因为 .NET 任务计划程序会自动将异步/等待调用封送回 UI 线程。
public async void MakeToast(string message) { Toast toast = new Toast(message) await Task.Delay(new Random().Next(1000)); OnToastCompleted(toast); }
如果现在生成项目,则应该会顺利完成生成操作。
编写 JavaScript 应用程序
现在,我们可以在 JavaScript 应用程序中添加一个按钮,以让其使用我们刚才定义的用于生成 toast 的类。 在此之前,我们必须添加对刚才创建的 ToasterComponent 项目的引用。 在**“解决方案资源管理器”中,打开 ToasterApplication 项目的快捷菜单,选择“添加”>“引用”,然后选择“添加新引用”按钮。 在“添加引用”对话框中,从左窗格中的“解决方案”下选择组件项目,然后在中间窗格中选择“ToasterComponent”。 选择“确定”**按钮。
在**“解决方案资源管理器”中,打开 ToasterApplication 项目的快捷菜单,然后选择“设为启动项目”**。
在 default.js 文件的末尾,添加一个命名空间以包含要调用组件并由其回调的函数。 该命名空间将包含两个函数,一个用于制作 toast,另一个用于处理 toast 完成事件。 makeToast 的实现会创建 Toaster 对象、注册事件处理程序并制作 toast。 到目前为止,事件处理程序并不执行过多的操作,如下所示:
WinJS.Namespace.define("ToasterApplication", { makeToast: function () { var toaster = new ToasterComponent.Toaster(); //toaster.addEventListener("ontoastcompletedevent", ToasterApplication.toastCompletedEventHandler); toaster.ontoastcompletedevent = ToasterApplication.toastCompletedEventHandler; toaster.makeToast("Peanut Butter"); }, toastCompletedEventHandler: function (event) { // The sender of the event (the delegate’s first type parameter) // is mapped to event.target. The second argument of the delegate // is contained in event, which means event in this case is a // Toast class, with a toastType string. var toastType = event.toastType; document.getElementById("toasterOutput").innerHTML = "<p>Made " + toastType + " toast</p>"; }, });
makeToast 函数必须挂接至一个按钮。 更新 default.html,使之包括一个按钮以及一些用于输出 toast 制作结果的空间:
<body> <h1>Click the button to make toast</h1> <button onclick="ToasterApplication.makeToast()">Make Toast!</button> <div id="toasterOutput"> <p>No Toast Yet...</p> </div> </body>
如果不使用 TypedEventHandler,现在将可以在本地计算机上运行应用程序并单击该按钮来制作 toast。 但我们的应用程序现在不会执行任何操作。 为找出原因,我们来调试激发 ToastCompletedEvent 的托管代码。 停止项目,然后在菜单栏上选择**“调试”>“Toaster Application 属性”。 将“调试器类型”更改为“仅限托管”。 再次转到菜单栏,选择“调试”>“异常”,然后选择“公共语言运行时异常”**。
现在运行应用程序并单击“make-toast”(制作吐丝)按钮。 调试器会捕捉无效的强制转换异常。 尽管从其消息中不能明确看出来,但此异常确实发生了,因为该接口的代理缺失。
为组件创建代理和存根的第一步是向接口添加唯一的 ID 或 GUID。 但是,要使用的 GUID 格式取决于编码时使用的是 C#、Visual Basic、其他 .NET 语言还是 C++。
为组件的接口生成 GUID
对于 C#、Visual Basic 或其他 .NET 语言:
在菜单栏上,选择**“工具”>“创建 GUID”。 在对话框中,选择“5. [Guid(“xxxxxxxx-xxxx...xxxx)]”。 选择“新建 GUID”按钮,然后选择“复制”**按钮。
返回接口定义,将新 GUID 粘贴 IToaster 接口的紧前面,如以下示例所示。(请勿使用示例中的 GUID。 每个唯一的接口都应该有其自己的 GUID。)
[Guid("FC198F74-A808-4E2A-9255-264746965B9F")] public interface IToaster...
为 System.Runtime.InteropServices 命名空间添加一条 using 指令。
对 IToast 接口重复这些步骤。
对于 C++:
在菜单栏上,选择**“工具”>“创建 GUID”。 在对话框中,选择“3. 静态常量结构 GUID = {...}”。 选择“新建 GUID”按钮,然后选择“复制”**按钮。
将 GUID 粘贴到 IToaster 接口定义的紧前面。 粘贴完毕后,GUID 应类似于以下示例。(请勿使用示例中的 GUID。 每个唯一的接口都应该有其自己的 GUID。)
// {F8D30778-9EAF-409C-BCCD-C8B24442B09B} static const GUID <<name>> = { 0xf8d30778, 0x9eaf, 0x409c, { 0xbc, 0xcd, 0xc8, 0xb2, 0x44, 0x42, 0xb0, 0x9b } };
为 Windows.Foundation.Metadata 添加 using 指令以将 GuidAttribute 纳入范围。
现在,手动将 const GUID 转换为 GuidAttribute,以便将其设置为以下示例中显示的格式。 请注意,大括号被替换为中括号和小括号,并移除了尾随的分号。
// {E976784C-AADE-4EA4-A4C0-B0C2FD1307C3} [GuidAttribute(0xe976784c, 0xaade, 0x4ea4, 0xa4, 0xc0, 0xb0, 0xc2, 0xfd, 0x13, 0x7, 0xc3)] public interface IToaster {...
对 IToast 接口重复这些步骤。
现在接口已具有唯一的 ID,接下来可将 .winmd 文件送入 winmdidl 命令行工具以创建一个 IDL 文件,然后将该 IDL 文件送入 MIDL 命令行工具,以便为代理和存根生成 C 源代码。 如果创建了后生成事件,Visual Studio 将为我们执行此操作,如以下步骤所示。
生成代理和存根源代码
若要添加自定义后生成事件,请在**“解决方案资源管理器”中打开 ToasterComponent 项目的快捷菜单,然后选择“属性”。 在属性页的左窗格中,选择“生成事件”,然后选择“编辑后期生成事件”**按钮。 将以下命令添加到后生成命令行。(必须先调用批处理文件,以便将环境变量设置为查找 winmdidl 工具。)
call "$(DevEnvDir)..\..\vc\vcvarsall.bat" $(PlatformName) winmdidl /outdir:output "$(TargetPath)" midl /metadata_dir "%WindowsSdkDir%References\CommonConfiguration\Neutral" /iid "$(ProjectDir)$(TargetName)_i.c" /env win32 /h "$(ProjectDir)$(TargetName).h" /winmd "Output\$(TargetName).winmd" /W1 /char signed /nologo /winrt /dlldata "$(ProjectDir)dlldata.c" /proxy "$(ProjectDir)$(TargetName)_p.c" "Output\$(TargetName).idl"
重要
对于 ARM 或 x64 项目配置,请将 MIDL /env 参数更改为 x64 或 arm32。
若要确保每次更改 .winmd 文件后都重新生成 IDL 文件,请将**“运行后期生成事件”更改为“生成更新项目输出时”**。
**“生成事件”**属性页应如下所示:
重新生成解决方案以生成和编译 IDL。
通过在 ToasterComponent 项目目录中查找 ToasterComponent.h、ToasterComponent_i.c、ToasterComponent_p.c 和 dlldata.c,可验证 MIDL 是否正确编译了解决方案。
将代理和存根代码编译为 DLL
现在所需文件已经齐全,下面即可编译它们来生成 DLL(C++ 文件)。 若要尽量简化此过程,请添加一个新项目来支持生成代理的操作。 打开 ToasterApplication 解决方案的快捷菜单,然后选择**“添加”>“新建项目”。 在“新建项目”对话框的左窗格中,展开“Visual C++”>“Windows 应用商店”,然后在中间窗格中选择“DLL (Windows 应用商店应用程序)”。(请注意,这不是 C++ Windows 运行时组件项目。)将项目命名为 Proxies,然后选择“确定”**按钮。 当 C# 类中发生更改时,后生成事件将更新这些文件。
默认情况下,Proxies 项目会生成 .h 头文件和 .cpp C++ 文件。 由于 DLL 基于从 MIDL 中生成的文件而生成,因此不需要 .h 和 .cpp 文件。 在**“解决方案资源管理器”中,打开它们的快捷菜单,选择“移除”**,然后确认删除。
现在项目已变空,下面可重新添加 MIDL 生成的文件。 打开 Proxies 项目的快捷菜单,然后选择**“添加”>“现有项”。 在对话框中,导航到 ToasterComponent 项目目录,然后选择 ToasterComponent.h、ToasterComponent_i.c、ToasterComponent_p.c 和 dlldata.c 文件。 选择“添加”**按钮。
在 Proxies 项目中,创建一个 .def 文件以定义 dlldata.c 中描述的 DLL 导出。 打开项目的快捷菜单,然后选择**“添加”>“新项”。 在对话框的左窗格中选择“代码”,然后在中间窗格中选择“模块定义文件”。 将文件命名为 proxies.def,然后选择“添加”**按钮。 打开此 .def 文件并进行修改,使之包括 dlldata.c 中定义的 EXPORTS:
EXPORTS DllCanUnloadNow PRIVATE DllGetClassObject PRIVATE
如果现在生成项目,生成操作将失败。 若要正确编译此项目,必须更改项目的编译和链接方式。 在**“解决方案资源管理器”中,打开 Proxies 项目的快捷菜单,然后选择“属性”**。 按如下所示更改属性页:
在左窗格中选择**“C/C++”>“预处理器”,在右窗格中选择“预处理器定义”,选择向下箭头按钮,然后选择“编辑”**。 在框中添加以下定义:
WIN32;_WINDOWS
在**“C/C++”>“预编译头”下,将“预编译头”更改为“不使用预编译头”,然后选择“应用”**按钮。
在**“链接器”>“常规”下,将“忽略导入库”更改为“是”,然后选择“应用”**按钮。
在**“链接器”>“输入”下,选择“附加依赖项”,选择向下箭头按钮,然后选择“编辑”**。 在框中添加以下文本:
rpcrt4.lib;runtimeobject.lib
不要将这些库直接粘贴到列表行中。 使用“编辑”框可确保 Visual Studio 中的 MSBuild 维护正确的附加依赖项。
完成这些更改后,选择“属性页”对话框中的**“确定”**按钮。
接下来,为 ToasterComponent 项目创建一个依赖项。这样可确保 Toaster 在代理项目生成之前生成。 此操作非常必要,因为 Toaster 项目负责生成用于生成代理的文件。
打开 Proxies 项目的快捷菜单,然后选择**“项目依赖项”**。 选中相应的复选框以指示 Proxies 项目依赖于 ToasterComponent 项目,从而确保 Visual Studio 按正确的顺序生成它们。
通过在 Visual Studio 菜单栏上选择**“生成”>“重新生成解决方案”**,验证解决方案是否已正确生成。
注册代理和存根
在 ToasterApplication 项目中,打开 package.appxmanifest 的快捷菜单,然后选择**“打开方式”。 在“打开方式”对话框中,选择“XML 文本编辑器”,然后选择“确定”**按钮。 我们将粘贴一些 XML,它们提供 windows.activatableClass.proxyStub 扩展注册,并且基于代理中的 GUID。 若要查找将在 .appxmanifest 文件中使用的 GUID,请打开 ToasterComponent_i.c。 查找与以下示例中的内容类似的条目。 另请注意 IToast, IToaster 的定义和第三个接口,此接口是一个强类型事件处理程序,它具有两个参数:Toaster 和 Toast。 这与 Toaster 类中定义的事件匹配。 请注意,IToast 和 IToaster 的 GUID 与 C# 文件中在接口上定义的 GUID 匹配。 由于此强类型事件处理程序接口是自动生成的,因而此接口的 GUID 也会自动生成。
MIDL_DEFINE_GUID(IID, IID___FITypedEventHandler_2_ToasterComponent__CToaster_ToasterComponent__CToast,0x1ecafeff,0x1ee1,0x504a,0x9a,0xf5,0xa6,0x8c,0x6f,0xb2,0xb4,0x7d); MIDL_DEFINE_GUID(IID, IID___x_ToasterComponent_CIToast,0xF8D30778,0x9EAF,0x409C,0xBC,0xCD,0xC8,0xB2,0x44,0x42,0xB0,0x9B); MIDL_DEFINE_GUID(IID, IID___x_ToasterComponent_CIToaster,0xE976784C,0xAADE,0x4EA4,0xA4,0xC0,0xB0,0xC2,0xFD,0x13,0x07,0xC3);
现在,我们复制 GUID,将其粘贴到我们在 package.appxmanifest 中添加的 Extensions 节点中,然后重新设置其格式。 清单条目类似于以下示例,但同样,请确保使用你自己的 GUID。 请注意,XML 中的 ClassId GUID 与 ITypedEventHandler2 相同。 这是因为该 GUID 是 ToasterComponent_i.c 中列出的第一个 GUID。 此处的 GUID 不区分大小写。 你可以返回接口定义并获取 GuidAttribute 值(包含正确的格式),而不必手动对 IToast 和 IToaster 的 GUID 重新设置格式。 在 C++ 中,注释中提供了已正确设置格式的 GUID。 无论在任何情况下,都必须对用于 ClassId 和事件处理程序的 GUID 手动重新设置格式。
<Extensions> <!—Use your own GUIDs!!!--> <Extension Category="windows.activatableClass.proxyStub"> <ProxyStub ClassId="1ecafeff-1ee1-504a-9af5-a68c6fb2b47d"> <Path>Proxies.dll</Path> <Interface Name="IToast" InterfaceId="F8D30778-9EAF-409C-BCCD-C8B24442B09B"/> <Interface Name="IToaster" InterfaceId="E976784C-AADE-4EA4-A4C0-B0C2FD1307C3"/> <Interface Name="ITypedEventHandler_2_ToasterComponent__CToaster_ToasterComponent__CToast" InterfaceId="1ecafeff-1ee1-504a-9af5-a68c6fb2b47d"/> </ProxyStub> </Extension> </Extensions>
将 Extensions XML 节点粘贴为 Package 节点的直接子级,以及 Resources 等节点的同级。
继续之前,必须确保:
ProxyStub ClassId 已设置为 ToasterComponent_i.c 文件中的第一个 GUID。 使用该文件中为 classId 定义的第一个 GUID。(这可能与 ITypedEventHandler2 的 GUID 相同。)
Path 是代理二进制文件的包相对路径。(在本演练中,proxies.dll 与 ToasterApplication.winmd 位于同一文件夹内。)
GUID 的格式正确无误。(此处很容易出错。)
清单中的接口 ID 与 ToasterComponent_i.c 文件中的 IID 匹配。
接口名称在清单中是唯一的。 由于系统不使用这些名称,因此你可以选择它们的值。 选择的接口名称最好与定义的接口明确匹配。 对于生成的接口,名称应充当接口的指示符。 你可以使用 ToasterComponent_i.c 文件来帮助你生成接口名称。
如果尝试现在运行解决方案,将显示一条错误,指示 proxies.dll 不是负载的一部分。 在 ToasterApplication 项目中,打开**“References”文件夹的快捷菜单,然后选择“添加引用”。 选中 Proxies 项目旁边的复选框。 此外,请确保同时选中 ToasterComponent 旁边的复选框。 选择“确定”**按钮。
现在应该能够生成项目了。 赶快运行该项目并验证其是否能制作土司。