使用 C# 和 Visual Basic 创建 Windows 运行时组件

你可以使用托管代码创建自己的 Windows 运行时类型并将其打包在 Windows 运行时组件中。 可在以 C++、JavaScript、Visual Basic 或 C# 编写的通用 Windows 平台 (UWP) 应用中使用你的组件。 本主题概述了用于创建组件的规则,并讨论了有关 Windows 运行时的 .NET 支持的一些方面。 一般情况下,该支持设计为对 .NET 程序员透明可见。 但是,在你创建要与 JavaScript 或 C++ 一起使用的组件时,需要意识到这些语言支持 Windows 运行时的方法差异。

如果你要创建仅在以 Visual Basic 或 C# 编写的 UWP 应用中使用的组件,并且该组件不包含 UWP 控件,请考虑在 Microsoft Visual Studio 中使用“类库”模板而不是“Windows 运行时组件”项目模板。 简单类库的限制较少。

注意

对于在 .NET 6 或更高版本中编写桌面应用的 C# 开发人员,请使用 C#/WinRT 创作 Windows 运行时组件。 请参阅使用 C#/WinRT 创作 Windows 运行时组件

声明 Windows 运行时组件中的类型

在内部,组件中的 Windows 运行时类型可以使用任何 UWP 应用中允许的 .NET 功能。 有关详细信息,请参阅适用于 UWP 应用的 .NET

在外部,类型的成员只能为其参数和返回值公开 Windows 运行时类型。 下表介绍 Windows 运行时组件公开的 .NET 类型的限制。

  • 组件中所有公共类型和成员的字段、参数和返回值必须Windows 运行时类型。 此限制包括你创建的 Windows 运行时类型以及 Windows 运行时本身提供的类型。 它还包括许多 .NET 类型。 包括这些类型是支持 .NET 在托管代码中自然使用 Windows 运行时的一部分 — 你的代码看起来像使用熟悉的 .NET 类型,而非基本的 Windows 运行时类型。 例如,你可以使用 .NET 基元类型(例如 Int32 和双精度型)、某些基本类型(例如 DateTimeOffset 和 Uri)以及一些常用的泛型接口类型(例如 IEnumerable<T>,这在 Visual Basic 中是 IEnumerable(Of T) 和 IDictionary<TKey,TValue>)。 请注意,以下泛型类型的类型参数必须为 Windows 运行时类型。 本主题稍后的将 Windows 运行时类型传递给托管代码将托管类型传递给 Windows 运行时部分对此进行了讨论。

  • 公共类和接口可以包含方法、属性和事件。 可以为事件声明委托或使用 EventHandler<T> 委托。 公共类或接口无法:

    • 泛型。
    • 实现一个不是 Windows 运行时接口的接口(但是,你可以创建自己的 Windows 运行时接口并实现它们)。
    • 从 Windows 运行时之外的类型(例如 System.Exception 和 System.EventArgs)进行派生
  • 所有公共类型都必须具有与程序集名称匹配的根命名空间,并且程序集名称不得以“Windows”开头。

    提示。 默认情况下,Visual Studio 项目具有与程序集名称匹配的命名空间名称。 在 Visual Basic 中,此默认命名空间的 Namespace 语句未显示在代码中。

  • 公共结构不能包含除公共字段以外的任何成员,并且这些字段必须是值类型或字符串。

  • 公共类必须密封(在 Visual Basic 中不可密封)。 如果编程模型要求具有多形性,则你可以创建公共接口,并且在必须为多形性的类上实现该接口。

调试组件

如果 UWP 应用和组件均通过托管代码生成,则你可以同时调试它们。

在使用 C++ 将组件作为 UWP 应用的一部分进行测试时,可以同时调试托管代码和本机代码。 默认值仅为本机代码。

调试本机C++代码和托管代码

  1. 打开 Visual C++ 项目的快捷菜单,然后选择“ 属性”。
  2. 在属性页的“配置属性”,选择“调试”。
  3. 选择“调试器类型”,然后在下拉列表框中将“本机仅”更改为“混合”(托管和本机)。 选择 “确定”
  4. 在本机代码和托管代码中设置断点。

在使用 JavaScript 将组件作为 UWP 应用的一部分进行测试时,默认情况下该解决方案处于 JavaScript 调试模式。 在 Visual Studio 中,不能同时调试 JavaScript 和托管代码。

调试托管代码而不是 JavaScript

  1. 打开 JavaScript 项目的快捷菜单,然后选择“ 属性”。
  2. 在属性页的“配置属性”,选择“调试”。
  3. 选择调试器类型,然后在下拉列表框中将“仅脚本”更改为“仅托管”。 选择 “确定”
  4. 在托管代码中设置断点,并照常调试。

将Windows 运行时类型传递给托管代码

如之前在声明 Windows 运行时组件中的类型部分中所述,某些 .NET 类型可以显示在公共类成员的签名中。 这是支持 .NET 在托管代码中自然使用 Windows 运行时的一部分。 它包括基元类型和某些类和接口。 在通过 JavaScript 或 C++ 代码使用组件时,请务必知晓向调用方显示 .NET 类型的方式。 有关 JavaScript 示例,请参阅创建 C# 或 Visual Basic Windows 运行时组件并通过 JavaScript 调用此组件的演练。 本部分讨论常用类型。

在 .NET 中,基元类型(例如 Int32 结构)具有许多有用的属性和方法,例如 TryParse 方法。 相比之下,Windows 运行时中的基元类型和结构只有字段。 在将这些类型传递到托管代码时,它们显示为 .NET 类型,并且你可以像往常一样使用 .NET 类型的属性和方法。 以下列表汇总了 IDE 中自动进行的替换:

  • 对于 Windows 运行时基元 Int32、Int64、单精度型、双精度型、布尔型、字符串(不可变的 Unicode 字符集合)、Enum、UInt32、UInt64 和 Guid,请在系统命名空间中使用相同名称的类型
  • 对于 UInt8,请使用 System.Byte
  • 对于 Char16,请使用 System.Char
  • 对于 IInspectable 接口,请使用 System.Object

如果 C# 或 Visual Basic 为任意一种类型提供语言关键字,则你可以改为使用语言关键字。

除基元类型外,一些基本的常用 Windows 运行时类型作为其 .NET 等效项显示在托管代码中。 例如,假设 JavaScript 代码使用 Windows.Foundation.Uri 类,并且你希望将其传递到 C# 或 Visual Basic 方法。 托管代码中的等效类型是 .NET System.Uri 类,并且这是用于方法参数的类型。 你可以在 Windows 运行时类型显示为 .NET 类型时进行区分,因为 Visual Studio 中的 IntelliSense 在编写托管代码时会隐藏 Windows 运行时类型并显示等效的 .NET 类型。 (通常这两种类型具有相同的名称。但是,请注意,Windows.Foundation.DateTime 结构以 System.DateTimeOffset 而不是 System.DateTime 的形式出现在托管代码中。

对于一些常用的集合类型,映射介于由 Windows 运行时类型实现的接口和由相应的 .NET 类型实现的接口之间。 与上述类型一样,你使用 .NET 类型声明参数类型。 这会隐藏类型之间的一些差异,并使编写 .NET 代码变得更加自然。

下表列出了这些泛型接口类型中最常见的类型,以及其他通用类和接口映射。 有关 .NET 映射的 Windows 运行时类型的完整列表,请参阅 Windows 运行时类型的 .NET 映射

Windows 运行时 .NET
IIterable<T> IEnumerable<T>
IVector<T> IList<T>
IVectorView<T> IReadOnlyList<T>
IMap<K, V> IDictionary<TKey, TValue>
IMapView<K、V> IReadOnlyDictionary<TKey, TValue>
IKeyValuePair<K, V> KeyValuePair<TKey, TValue>
IBindableIterable IEnumerable
IBindableVector IList
Windows.UI.Xaml.Data.INotifyPropertyChanged System.ComponentModel.INotifyPropertyChanged
Windows.UI.Xaml.Data.PropertyChangedEventHandler System.ComponentModel.PropertyChangedEventHandler
Windows.UI.Xaml.Data.PropertyChangedEventArgs System.ComponentModel.PropertyChangedEventArgs

当类型实现多个接口时,可以使用它实现的任何接口作为参数类型或成员的返回类型。 例如,你可以将 Dictionary<int, string>(在 Visual Basic 中是 Dictionary(Of Integer, String))传递或返回为 IDictionary<int, string>、IReadOnlyDictionary<int, string> 或 IEnumerable<System.Collections.Generic.KeyValuePair<TKey, TValue>>

重要

JavaScript 使用在托管类型实现的接口列表中首先显示的接口。 例如,如果你将 Dictionary<int, string> 返回到 JavaScript 代码,它会显示为 IDictionary<int, string>,无论你指定哪个接口作为返回类型都是如此。 这意味着,如果第一个接口不包含在后续接口上显示的成员,则该成员对 JavaScript 不可见。

在 Windows 运行时中,IMap<K, V> 和 IMapView<K, V> 使用 IKeyValuePair 进行迭代。 在将它们传递到托管代码时,它们显示为 IDictionary<TKey, TValue> 和 IReadOnlyDictionary<TKey, TValue>,所以自然而然地,你会使用 System.Collections.Generic.KeyValuePair<TKey, TValue> 枚举它们

接口在托管代码中的显示方式将影响实现这些接口的类型的显示方式。 例如,PropertySet 类实现 IMap<K, V>,而它在托管代码中显示为 IDictionary<TKey, TValue>。 PropertySet 显示为好像实现了 IDictionary<TKey, TValue> 而非 IMap<K, V>,所以在托管代码中,它似乎具有 Add 方法,其行为类似于 .NET 字典上的 Add 方法。 它不会显示为具有 Insert 方法。 可以在主题创建 C# 或 Visual Basic Windows 运行时组件并通过 JavaScript 调用此组件的演练中查看此示例。

将托管类型传递给Windows 运行时

如前面部分中所述,一些 Windows 运行时类型可在组件成员的签名或 Windows 运行时成员的签名中显示为 .NET 类型(如果你在 IDE 中使用这些类型)。 在你将 .NET 类型传递到这些成员或将它们用作组件成员的返回值时,它们将作为相应的 Windows 运行时类型显示为另一侧的代码。 有关此操作在通过 JavaScript 调用组件时产生的影响的示例,请参阅创建 C# 或 Visual Basic Windows 运行时组件并通过 JavaScript 调用此组件的演练中的“从组件返回托管的类型”部分。

重载的方法

在Windows 运行时中,可以重载方法。 但是,如果使用相同数量的参数声明多个重载,必须将 Windows.Foundation.Metadata.DefaultOverloadAttribute 属性仅应用到其中一个重载。 该重载是唯一可从 JavaScript 调用的重载。 例如,在以下代码中,采用 intVisual Basic 中的整数 )的重载是默认重载。

public string OverloadExample(string s)
{
    return s;
}

[Windows.Foundation.Metadata.DefaultOverload()]
public int OverloadExample(int x)
{
    return x;
}
Public Function OverloadExample(ByVal s As String) As String
    Return s
End Function

<Windows.Foundation.Metadata.DefaultOverload> _
Public Function OverloadExample(ByVal x As Integer) As Integer
    Return x
End Function

[IMPORTANT] JavaScript 允许你将任何值传递到 OverloadExample,并且将该值强制转换为参数所需的类型。 你可以通过“四十二”、“42”或“42.3”调用 OverloadExample,但所有这些值均会传递到默认重载。 之前示例中的默认重载分别返回 0、42 和 42。

无法将 DefaultOverloadAttribute 属性应用到构造函数。 类中的所有构造函数必须具有不同的参数数。

实现 IStringable

从 Windows 8.1 开始,Windows 运行时包括 IStringable 接口,该接口的一项方法 IStringable.ToString 提供的基本格式支持可与 Object.ToString 提供的格式支持相媲美。 如果你确实选择在 Windows 运行时组件中导出的公共托管类型中实现 IStringable,将会应用以下限制

  • 仅可以在“类实现”关系中定义 IStringable 接口,例如以下使用 C# 编写的代码

    public class NewClass : IStringable
    

    或以下 Visual Basic 代码:

    Public Class NewClass : Implements IStringable
    
  • 不能在接口上实现 IStringable

  • 不能将参数声明为属于类 IStringable

  • IStringable 不能是方法、属性或字段的返回类型

  • 不能使用如下方法定义从基类隐藏 IStringable 实现

    public class NewClass : IStringable
    {
       public new string ToString()
       {
          return "New ToString in NewClass";
       }
    }
    

    相反,IStringable.ToString 实现必须始终重写基类实现。 仅可以通过在强类型的类实例上调用 ToString 实现来隐藏它

注意

在许多情况下,对实现 IStringable 或隐藏其 ToString 实现的托管类型的本地代码调用会产生意外行为

异步操作

若要在组件中实现异步方法,请将“Async”添加到方法名称的末尾并返回一个表示异步操作的 Windows 运行时接口:IAsyncAction、IAsyncActionWithProgress<TProgress>、IAsyncOperation<TResult> 或 IAsyncOperationWithProgress<TResult, TProgress>

可以使用 .NET 任务(Task 类和泛型 Task<TResult> 类)实现异步方法。 必须返回表示操作正在运行的任务,例如从使用 C# 或 Visual Basic 编写的异步方法返回的任务或从 Task.Run 方法返回的任务。 如果使用构造函数创建任务,则必须在返回任务之前调用其 Task.Start 方法。

使用 await(在 Visual Basic 中为 Await)的方法需要 async 关键字(在 Visual Basic 中为 Async)。 如果从 Windows 运行时组件公开此类方法,请将 async 关键字应用于传递到 Run 方法的委托

对于不支持取消或进度报告的异步操作和操作,可以使用 WindowsRuntimeSystemExtensions.AsAsyncActionAsAsyncOperation<TResult> 扩展方法将任务包装在相应的接口中。 例如,以下代码通过使用 Task.Run<TResult> 方法启动任务来实现异步方法。 AsAsyncOperation<TResult> 扩展方法将任务返回为 Windows 运行时异步操作

public static IAsyncOperation<IList<string>> DownloadAsStringsAsync(string id)
{
    return Task.Run<IList<string>>(async () =>
    {
        var data = await DownloadDataAsync(id);
        return ExtractStrings(data);
    }).AsAsyncOperation();
}
Public Shared Function DownloadAsStringsAsync(ByVal id As String) _
     As IAsyncOperation(Of IList(Of String))

    Return Task.Run(Of IList(Of String))(
        Async Function()
            Dim data = Await DownloadDataAsync(id)
            Return ExtractStrings(data)
        End Function).AsAsyncOperation()
End Function

以下 JavaScript 代码介绍如何使用 WinJS.Promise 对象调用该方法。 异步调用完成后,将执行传递给 then 方法的函数。 stringList 参数包含 DownloadAsStringAsync 方法返回的字符串列表,并且该函数会执行处理所需的任何操作

function asyncExample(id) {

    var result = SampleComponent.Example.downloadAsStringAsync(id).then(
        function (stringList) {
            // Place code that uses the returned list of strings here.
        });
}

对于支持取消和进度报告的异步操作,请使用 AsyncInfo 类生成启动的任务,并将任务的取消和进度报告功能与相应的 Windows 运行时接口的取消和进度报告功能连接起来。 有关支持取消和进度报告的示例,请参阅创建 C# 或 Visual Basic Windows 运行时组件并通过 JavaScript 调用此组件的演练

请注意,即使异步方法不支持取消或进度报告,也可以使用 AsyncInfo 类的方法。 如果你使用 Visual Basic lambda 函数或 C# 匿名方法,则不要为令牌和 IProgress<T> 接口提供参数。 如果使用 C# lambda 函数,请提供令牌参数,但忽略它。 在改用 AsyncInfo.Run<TResult>(Func<CancellationToken, Task<TResult>>) 方法重载时,使用了 AsAsyncOperation<TResult> 方法的上一示例与此类似。

public static IAsyncOperation<IList<string>> DownloadAsStringsAsync(string id)
{
    return AsyncInfo.Run<IList<string>>(async (token) =>
    {
        var data = await DownloadDataAsync(id);
        return ExtractStrings(data);
    });
}
Public Shared Function DownloadAsStringsAsync(ByVal id As String) _
    As IAsyncOperation(Of IList(Of String))

    Return AsyncInfo.Run(Of IList(Of String))(
        Async Function()
            Dim data = Await DownloadDataAsync(id)
            Return ExtractStrings(data)
        End Function)
End Function

如果你创建可以选择支持取消或进度报告的异步方法,请考虑添加缺少取消令牌或 IProgress<T> 接口的参数的重载。

引发异常

可以引发适用于 Windows 应用的 .NET 中包含的任何异常类型。 不能在Windows 运行时组件中声明自己的公共异常类型,但可以声明和引发非公共类型。

如果组件未处理异常,则会在调用组件的代码中引发相应的异常。 异常对调用方显示的方式取决于调用语言支持Windows 运行时的方式。

  • 在 JavaScript 中,异常显示为异常消息被堆栈跟踪替换的对象。 在 Visual Studio 中调试应用时,可以看到调试器异常对话框中显示的原始消息文本,标识为“WinRT 信息”。 无法从 JavaScript 代码访问原始消息文本。

    提示。 目前,堆栈跟踪包含托管异常类型,但我们不建议分析跟踪以标识异常类型。 请改用 HRESULT 值,如本节稍后所述。

  • 在C++中,异常显示为平台异常。 如果托管异常的 HResult 属性能够映射到特定平台异常的 HRESULT,将使用该特定异常;否则,将引发 Platform::COMException 异常。 托管异常的消息文本不适用于C++代码。 如果引发了特定的平台异常,将显示该异常类型的默认消息文本;否则,不会显示消息文本。 请参阅异常(C++/CX)。

  • 在 C# 或 Visual Basic 中,异常是正常的托管异常。

从组件引发异常时,可以通过引发 HResult 属性值特定于组件的非公共异常类型,使 JavaScript 或C++调用方更轻松地处理异常。 HRESULT 通过异常对象的编号属性提供给 JavaScript 调用方,并通过 COMException::HResult 属性提供给 C++ 调用方

注意

为 HRESULT 使用负值。 正值被解释为成功,并且 JavaScript 或C++调用方中不会引发异常。

声明和引发事件

声明类型以保存事件的数据时,请派生自 Object 而不是 EventArgs,因为 EventArgs 不是Windows 运行时类型。 使用 EventHandler<TEventArgs> 作为事件类型,并将事件参数类型用作泛型类型参数。 就像在 .NET 应用程序中一样引发该事件。

从 JavaScript 或 C++ 使用Windows 运行时组件时,该事件遵循这些语言预期的Windows 运行时事件模式。 在通过 C# 或 Visual Basic 使用组件时,事件显示为普通的 .NET 事件。 创建 C# 或 Visual Basic Windows 运行时组件并通过 JavaScript 调用此组件的演练中提供了示例。

如果实现自定义事件访问器(在 Visual Basic 中使用 Custom 关键字声明事件),则必须遵循实现中的Windows 运行时事件模式。 请参阅 Windows 运行时组件中的自定义事件和事件访问器。 请注意,在通过 C# 或 Visual Basic 代码处理事件时,它仍显示为普通的 .NET 事件。

后续步骤

在创建了 Windows 运行时组件以供自己使用后,你可能会发现它封装的功能对其他开发人员也很有用。 有两个选项可用于打包组件以便分发给其他开发人员。 请参阅分发托管Windows 运行时组件

有关 Visual Basic 和 C# 语言功能以及 Windows 运行时的 .NET 支持的详细信息,请参阅 Visual BasicC# 文档。

故障排除

症状 纠正方法
在 C++/WinRT 应用中,当使用利用 XAML 的 C# Windows 运行时组件时,编译器会生成一个错误,格式为:“'MyNamespace_XamlTypeInfo': 不是 'winrt::MyNamespace' 的成员”,其中 MyNamespace 是 Windows 运行时组件命名空间的名称。 在 C++/WinRT 应用中的 pch.h 中,根据需要添加 #include <winrt/MyNamespace.MyNamespace_XamlTypeInfo.h> 来替换 MyNamespace。