![](https://i-blog.csdnimg.cn/blog_migrate/881ddd080324f2a05413322224a2345e.png)
以前,我们研究了Uno Platform如何将XAML标记文件转换为C#代码。 在本文中,我将讨论Uno使用代码生成的另一种方式,它使我们能够使本机Android和iOS视图符合UWP的API,并解决棘手的多重继承问题。
想要这一切
Uno在Android和iOS上的部分功能是能够轻松地将UWP视图类型与纯本地视图混合在一起。 这是可能的,因为在Uno中,所有视图都继承自本机基本视图类型:Android上的视图 ,iOS上的UIView 。
但是,正如我在之前的文章中提到的那样,这对再现UWP的继承层次结构构成了挑战。 UIElement是UWP中的原始视图类型,但它又从DependencyObject类派生。 DependencyObject
是具有DependencyProperties
所有对象(即支持数据绑定的任何对象)的基类。 这包括所有视图以及一些非视图框架类型,例如Transforms和Brushes 。
我们想从ViewGroup
或UIView
继承。 我们还想从DependencyObject.
继承DependencyObject.
C#不允许多重继承,那么我们该怎么办? 由于我们无法更改iOS或Android框架,因此我们在Uno中选择将DependencyObject
为interface 。 这样就可以将Uno FrameworkElement
用作UIView
并同时将其作为DependencyObject
。 但是,仅凭这一点还不够。
如果您的应用中有这样的代码怎么办?
public class MyBindableObject : DependencyObject
{
public string MyProperty
{
get { return (string)GetValue(MyPropertyProperty); }
set { SetValue(MyPropertyProperty, value); }
}
// Using a DependencyProperty as the backing store for MyProperty. This enables animation, styling, binding, etc...
public static readonly DependencyProperty MyPropertyProperty =
DependencyProperty.Register("MyProperty", typeof(string), typeof(MyBindableObject), new PropertyMetadata(default(string)));
}
我们从DependencyObject
继承并使用标准语法定义了DependencyProperty
,该语法使用DependencyObject.GetValue
和DependencyObject.SetValue
方法。 在UWP上,这些是在基类中定义的,但是如果DependencyObject
是接口, 则没有基类。 实际上,如果只是接口,则代码不会编译,因为尚未实现该接口。
幸运的是, DependencyObject
不仅是 Uno中的接口,而且上面的代码将像在UWP上一样按原样在Android和iOS上进行编译。 代码生成使之成为现实。 这是一些程序员的技巧来说明这一点。 详细说明如下。
![](https://i-blog.csdnimg.cn/blog_migrate/30448eaff62017c361a3cf3f2fa9bf67.png)
在UWP上, UIElement
继承自 DependencyObject
类。
![](https://i-blog.csdnimg.cn/blog_migrate/3342a62b4daefbc3ea6cab11274b6cc7.png)
多重继承-不是一种选择。
![](https://i-blog.csdnimg.cn/blog_migrate/168add877e0dda7418ce85ca02facf23.png)
在Uno中, DependencyObject
是一个接口,其实现由代码生成自动提供。
在其他情况下,我们也面临这种问题的较弱形式-希望具有两种基本类型。 在框架的某些地方,我们从派生的本机视图类型继承。 例如, ScrollContentPresenter
继承自Android和iOS上的本机滚动视图。 但是我们也希望ScrollContentPresenter
公开FrameworkElement
的方法和属性。
通过使用代码生成在C#中实现mixins,我们成功解决了这两个问题。
混合起来
大多数静态类型的语言由于其带来的复杂性(即“钻石问题”)而不允许多个基类。 (C ++是一个显着的例外。)但是,在动态类型的语言中,使用mixins以可重用的方式将额外的功能附加到类上是很常见的。
C#作为一种静态类型的语言,不支持将mixins作为一流的语言功能。 但是,代码生成使我们可以对其进行仿真。 Uno使用代码生成以(至少)两种不同的方式添加混合。
我将从更简单的方法开始:使用“ T4”模板。 引用Microsoft的文档:
在Visual Studio中,T4文本模板是文本块和可以生成文本文件的控制逻辑的混合体。 在Visual C#或Visual Basic中,控制逻辑被编写为程序代码的片段。 在Visual Studio 2015 Update 2和更高版本中,可以在T4模板指令中使用C#6.0版功能。 生成的文件可以是任何种类的文本,例如网页,资源文件或任何语言的程序源代码。
来源: https : //docs.microsoft.com/zh-cn/visualstudio/modeling/code-generation-and-t4-text-templates?view=vs-2017
T4模板(“ .tt文件”)已经存在了一段时间。 它们本质上是静态文本(在我们的例子中是C#代码)和条件逻辑的混合。 这是一个片段:
namespace <#= mixin.NamespaceName #>
{
public partial class <#= mixin.ClassName #> : IFrameworkElement
{
#if !<#= mixin.IsFrameworkElement #>
/// <summary>
/// Gets the parent of this FrameworkElement in the object tree.
/// </summary>
public DependencyObject Parent => ((IDependencyObjectStoreProvider)this).Store.Parent as DependencyObject;
#endif
#if <#= mixin.HasAttachedToWindow #>
partial void OnAttachedToWindowPartial()
{
OnLoading();
OnLoaded();
}
这来自在Uno中添加IFrameworkElement
功能的模板 。 它实现了诸如Width
/ Height
, Opacity
, Style
等属性。在编译时,模板运行并使用ScrollContentPresenter
那些成员和其他几个类(包括FrameworkElement
本身)创建一个部分类 。
T4方法经过了充分的测试,并且在这种情况下效果很好。 但是,它有两个限制:
- 它需要手动设置:每个要使用mixin的类都必须明确注册。
- 它需要手动标记,以确保生成的代码不会“踩”创作的代码,例如,当创作的代码已经定义
Foo()
时,通过生成Foo()
方法。 - 它不支持外部代码。 您无法在应用程序中使用上面的mixin(缺少将模板复制粘贴到应用程序中)。
出于这个原因,为了有一个混入来实现DependencyObject
的功能,我们进行了一些更复杂,更神奇的事情。
DependencyObjectGenerator —神奇的实现
Roslyn (也称为“ .NET编译器平台”)的发布对代码生成是一个福音。 通过Roslyn,Microsoft开源了C#编译器,但他们还公开了用于代码分析的强大API。 使用Roslyn,可以轻松访问编译器拥有的所有语法和语义信息。
为了利用此功能来生成代码,我们创建了Uno.SourceGeneration包。 像Uno Platform一样,它是免费和开源的。 它创建了一个构建任务,并允许您根据Roslyn对您的解决方案的分析轻松地添加生成的代码。 这可能是增加现有类型的局部类定义,也可能是全新的类。
在Uno中,这由DependencyObjectGenerator类使用。 该生成器在解决方案中寻找实现DependencyObject
接口的每个类,例如上面的MyBindableObject
示例。 对于每个这样的类,它将自动生成DependencyObject
的方法和属性。
由于生成器具有来自Roslyn的全套语义信息,因此它可以以一种聪明的方式做到这一点。 例如,如果它检测到该类是视图类型,则它将添加方法以在加载或卸载视图时更新绑定信息。
这是DependencyObjectGenerator
的一小段代码 :
private void WriteAndroidAttachedToWindow(INamedTypeSymbol typeSymbol, IndentedStringBuilder builder)
{
var isAndroidView = typeSymbol.Is(_androidViewSymbol);
var isAndroidActivity = typeSymbol.Is(_androidActivitySymbol);
var isAndroidFragment = typeSymbol.Is(_androidFragmentSymbol);
var isUnoViewGroup = typeSymbol.Is(_unoViewgroupSymbol);
var implementsIFrameworkElement = typeSymbol.Interfaces.Any(t => t == _iFrameworkElementSymbol);
var hasOverridesAttachedToWindowAndroid = isAndroidView &&
typeSymbol
.GetMethods()
.Where(m => IsNotDependencyObjectGeneratorSourceFile(m))
.None(m => m.Name == "OnAttachedToWindow");
if (isAndroidView || isAndroidActivity || isAndroidFragment)
{
if (!isAndroidActivity && !isAndroidFragment)
{
WriteRegisterLoadActions(typeSymbol, builder);
}
builder.AppendLine($@"
#if {hasOverridesAttachedToWindowAndroid} //Is Android view (that doesn't already override OnAttachedToWindow)
#if {isUnoViewGroup} //Is UnoViewGroup
// Both methods below are implementation of abstract methods
// which are called from onAttachedToWindow in Java.
protected override void OnNativeLoaded()
{{
_loadActions.ForEach(a => a.Item1());
BinderAttachedToWindow();
}}
在此方法中,我们有一个INamedTypeSymbol ,它是Roslyn的一个对象,其中封装了有关类型的信息。 我们已经确定typeSymbol
实现DependencyObject
; 在这里,我们检查它是否为Android View
,如果是,则覆盖已加载的方法。 您可能会注意到,我们还在检查类型是否尚未覆盖相同的方法,因此我们不会意外生成与创作代码冲突并导致编译器错误的代码。 每当您的应用程序编译时,所有这些都在后台进行,而无需用户干预。
最终结果是,即使是接口而不是类, DependencyObject
可以在Uno中与UWP几乎完全相同地使用! 有一些极端的情况:例如,一些通用约束不会以相同的方式起作用。 但总的来说,它的效果非常好。
目前为止就这样了。 让我们知道您想进一步了解Uno的其他“内幕”方面!
![](https://i-blog.csdnimg.cn/blog_migrate/2961ee577a1d247e377e8c3c6a4bec54.png)