使用 C# 构建实现接口的 WinRT 组件
本文演示了如何在 C# Windows 应用商店 DLL 或 C# Windows 应用商店应用程序中实现为 WinRT 组件定义的接口。
引言
在上一篇文章中,我展示了如何在 C++/CX 中创建实现一组接口的 WinRT 组件。为了演示,我提供了同一接口在不同组件中的几种实现。本文扩展了该练习,将向您展示如何在 C#(即 .NET)中创建 WinRT 组件,这些组件实现了在 C++/CX Windows 应用商店 DLL 中定义的接口。
我在这篇文章中展示的内容在我撰写本文时是有效的,因为在使用 Visual Studio 2012 创建这些组件时,我发现编译器中存在一些错误,希望这些错误能很快被微软修复。
背景
我建议在阅读本文之前,先阅读上一篇文章,因为我使用的接口是在那里创建的。正如我之前提到的,即使微软试图隐藏它,WinRT 实际上完全基于 Windows 3.11 为 OLE(对象链接和嵌入)引入的 COM 技术。所以如果你有 COM 知识,你会发现这非常简单。
接口
在上一篇文章中,我用 C++/CX 定义了三个非常简单的接口:IPerson
、ICitizen
和 IAddress
,并且我在 C++ 中提供了几种不同的实现。C++/CX 使创建 Windows 应用商店组件变得非常容易,以至于它甚至隐藏了这些组件只有在拥有接口的情况下才能存在的事实,因此它为您生成了一个默认接口。
但是,如果您创建自己的组件架构,您将设计自己的接口,这些接口是与组件交互的唯一方式。与基于类的设计不同,在组件设计中,对象只能通过其暴露行为和属性的接口来访问。组件和接口一样,只有公共成员,组件中没有受保护、内部甚至私有成员。只有它的接口和您无法访问的实现。
接口定义如下。
namespace WinRTCompV2
{
/**
@brief Defines the behavior of an object that can be saved.
*/
public interface class ISaveable
{
/**
Check if the object can be saved
@return true if can be saved, false otherwise
*/
bool CanSave();
};
/**
@brief Represents a simple address
*/
public interface class IAddress : ISaveable
{
/**
Street element of the address
*/
property String^ Street;
/**
ZipCode of the address
*/
property String^ ZipCode;
/**
City of the address
*/
property String^ City;
};
/**
@brief Represents a simple person object
*/
public interface class IPerson : ISaveable
{
/**
Name property for the person
*/
property String^ Name;
/**
Name property for the person
*/
property String^ Surname;
};
/**
@brief Represents a citizen, defined as a person with an address
*/
public interface class ICitizen : IPerson
{
property IAddress^ Address;
};
}
C++/CX 将关键字 interface 定义为 C++ 语言的扩展。当您创建 WinRT 组件时,此接口不像 C# 中那样是对托管对象的引用,它实际上指向内存中的物理地址。WinRT 组件是本机的。当然,当您要在 C# 中实现它时,代码将在 CLR 中执行,但是当您在 C++ 程序中创建它时获得的指向此对象的指针是指向一种引导程序的指针,该引导程序正在调用 C# 实现。
COM 的一个特点是您可以用不同的语言(如 C++ 或 VB,后来是 .NET 中的 C#)实现 COM 组件。WinRT 也不例外,这就是为什么我们也可以在 C# 中实现 WinRT 组件。事实上,我怀疑当您在 C# 中实现 WinRT 组件时,幕后的代码与在 C# 中实现的 COM 组件的代码非常相似!
在 Windows 应用商店 C# DLL 中实现接口
在 Visual Studio 中,如果您选择 C# 和 Windows 应用商店,您会看到几个项目模板。我们现在感兴趣的是 Windows 运行时组件。在 Windows 运行时组件中,所有标记为 public 的类都将作为 WinRT 组件导出,这意味着它们将是现代 COM 对象!
使用 C# 类创建 WinRT 组件时,您必须遵循一些规则。
- 该类**必须**标记为 sealed
- 该类**不能**继承自任何其他类
- 该类可以实现一个或多个接口
我第一次实现接口 IPerson
如下:
public sealed class Person : IPerson
{
#region Fields
private string name;
private string surname;
#endregion
#region Constructors
public Person()
{
}
public Person(string name, string surname)
{
this.name = name;
this.surname = surname;
}
#endregion
public string Name
{
get { return name; }
set { name = value; }
}
public string Surname
{
get { return surname; }
set { surname = value; }
}
public bool CanSave()
{
return name.Length > 0 && surname.Length > 0;
}
}
乍一看,这段代码看起来是正确的,而且它应该是正确的,因为这是当您要求编译器实现接口时它为您生成的代码。
但实际上,这段代码无法编译,因为 C# 编译器目前存在一个错误。如果您尝试编译这段代码,您将对 Name
和 Surname
属性都收到以下错误:
方法 'WinRTCompNetDll.Person.WinRTCompV2.IPerson.set_Name(System.String)
' 的参数名为 '',而实现的接口方法 'WinRTCompV2.IPerson.Name.set(System.String)
' 中的相应参数名为 '__param0
'。请确保名称相同。
这个问题微软 Visual Studio 产品组已知晓,他们很友好地给了我两种解决方法,以便能够在 C# 组件中实现 C++/CX DLL 中定义的接口。
创建 C# 组件的第一个也是最有效的解决方案是简单地显式实现接口。现在可以编译的代码如下所示:
public sealed class Person : IPerson
{
#region Fields
private string name;
private string surname;
#endregion
#region Constructors
public Person()
{
}
public Person(string name, string surname)
{
this.name = name;
this.surname = surname;
}
#endregion
string IPerson.Name
{
get { return name; }
set { name = value; }
}
string IPerson.Surname
{
get { return surname; }
set { surname = value; }
}
public bool CanSave()
{
return name.Length > 0 && surname.Length > 0;
}
}
这个解决方案在使用组件的方式上有一点副作用。在简化 COM 的过程中,WinRT 的架构师决定编译器将自动从实现类的公共成员生成一个接口。与纯 COM 不同,您不需要总是有一个接口来使用组件,如果您只需要组件的默认接口,您可以直接使用该类。问题是现在我们类 Person 的属性是私有的,只有当您显式获取接口时才能使用。如果您尝试直接使用实现类,您将只看到方法 CanSave()
。这方面在项目的单元测试中得到了演示。
他们给我的第二个解决方案是在 Windows 应用商店应用程序中实现组件。
在 Windows 应用商店 C# 应用程序中实现接口
事实上,这是微软一位非常有帮助的支持人员给我的第一个解决方案,当我尝试它时,我发现了一些问题,我想与 CodeProject 的读者分享。
在 Windows 应用商店组件 DLL 中,如果您尝试创建一个未标记为 sealed 的公共类,您将收到编译错误。但是,当我尝试在 Windows 应用商店应用程序中实现我的接口时,我发现我可以创建一个实现 C++/CX 接口的公共类,该接口未标记为 sealed... 这违反了所有 Windows 应用商店组件类都必须标记为 sealed 的规则。
然后,因为我能够创建这些未密封的类,我尝试创建一个继承自前一个类并实现我的一个 C++/CX 接口的公共密封类。这违反了 WinRT 组件类的另一个规则,即您不能继承自另一个类。
以下是这些类的代码。
namespace WinRTCompNet
{
public class Person : IPerson
{
protected string display = string.Empty;
private string name = string.Empty;
private string surname = string.Empty;
public Person()
{
}
public Person(string name, string surname)
{
this.name = name;
this.surname = surname;
FormatDisplay();
}
public string Name
{
get { return name; }
set
{
name = value;
FormatDisplay();
}
}
public string Surname
{
get { return surname; }
set
{
surname = value;
FormatDisplay();
}
}
public bool CanSave()
{
return Name.Length > 0 && Surname.Length > 0;
}
private void FormatDisplay()
{
display = string.Format("{0} {1}", name, surname);
}
}
}
namespace WinRTCompNet
{
public sealed class Citizen : Person, ICitizen
{
public Citizen() :
base()
{
this.Address = new Address();
}
public Citizen(string name, string surname, Address address) :
base(name, surname)
{
this.Address = address;
}
public IAddress Address
{
get;
set;
}
public string FormatDisplay()
{
return display;
}
public bool CanSave()
{
return base.CanSave() && Address.CanSave();
}
}
}
除了密封和继承问题之外,还有更多问题需要考虑。我在类 Person
中声明了一个受保护成员,以及一个私有方法,其唯一目的是设置受保护成员的值。然后在继承类中,我创建了一个与基类的私有方法同名的公共方法,它返回受保护成员的值。所有这些都编译并完美运行!
您还可以注意到,在 Windows 应用商店应用程序中,接口 IPerson
的公共实现是有效的,而它在 DLL 中会产生编译错误。
我已经在 C# 单元测试中测试了这个实现,它有效。在这个测试中,我声明了一个 IPerson
类型的变量,并将 C++/CX 实现或 Windows 应用商店应用程序中的 C# 实现分配给这个接口。我还在一个非常简单的应用程序中使用了在 Exe 中实现的组件。这两个测试似乎证明这些“非法”组件可以正常使用。但是,我想通过在 C++ 单元测试中使用它们来 100% 确定它们确实是 WinRT 组件。
Exe 可以在项目中引用,并且智能感知可以看到命名空间和组件,所以我写了一个简单的测试,但不幸的是 DLL 无法编译。奇怪的是编译器根本找不到命名空间……而智能感知可以。我已经向微软提交了这些问题,我希望很快能得到解释或解决方案。
关注点
我给出的例子非常简单,但它说明了如果您尝试在 C# 中实现您在 C++/CX 组件中定义的接口时会遇到的一些问题,所以我希望您会觉得它有用。我个人在网上搜索了这个问题,因为我找不到解决这个问题的任何方法,我在微软论坛上发了一个帖子,他们很快给了我答复,并对这个问题进行了很好的跟进。
WinRT 即使是建立在 COM 这样成熟的技术之上,看起来仍然处于成熟的早期阶段。很明显,Visual Studio 对 WinRT 的支持仍然需要微软 VS 产品组进行完善。但它是 Windows 应用程序开发令人兴奋的演进,它使 C++ 和组件架构开发重新焕发活力。