创建 C# 或 Visual Basic Windows 运行时组件并通过 JavaScript 调用此组件的演练

本演练演示了如何将 .NET 与 Visual Basic 或 C# 结合使用来创建自己的 Windows 运行时类型(打包在 Windows 运行时组件中),以及之后如何从 JavaScript 通用 Windows 平台 (UWP) 应用中调用该组件。

通过 Visual Studio,可轻松地在使用 C# 或 Visual Basic 编写的 Windows 运行时组件 (WRC) 项目中创作和部署自己的自定义 Windows 运行时类型,然后从 JavaScript 应用程序项目中引用该 WRC,并从该应用程序中使用这些自定义类型。

在内部,Windows 运行时类型可以使用 UWP 应用程序中允许的任何 .NET 功能。

在外部,类型的成员只能为其参数和返回值公开 Windows 运行时类型。 在生成解决方案时,Visual Studio 会生成 .NET WRC 项目,然后执行可创建 Windows 元数据 (.winmd) 文件的生成步骤。 这是 Visual Studio 在应用中包括的Windows 运行时组件。

注意

.NET 自动将某些常用的 .NET 类型(例如基元数据类型和集合类型)映射到其 Windows 运行时等效项。 这些 .NET 类型可在 Windows 运行时组件的公共接口中使用,并且将作为相应的 Windows 运行时类型向该组件的用户显示。 请参阅使用 C# 和 Visual Basic 创建 Windows 运行时组件

先决条件:

注意

Visual Studio 2019 不支持使用 JavaScript 的通用 Windows 平台 (UWP) 项目。 请参阅 Visual Studio 2019 中的 JavaScript 和 TypeScript。 若要遵循本主题,我们建议使用 Visual Studio 2017。 请参阅 Visual Studio 2017 中的 JavaScript

创建简单的Windows 运行时类

本部分将创建一个 JavaScript UWP 应用程序,并将 Visual Basic 或 C# Windows 运行时组件项目添加到解决方案中。 它演示了如何定义 Windows 运行时类型、创建 JavaScript 中类型的实例并调用静态和实例成员。 特意将示例应用的视觉显示设置为暗调效果,从而重点关注组件。

  1. 在 Visual Studio 中创建新的 JavaScript 项目:在菜单栏上,选择“ 文件”、“新建”、“项目”。 在“新建项目”对话框的“已安装的模板”部分中,选择 JavaScript,然后选择“Windows”,然后选择“通用”。 (如果 Windows 不可用,请确保使用的是 Windows 8 或更高版本。选择 “空白应用程序 ”模板,然后输入 SampleApp 以获取项目名称。

  2. 创建组件项目:在解决方案资源管理器中,打开 SampleApp 解决方案的快捷菜单,然后选择“添加”,然后选择“新建项目”将新的 C# 或 Visual Basic 项目添加到解决方案。 在“添加新项目”对话框的“已安装的模板”部分中,选择“Visual Basic”或“Visual C#”,然后选择“Windows”,然后选择“通用”。 选择Windows 运行时组件模板,并为项目名称输入 SampleComponent

  3. 将类的名称更改为 Example。 请注意,默认情况下,该类标记为 公共密封Visual Basic 中的 Public NotInheritable )。 必须封装通过你的组件公开的所有 Windows 运行时类。

  4. 向类添加两个简单成员:静态方法(Visual Basic 中的共享方法)和实例属性:

    namespace SampleComponent
    {
        public sealed class Example
        {
            public static string GetAnswer()
            {
                return "The answer is 42.";
            }
    
            public int SampleProperty { get; set; }
        }
    }
    
    Public NotInheritable Class Example
        Public Shared Function GetAnswer() As String
            Return "The answer is 42."
        End Function
    
        Public Property SampleProperty As Integer
    End Class
    
  5. 可选:若要为新添加的成员启用 IntelliSense,请在解决方案资源管理器中打开 SampleComponent 项目的快捷菜单,然后选择“生成”。

  6. 在 解决方案资源管理器,在 JavaScript 项目中,打开引用的快捷菜单,然后选择“添加引用”以打开引用管理器 依次选择“项目”和“解决方案”。 选中 SampleComponent 项目的复选框,然后选择“确定添加引用。

从 JavaScript 调用组件

若要使用 JavaScript 中的Windows 运行时类型,请在 Visual Studio 模板提供的default.js文件(项目 js 文件夹中)的匿名函数中添加以下代码。 它应在 app.oncheckpoint 事件处理程序和对 app.start 的调用之前执行。

var ex;

function basics1() {
   document.getElementById('output').innerHTML =
        SampleComponent.Example.getAnswer();

    ex = new SampleComponent.Example();

   document.getElementById('output').innerHTML += "<br/>" +
       ex.sampleProperty;

}

function basics2() {
    ex.sampleProperty += 1;
    document.getElementById('output').innerHTML += "<br/>" +
        ex.sampleProperty;
}

请注意,每个成员名称的第一个字母从大写更改为小写。 此转换是 JavaScript 为使Windows 运行时自然使用提供支持的一部分。 命名空间和类名是 Pascal 大小写的。 成员名称是骆驼大小写的,但事件名称都是小写的。 请参阅在 JavaScript 中使用Windows 运行时。 骆驼大小写规则可能会令人困惑。 一系列初始大写字母通常显示为小写字母,但如果三个大写字母后跟小写字母,则只有前两个字母以小写形式显示:例如,名为 IDStringKind 的成员显示为 idStringKind。 在 Visual Studio 中,可以生成Windows 运行时组件项目,然后在 JavaScript 项目中使用 IntelliSense 查看正确的大小写。

类似地,.NET 提供了支持以便在托管代码中自然地使用 Windows 运行时。 在本文的后续部分以及文章使用 C# 和 Visual Basic 的 Windows 运行时组件UWP 应用和 Windows 运行时的 .NET 支持中对此进行了讨论。

创建简单的用户界面

在 JavaScript 项目中,打开default.html文件并更新正文,如以下代码所示。 此代码包括示例应用的完整控件集,并指定单击事件的函数名称。

请注意 ,首次运行应用时,仅支持 Basics1 和 Basics2 按钮。

<body>
            <div id="buttons">
            <button id="button1" >Basics 1</button>
            <button id="button2" >Basics 2</button>

            <button id="runtimeButton1">Runtime 1</button>
            <button id="runtimeButton2">Runtime 2</button>

            <button id="returnsButton1">Returns 1</button>
            <button id="returnsButton2">Returns 2</button>

            <button id="events1Button">Events 1</button>

            <button id="btnAsync">Async</button>
            <button id="btnCancel" disabled="disabled">Cancel Async</button>
            <progress id="primeProg" value="25" max="100" style="color: yellow;"></progress>
        </div>
        <div id="output">
        </div>
</body>

在 JavaScript 项目中的 css 文件夹中,打开default.css。 按所示修改正文部分,并添加样式来控制按钮的布局和输出文本的位置。

body
{
    -ms-grid-columns: 1fr;
    -ms-grid-rows: 1fr 14fr;
    display: -ms-grid;
}

#buttons {
    -ms-grid-rows: 1fr;
    -ms-grid-columns: auto;
    -ms-grid-row-align: start;
}
#output {
    -ms-grid-row: 2;
    -ms-grid-column: 1;
}

现在,通过将 then 子句添加到 default.js 中 app.onactivated 中的 processAll 调用来添加事件侦听器注册代码。 替换调用 setPromise 的现有代码行,并将其更改为以下代码:

args.setPromise(WinJS.UI.processAll().then(function () {
    var button1 = document.getElementById("button1");
    button1.addEventListener("click", basics1, false);
    var button2 = document.getElementById("button2");
    button2.addEventListener("click", basics2, false);
}));

这是将事件添加到 HTML 控件比直接在 HTML 中添加单击事件处理程序更好的方法。 请参阅创建“Hello, World”应用 (JS)

生成并运行应用

在生成之前,请根据计算机将所有项目的目标平台更改为 Arm、x64 或 x86。

若要生成并运行解决方案,请选择 F5 键。 (如果收到一条运行时错误消息,指出 SampleComponent 未定义,则缺少对类库项目的引用。

Visual Studio 首先编译类库,然后执行运行Winmdexp.exe(Windows 运行时元数据导出工具)的 MSBuild 任务来创建Windows 运行时组件。 该组件包含在包含托管代码和描述代码的 Windows 元数据的 .winmd 文件中。 WinMdExp.exe在Windows 运行时组件中编写无效的代码,并在 Visual Studio IDE 中显示错误消息时生成错误消息。 Visual Studio 将你的组件添加至 UWP 应用程序的应用程序包(.appx 文件),并生成相应的清单。

选择“基本信息 1”按钮,将静态 GetAnswer 方法中的返回值分配给输出区域,创建 Example 类的实例,并在输出区域中显示其 SampleProperty 属性的值。 输出如下所示:

"The answer is 42."
0

选择“基本信息 2”按钮可递增 SampleProperty 属性的值,并在输出区域中显示新值。 基元类型(如字符串和数字)可用作参数类型和返回类型,并且可以在托管代码和 JavaScript 之间传递。 由于 JavaScript 中的数字以双精度浮点格式存储,因此它们将转换为 .NET Framework 数值类型。

请注意 ,默认情况下,只能在 JavaScript 代码中设置断点。 若要调试 Visual Basic 或 C# 代码,请参阅“使用 C# 和 Visual Basic 创建 Windows 运行时组件”。

若要停止调试并关闭应用,请从应用切换到 Visual Studio,然后选择 Shift+F5。

使用 JavaScript 和托管代码中的Windows 运行时

可以从 JavaScript 或托管代码调用Windows 运行时。 Windows 运行时对象可以在两者之间来回传递,并且可以从任一端处理事件。 但是,在两种环境中使用 Windows 运行时类型的方式在一些细节方面略有不同,因为 JavaScript 和 .NET 支持 Windows 运行时的方式不同。 以下示例使用 Windows.Foundation.Collections.PropertySet 类演示了这些差异。 在此示例中,你将在托管代码中创建 PropertySet 集合的实例,并注册事件处理程序以跟踪集合中的更改。 然后,添加用于获取集合的 JavaScript 代码,注册其自己的事件处理程序,并使用该集合。 最后,添加一个方法,该方法从托管代码对集合进行更改,并显示处理托管异常的 JavaScript。

重要说明 在此示例中,事件在 UI 线程上触发。 如果从后台线程触发事件(例如在异步调用中),则需要执行一些额外的工作,以便 JavaScript 处理该事件。 有关详细信息,请参阅引发 Windows 运行时组件中的事件

在 SampleComponent 项目中,添加名为 PropertySetStats 的新 公共密封 类(Visual Basic 中的 Public NotInheritable 类)。 该类包装 PropertySet 集合并处理其 MapChanged 事件。 事件处理程序跟踪每种类型的更改数,DisplayStats 方法生成采用 HTML 格式的报表。 请注意其他 using 语句(Visual Basic 中的 Imports 语句);请小心将其添加到现有 using 语句,而不是覆盖它们。

using Windows.Foundation.Collections;

namespace SampleComponent
{
    public sealed class PropertySetStats
    {
        private PropertySet _ps;
        public PropertySetStats()
        {
            _ps = new PropertySet();
            _ps.MapChanged += this.MapChangedHandler;
        }

        public PropertySet PropertySet { get { return _ps; } }

        int[] counts = { 0, 0, 0, 0 };
        private void MapChangedHandler(IObservableMap<string, object> sender,
            IMapChangedEventArgs<string> args)
        {
            counts[(int)args.CollectionChange] += 1;
        }

        public string DisplayStats()
        {
            StringBuilder report = new StringBuilder("<br/>Number of changes:<ul>");
            for (int i = 0; i < counts.Length; i++)
            {
                report.Append("<li>" + (CollectionChange)i + ": " + counts[i] + "</li>");
            }
            return report.ToString() + "</ul>";
        }
    }
}
Imports System.Text

Public NotInheritable Class PropertySetStats
    Private _ps As PropertySet
    Public Sub New()
        _ps = New PropertySet()
        AddHandler _ps.MapChanged, AddressOf Me.MapChangedHandler
    End Sub

    Public ReadOnly Property PropertySet As PropertySet
        Get
            Return _ps
        End Get
    End Property

    Dim counts() As Integer = {0, 0, 0, 0}
    Private Sub MapChangedHandler(ByVal sender As IObservableMap(Of String, Object),
        ByVal args As IMapChangedEventArgs(Of String))

        counts(CInt(args.CollectionChange)) += 1
    End Sub

    Public Function DisplayStats() As String
        Dim report As New StringBuilder("<br/>Number of changes:<ul>")
        For i As Integer = 0 To counts.Length - 1
            report.Append("<li>" & CType(i, CollectionChange).ToString() &
                          ": " & counts(i) & "</li>")
        Next
        Return report.ToString() & "</ul>"
    End Function
End Class

事件处理程序遵循熟悉的 .NET Framework 事件模式,除了事件的发送程序(在此情况下为 PropertySet 对象)将强制转换为 IObservableMap<string, object> 接口(在 Visual Basic 中为 IObservableMap(Of String, Object)),后者是 Windows 运行时接口 IObservableMap<K, V> 的实例化。 (如有必要,可以将发件人强制转换为其类型。此外,事件参数将呈现为接口而不是对象。

在default.js文件中,添加 Runtime1 函数,如下所示。 此代码创建 PropertySetStats 对象,获取其 PropertySet 集合,并添加自己的事件处理程序 onMapChanged 函数来处理 MapChanged 事件。 对集合进行更改后,runtime1 调用 DisplayStats 方法以显示更改类型的摘要。

var propertysetstats;

function runtime1() {
    document.getElementById('output').innerHTML = "";

    propertysetstats = new SampleComponent.PropertySetStats();
    var propertyset = propertysetstats.propertySet;

    propertyset.addEventListener("mapchanged", onMapChanged);

    propertyset.insert("FirstProperty", "First property value");
    propertyset.insert("SuperfluousProperty", "Unnecessary property value");
    propertyset.insert("AnotherProperty", "A property value");

    propertyset.insert("SuperfluousProperty", "Altered property value")
    propertyset.remove("SuperfluousProperty");

    document.getElementById('output').innerHTML +=
        propertysetstats.displayStats();
}

function onMapChanged(change) {
    var result
    switch (change.collectionChange) {
        case Windows.Foundation.Collections.CollectionChange.reset:
            result = "All properties cleared";
            break;
        case Windows.Foundation.Collections.CollectionChange.itemInserted:
            result = "Inserted " + change.key + ": '" +
                change.target.lookup(change.key) + "'";
            break;
        case Windows.Foundation.Collections.CollectionChange.itemRemoved:
            result = "Removed " + change.key;
            break;
        case Windows.Foundation.Collections.CollectionChange.itemChanged:
            result = "Changed " + change.key + " to '" +
                change.target.lookup(change.key) + "'";
            break;
        default:
            break;
     }

     document.getElementById('output').innerHTML +=
         "<br/>" + result;
}

在 JavaScript 中处理Windows 运行时事件的方式与在 .NET Framework 代码中处理事件的方式大相径庭。 JavaScript 事件处理程序仅采用一个参数。 在 Visual Studio 调试器中查看此对象时,第一个属性是发送方。 事件参数接口的成员也直接显示在此对象上。

若要运行应用,请选择 F5 键。 如果类未密封,则会收到错误消息“导出未密封的类型”SampleComponent.Example“当前不受支持。 请将其标记为密封。

选择“运行时 1”按钮。 事件处理程序在添加或更改元素时显示更改,最后调用 DisplayStats 方法以生成计数摘要。 若要停止调试并关闭应用,请切换回 Visual Studio,然后选择 Shift+F5。

若要从托管代码将另外两个项添加到 PropertySet 集合,请将以下代码添加到 PropertySetStats 类:

public void AddMore()
{
    _ps.Add("NewProperty", "New property value");
    _ps.Add("AnotherProperty", "A property value");
}
Public Sub AddMore()
    _ps.Add("NewProperty", "New property value")
    _ps.Add("AnotherProperty", "A property value")
End Sub

此代码突出显示了在两个环境中使用Windows 运行时类型的方式的另一个差异。 如果你自己键入此代码,你会注意到 IntelliSense 不会显示你在 JavaScript 代码中使用的插入方法。 而是显示在 .NET 的集合中常见的 Add 方法。 这是因为某些常用的集合接口在 Windows 运行时和 .NET 中的名称不同但功能相似。 在托管代码中使用这些接口时,它们显示为其 .NET Framework 等效项。 使用 C# 和 Visual Basic 创建 Windows 运行时组件中对此进行了详细讨论。 在 JavaScript 中使用同一接口时,唯一的 Windows 运行时更改是成员名称开头的大写字母变为小写。

最后,若要调用具有异常处理的 AddMore 方法,请将 runtime2 函数添加到default.js。

function runtime2() {
   try {
      propertysetstats.addMore();
    }
   catch(ex) {
       document.getElementById('output').innerHTML +=
          "<br/><b>" + ex + "<br/>";
   }

   document.getElementById('output').innerHTML +=
       propertysetstats.displayStats();
}

按照之前所做的相同方式添加事件处理程序注册代码。

var runtimeButton1 = document.getElementById("runtimeButton1");
runtimeButton1.addEventListener("click", runtime1, false);
var runtimeButton2 = document.getElementById("runtimeButton2");
runtimeButton2.addEventListener("click", runtime2, false);

若要运行应用,请选择 F5 键。 选择 运行时 1 ,然后选择 运行时 2。 JavaScript 事件处理程序将第一次更改报告给集合。 但是,第二个更改具有重复键。 .NET Framework 字典的用户期望 Add 方法引发异常,这就是所发生的情况。 JavaScript 处理 .NET 异常。

请注意 ,无法显示来自 JavaScript 代码的异常消息。 消息文本由堆栈跟踪替换。 有关详细信息,请参阅“使用 C# 和 Visual Basic 创建 Windows 运行时组件”中的“引发异常”。

相比之下,当 JavaScript 使用重复键调用 insert 方法时,项的值已更改。 这种行为的差异是由于 JavaScript 和 .NET 支持 Windows 运行时的方式不同,如使用 C# 和 Visual Basic 创建 Windows 运行时组件中所述。

从组件返回托管类型

如前所述,可以在 JavaScript 代码和 C# 或 Visual Basic 代码之间来回传递本机Windows 运行时类型。 大多数情况下,两种情况下,类型名称和成员名称将相同(只是成员名称以 JavaScript 中的小写字母开头)。 但是,在上一部分中,PropertySet 类似乎在托管代码中具有不同的成员。 (例如,在 JavaScript 中调用插入方法,在调用 Add 方法的 .NET 代码中)。本部分探讨这些差异如何影响传递给 JavaScript 的 .NET Framework 类型。

除了返回在组件中创建或从 JavaScript 传递到组件的Windows 运行时类型之外,还可以将托管类型(在托管代码中创建)返回到 JavaScript,就好像它是相应的Windows 运行时类型一样。 即使在运行时类的第一个简单示例中,成员的参数和返回类型也是 Visual Basic 或 C# 基元类型,它们是 .NET Framework 类型。 若要对集合进行演示,请将以下代码添加到 Example 类,以创建返回字符串的泛型字典的方法(按整数编制索引):

public static IDictionary<int, string> GetMapOfNames()
{
    Dictionary<int, string> retval = new Dictionary<int, string>();
    retval.Add(1, "one");
    retval.Add(2, "two");
    retval.Add(3, "three");
    retval.Add(42, "forty-two");
    retval.Add(100, "one hundred");
    return retval;
}
Public Shared Function GetMapOfNames() As IDictionary(Of Integer, String)
    Dim retval As New Dictionary(Of Integer, String)
    retval.Add(1, "one")
    retval.Add(2, "two")
    retval.Add(3, "three")
    retval.Add(42, "forty-two")
    retval.Add(100, "one hundred")
    Return retval
End Function

请注意,字典必须作为由 Dictionary<TKey, TValue> 实现且映射到 Windows 运行时接口的接口返回。 在本例中,接口为 Visual Basic 中的 IDictionary<int、string> (整数、字符串)。 当Windows 运行时类型 IMap<int 时,字符串>将传递给托管代码,当托管类型传递给 JavaScript 时,它显示为 IDictionary<int、string>,而反向为 true。

重要说明 当托管类型实现多个接口时,JavaScript 将使用列表中第一个显示的接口。 例如,如果将 Dictionary<int、字符串> 返回到 JavaScript 代码,则无论指定为返回类型的接口,它都显示为 IDictionary<int,字符串> 。 这意味着,如果第一个接口不包含在后续接口上显示的成员,则该成员对 JavaScript 不可见。

 

若要测试新方法并使用字典,请添加 returns1 并返回 2 个函数以default.js:

var names;

function returns1() {
    names = SampleComponent.Example.getMapOfNames();
    document.getElementById('output').innerHTML = showMap(names);
}

var ct = 7;

function returns2() {
    if (!names.hasKey(17)) {
        names.insert(43, "forty-three");
        names.insert(17, "seventeen");
    }
    else {
        var err = names.insert("7", ct++);
        names.insert("forty", "forty");
    }
    document.getElementById('output').innerHTML = showMap(names);
}

function showMap(map) {
    var item = map.first();
    var retval = "<ul>";

    for (var i = 0, len = map.size; i < len; i++) {
        retval += "<li>" + item.current.key + ": " + item.current.value + "</li>";
        item.moveNext();
    }
    return retval + "</ul>";
}

将事件注册代码添加到同一事件注册代码,然后阻止其他事件注册代码:

var returnsButton1 = document.getElementById("returnsButton1");
returnsButton1.addEventListener("click", returns1, false);
var returnsButton2 = document.getElementById("returnsButton2");
returnsButton2.addEventListener("click", returns2, false);

关于此 JavaScript 代码,需要注意一些有趣的事项。 首先,它包括一个 showMap 函数,用于在 HTML 中显示字典的内容。 在 showMap 的代码中,请注意迭代模式。 在 .NET 中,泛型 IDictionary 接口上不存在 First 方法,并且大小由 Count 属性而非 Size 方法返回。 到 JavaScript,IDictionary<int,字符串>似乎是Windows 运行时类型 IMap<int,字符串>。 (请参阅 IMap<K,V> 接口。

在 returns2 函数中,如前面的示例中所示,JavaScript 调用 Insert 方法(在 JavaScript 中插入)以将项添加到字典。

若要运行应用,请选择 F5 键。 若要创建和显示字典的初始内容,请选择 “返回 1 ”按钮。 若要向字典再添加两个条目,请选择 “返回 2 ”按钮。 请注意,条目按插入顺序显示,正如字典<TKey、TValue> 所期望的那样。 如果需要对其进行排序,可以从 GetMapOfNames 返回 SortedDictionary<int 字符串> 。 (前面示例中使用的 PropertySet 类具有与 Dictionary< 不同的内部组织TKey,TValue>.)

当然,JavaScript 不是强类型语言,因此使用强类型泛型集合可能会导致一些令人惊讶的结果。 再次选择“返回 2”按钮。 JavaScript 强制将“7”强制转换为数字 7,并将存储在 ct 中的数字 7 强制转换为字符串。 它将字符串“44”强制为零。 但这只是开始。 多次选择“返回 2”按钮。 在托管代码中,Add 方法将生成重复的键异常,即使这些值被强制转换为正确的类型也是如此。 相反,Insert 方法更新与现有键关联的值,并返回一个布尔值,该值指示是否向字典添加新键。 这就是为什么与键 7 关联的值不断更改的原因。

另一个意外行为:如果将未分配的 JavaScript 变量作为字符串参数传递,则得到的是字符串“undefined”。 简言之,在将 .NET Framework 集合类型传递给 JavaScript 代码时要小心。

请注意 ,如果要连接大量文本,可以通过将代码移动到 .NET Framework 方法并使用 StringBuilder 类(如 showMap 函数中所示)更有效地执行此操作。

虽然无法从Windows 运行时组件公开自己的泛型类型,但可以使用如下代码为Windows 运行时类返回 .NET Framework 泛型集合:

public static object GetListOfThis(object obj)
{
    Type target = obj.GetType();
    return Activator.CreateInstance(typeof(List<>).MakeGenericType(target));
}
Public Shared Function GetListOfThis(obj As Object) As Object
    Dim target As Type = obj.GetType()
    Return Activator.CreateInstance(GetType(List(Of )).MakeGenericType(target))
End Function

列出<T> 实现 IList<T>,该 T 在 JavaScript 中显示为Windows 运行时类型 IVector<T>。

声明事件

可以使用标准 .NET Framework 事件模式或Windows 运行时使用的其他模式来声明事件。 .NET Framework 支持 System.EventHandler<TEventArgs> 委托与 Windows 运行时 EventHandler<T> 委托之间的等效性,因此使用 EventHandler<TEventArgs> 是实现标准 .NET Framework 模式的好方法。 若要查看其工作原理,请将以下一对类添加到 SampleComponent 项目:

namespace SampleComponent
{
    public sealed class Eventful
    {
        public event EventHandler<TestEventArgs> Test;
        public void OnTest(string msg, long number)
        {
            EventHandler<TestEventArgs> temp = Test;
            if (temp != null)
            {
                temp(this, new TestEventArgs()
                {
                    Value1 = msg,
                    Value2 = number
                });
            }
        }
    }

    public sealed class TestEventArgs
    {
        public string Value1 { get; set; }
        public long Value2 { get; set; }
    }
}
Public NotInheritable Class Eventful
    Public Event Test As EventHandler(Of TestEventArgs)
    Public Sub OnTest(ByVal msg As String, ByVal number As Long)
        RaiseEvent Test(Me, New TestEventArgs() With {
                            .Value1 = msg,
                            .Value2 = number
                            })
    End Sub
End Class

Public NotInheritable Class TestEventArgs
    Public Property Value1 As String
    Public Property Value2 As Long
End Class

在Windows 运行时中公开事件时,事件参数类继承自 System.Object。 它不会从 System.EventArgs 继承,如同在 .NET 中一样,因为 EventArgs 不是 Windows 运行时类型。

如果为事件声明自定义事件访问器(Visual Basic 中的自定义关键字),则必须使用Windows 运行时事件模式。 请参阅 Windows 运行时组件中的自定义事件和事件访问器

若要处理 Test 事件,请将 events1 函数添加到default.js。 events1 函数为 Test 事件创建事件处理程序函数,并立即调用 OnTest 方法引发事件。 如果在事件处理程序的正文中放置断点,可以看到传递给单个参数的对象包括源对象和 TestEventArgs 的两个成员。

var ev;

function events1() {
   ev = new SampleComponent.Eventful();
   ev.addEventListener("test", function (e) {
       document.getElementById('output').innerHTML = e.value1;
       document.getElementById('output').innerHTML += "<br/>" + e.value2;
   });
   ev.onTest("Number of feet in a mile:", 5280);
}

将事件注册代码添加到同一事件注册代码,然后阻止其他事件注册代码:

var events1Button = document.getElementById("events1Button");
events1Button.addEventListener("click", events1, false);

公开异步操作

.NET Framework 具有一组丰富的工具,用于基于 Task 和泛型 Task<TResult> 类进行异步处理和并行处理。 若要在Windows 运行时组件中公开基于任务的异步处理,请使用Windows 运行时接口 IAsyncAction、IAsyncActionWithProgress TProgress<>、IAsyncOperation<TResult>IAsyncOperationWithProgress TResult、TProgress。<> (在Windows 运行时中,操作返回结果,但操作不返回。

本部分演示报告进度并返回结果的可取消异步操作。 GetPrimesInRangeAsync 方法使用 AsyncInfo 类生成任务,并将其取消和进度报告功能连接到 WinJS.Promise 对象。 首先将 GetPrimesInRangeAsync 方法添加到示例类:

using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Foundation;

public static IAsyncOperationWithProgress<IList<long>, double>
GetPrimesInRangeAsync(long start, long count)
{
    if (start < 2 || count < 1) throw new ArgumentException();

    return AsyncInfo.Run<IList<long>, double>((token, progress) =>

        Task.Run<IList<long>>(() =>
        {
            List<long> primes = new List<long>();
            double onePercent = count / 100;
            long ctProgress = 0;
            double nextProgress = onePercent;

            for (long candidate = start; candidate < start + count; candidate++)
            {
                ctProgress += 1;
                if (ctProgress >= nextProgress)
                {
                    progress.Report(ctProgress / onePercent);
                    nextProgress += onePercent;
                }
                bool isPrime = true;
                for (long i = 2, limit = (long)Math.Sqrt(candidate); i <= limit; i++)
                {
                    if (candidate % i == 0)
                    {
                        isPrime = false;
                        break;
                    }
                }
                if (isPrime) primes.Add(candidate);

                token.ThrowIfCancellationRequested();
            }
            progress.Report(100.0);
            return primes;
        }, token)
    );
}
Imports System.Runtime.InteropServices.WindowsRuntime

Public Shared Function GetPrimesInRangeAsync(ByVal start As Long, ByVal count As Long)
As IAsyncOperationWithProgress(Of IList(Of Long), Double)

    If (start < 2 Or count < 1) Then Throw New ArgumentException()

    Return AsyncInfo.Run(Of IList(Of Long), Double)( _
        Function(token, prog)
            Return Task.Run(Of IList(Of Long))( _
                Function()
                    Dim primes As New List(Of Long)
                    Dim onePercent As Long = count / 100
                    Dim ctProgress As Long = 0
                    Dim nextProgress As Long = onePercent

                    For candidate As Long = start To start + count - 1
                        ctProgress += 1

                        If ctProgress >= nextProgress Then
                            prog.Report(ctProgress / onePercent)
                            nextProgress += onePercent
                        End If

                        Dim isPrime As Boolean = True
                        For i As Long = 2 To CLng(Math.Sqrt(candidate))
                            If (candidate Mod i) = 0 Then
                                isPrime = False
                                Exit For
                            End If
                        Next

                        If isPrime Then primes.Add(candidate)

                        token.ThrowIfCancellationRequested()
                    Next
                    prog.Report(100.0)
                    Return primes
                End Function, token)
        End Function)
End Function

GetPrimesInRangeAsync 是一个非常简单的质数查找器,设计就是这样。 此处的重点是实现异步操作,因此简单性非常重要,当我们演示取消时,实现速度缓慢是一个优势。 GetPrimesInRangeAsync 通过暴力破解查找质素:它将候选项除以小于或等于其平方根的所有整数,而不是仅使用质数。 单步执行以下代码:

  • 在启动异步操作之前,请执行管家活动,例如验证参数并引发无效输入的异常。

  • 此实现的关键是 AsyncInfo.Run<TResult, TProgress>(Func<CancellationToken, IProgress<TProgress>, Task<TResult>>) 方法,以及作为该方法唯一参数的委托。 委托必须接受取消令牌和用于报告进度的接口,并且必须返回使用这些参数的启动任务。 当 JavaScript 调用 GetPrimesInRangeAsync 方法时,将执行以下步骤(不一定按此处给出的顺序):

    • WinJS.Promise 对象提供函数来处理返回的结果、响应取消和处理进度报告。

    • AsyncInfo.Run 方法创建取消源和实现 IProgress<T> 接口的对象。 对委托,它将从取消源和 IProgress<T> 接口传递 CancellationToken 令牌。

      请注意 ,如果 Promise 对象不提供响应取消的函数,AsyncInfo.Run 仍会传递可取消令牌,并且仍会发生取消。 如果 Promise 对象不提供用于处理进度更新的函数,AsyncInfo.Run 仍提供实现 IProgress<T> 的对象,但其报告将被忽略。

    • 委托使用 Task.Run<TResult>(Func<TResult>, CancellationToken) 方法创建使用令牌和进度接口的启动任务。 启动任务的委托由计算所需结果的 lambda 函数提供。 稍后我们将对此进行详细介绍。

    • AsyncInfo.Run 方法创建一个对象,该对象实现 IAsyncOperationWithProgress<TResult、TProgress> 接口、将Windows 运行时取消机制与令牌源连接,并将 Promise 对象的进度报告函数与 IProgress<T> 接口连接起来。

    • IAsyncOperationWithProgress TResult,TProgress<> 接口将返回到 JavaScript。

  • 由启动任务表示的 lambda 函数不采用任何参数。 因为它是 lambda 函数,因此可以访问令牌和 IProgress 接口。 每次计算候选数时,lambda 函数:

    • 检查是否已达到下一个进度百分点。 如果有,lambda 函数将调用 IProgress<T>。报表方法,百分比将传递给 Promise 对象为报告进度指定的函数。
    • 如果操作已被取消,则使用取消令牌引发异常。 如果已调用 IAsyncInfo.Cancel 方法(IAsyncOperationWithProgress TResult,TProgress<> 接口继承),则 AsyncInfo.Run 方法设置的连接可确保通知取消令牌。
  • 当 lambda 函数返回质数列表时,该列表将传递给 WinJS.Promise 对象为处理结果指定的函数。

若要创建 JavaScript 承诺并设置取消机制,请将 asyncRun 和 asyncCancel 函数添加到default.js。

var resultAsync;
function asyncRun() {
    document.getElementById('output').innerHTML = "Retrieving prime numbers.";
    btnAsync.disabled = "disabled";
    btnCancel.disabled = "";

    resultAsync = SampleComponent.Example.getPrimesInRangeAsync(10000000000001, 2500).then(
        function (primes) {
            for (i = 0; i < primes.length; i++)
                document.getElementById('output').innerHTML += " " + primes[i];

            btnCancel.disabled = "disabled";
            btnAsync.disabled = "";
        },
        function () {
            document.getElementById('output').innerHTML += " -- getPrimesInRangeAsync was canceled. -- ";

            btnCancel.disabled = "disabled";
            btnAsync.disabled = "";
        },
        function (prog) {
            document.getElementById('primeProg').value = prog;
        }
    );
}

function asyncCancel() {    
    resultAsync.cancel();
}

不要忘记与之前相同的事件注册代码。

var btnAsync = document.getElementById("btnAsync");
btnAsync.addEventListener("click", asyncRun, false);
var btnCancel = document.getElementById("btnCancel");
btnCancel.addEventListener("click", asyncCancel, false);

通过调用异步 GetPrimesInRangeAsync 方法,asyncRun 函数将创建 WinJS.Promise 对象。 该对象的 then 方法采用三个函数来处理返回的结果、响应错误(包括取消),并处理进度报告。 在此示例中,返回的结果在输出区域中打印。 取消或完成会重置启动和取消操作的按钮。 进度报告更新进度控件。

asyncCancel 函数只调用 WinJS.Promise 对象的 cancel 方法。

若要运行应用,请选择 F5 键。 若要启动异步操作,请选择 “异步 ”按钮。 接下来会发生什么取决于计算机的速度。 如果在有时间闪烁之前进度栏压缩到完成状态,请将传递给 GetPrimesInRangeAsync 的起始编号的大小增加一个或多个十个因素。 可以通过增加或减少要测试的数字计数来微调操作持续时间,但在起始数中间添加零将产生更大的影响。 若要取消操作,请选择 “取消异步 ”按钮。