使用 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 中,此默认命名空间的命名空间声明不会显示在代码中。

  • 公共结构无法具有公共字段以外的任何成员,并且这些字段必须是值类型或字符串。

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

调试组件

如果 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> (Dictionary (Of Integer、String) 在 Visual Basic 中) 为 IDictionary<int、string>IReadOnlyDictionary<int、string>IEnumerable<System.Collections.Generic.KeyValuePair<TKey, TValue>>

重要

JavaScript 使用托管类型实现的、在接口列表中显示在最前面的接口。 例如,如果将 Dictionary<int, string> 返回到 JavaScript 代码,则无论将哪个接口指定为返回类型,它都显示为 IDictionary<int、string> 。 这意味着,如果第一个接口不包括显示在后续接口上的成员,JavaScript 将看不到该成员。

在Windows 运行时,使用 IKeyValuePair<循环访问 IMap K、V>IMapView<K、V>。 将它们传递到托管代码时,它们显示为 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 调用的重载。 例如,在以下代码中,接受 int(在 Visual Basic 中是 Integer)的重载是默认重载。

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 运行时接口之一:IAsyncActionIAsyncActionWithProgress<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关键字 (keyword) 应用于传递给 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 对象调用该方法。 然后,传递到该方法的函数在完成异步调用时执行。 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 中,异常是正常的托管异常。

在从组件引发异常时,你可以使 JavaScript 或 C++ 调用方处理异常变得更简单,方法是引发其 HResult 属性值特定于你的组件的非公共异常类型。 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。