为 X11 编写 XAML 对话框应用程序





5.00/5 (6投票s)
目前,主要的 Linux/Unix (X11) GUI 应用程序框架(GTK+、KDE)都不支持基于 XAML 的应用程序开发。Moonlight 项目(包括 XAML 支持)已于 2012 年 5 月 29 日停止。本文将介绍如何使用 Roma Widget Set (Xrw) 和 C# 来开发基于 XAML 的对话框应用程序。
引言
本文是一个实践教程,介绍如何使用 **Roma Widget Set** (Xrw) 和 XAML,以 MVVM (Model View ViewModel) 设计模式编写一个 X11 对话框界面(简单 GUI)应用程序。Roma Widget Set 是一个零依赖的 X11 GUI 应用程序框架(仅需要免费的 Mono 标准安装程序中的程序集和免费的 X11 发行版的库;它不特别需要 GNOME、KDE 或商业库),并且完全用 C# 实现。
据我所知,这是在 Moonlight 项目被放弃后,首次尝试利用 Xrw 为 X11 应用程序开发使用 XAML。
Roma Widget Set 和 XAML 实现都未完成。此示例应用程序旨在作为“概念验证”,并检查是否以及如何可能使用 XAML 创建基于 MVVM 设计模式的 X11 应用程序。
下一篇关于使用 Roma Widget Set 在 X11 上进行 XAML 开发的文章将是《用 XAML 为 X11 编写功能区应用程序》。
背景
动机
尽管 System.Windows.Forms
应用程序仍然存在,但越来越多的现有应用程序正在转向 XAML,并且在 Microsoft® Windows® 平台上,大多数新应用程序现在都是用 XAML 构建的。
尽管 XAML 给开发者带来了新的问题,例如
- 冗余且冗长的语言语法,
- 低效率导致更高的处理成本,
- 为分层模型设计,而非为关系模型设计,需要额外的努力来表达重叠(非分层)关系,
- 命名空间的使用导致处理和解释困难,以及
- 一个本应简单的语言存在严重的歧义
但它拥有——除了很多其他优点之外——三个非常重要的优点。XAML
- 是平台无关的,
- 是纯文本,易于维护,并且
- 强制实现 GUI 和业务逻辑的清晰分离和松耦合。
由于良好的文档可以尽量减少缺点带来的影响,因此优点就更加重要。为什么不将 XAML 用于 X11 应用程序开发,并通过一个好的教程来避免(由于相当陡峭的学习曲线)令人沮丧的初步体验呢?好的——让我们开始吧!
概念
要将 XAML 引入 X11 应用程序开发,需要克服一些挑战
- GUI 应用程序框架必须支持控件和其他框架元素的动态创建。
- XAML 语法应尽可能接近 Microsoft® 原始语法。这将
- 允许在 X11 和 Windows® 之间转移已获得的知识和编写的 XAML 代码,并且
- 支持 GUI 和业务逻辑的分离,以编写平台无关的代码。
- XAML 中定义的框架元素也必须可以通过 C# 代码访问。这是实现属性更改通知机制以实现从 ViewModel 到 View 的通知所必需的。
- GUI 应用程序框架必须支持适当的反射方法,以实现属性更改通知机制以实现从 View 到 ViewModel 的通知。
克服这些挑战的方法是
- 围绕 Roma Widget Set 的 XAML 包装器。XAML 包装器和 Xrw 完全用 C# 编写,并且可以利用动态语言特性。
- 自定义 XAML 解释器。这使得可以轻松地采用期望的语法和功能,使其尽可能接近 Microsoft® 原始语法。
- 单独创建的 XAML 预处理器,它动态生成部分类中与 XAML 中定义的那些部分相关联的那一部分。
- 为 Xrw 周围的 XAML 包装器实现复杂的泛型反射方法。
为了评估这些方法,应该实现一个基本的基于 XAML 的对话框应用程序。该示例应用程序是用 Mono Develop 在 C# 中编写的。它基于 Mono Develop 在代码编译前运行用户定义命令以运行 XAML 预处理器的功能。
焦点
本文旨在演示
- 可以使用 XAML 定义一个带有某些控件的窗口,并且
- 可以使用 XAML 将单击事件连接到按钮。
使用代码
示例应用程序是使用 Mono Develop 2.4.1 和 Mono 2.8.1 在 OPEN SUSE 11.3 Linux 32 位 EN 和 GNOME 桌面上编写的。将其移植到任何旧版本或新版本都不应成问题。示例应用程序的解决方案包含两个项目(完整的源代码可供下载)
- XamlDialogApp 包含示例应用程序的源代码。
- XamlPreprocessor 包含 XAML 预处理器的源代码。
该示例应用程序还通过了 Mono Develop 3.0.6 在 OPEN SUSE 12.3 Linux 64 位 DE 和 GNOME 桌面、IceWM、TWM 和 Xfce 上针对 Mono 3.0.4 的测试。
32 位和 64 位解决方案之间的唯一区别是某些 X11 特定数据类型的定义,正如在《用 Mono Develop 编写 Xlib - 第一部分:低级(概念验证)》一文中已经描述的那样。
Xlib/X11 窗口处理基于 X11Wrapper 程序集版本 0.7,它定义了 Xlib/X11 对 libX11.so 的调用的函数原型、结构和类型。该程序集是为《用 Mono Develop 编写 Xlib - 第一部分:低级(概念验证)》项目开发的,并在《使用 Roma Widget Set (C# X11) - 一个零依赖的 GUI 应用程序框架 - 基础》项目中得到了改进。
GUI 框架基于 Xrw 程序集版本 0.7,它定义了在 XAML 代码中使用的控件/小部件及其包装类(应尽可能接近 Microsoft® 原始代码)。该程序集是在《使用 Roma Widget Set (C# X11) - 一个零依赖的 GUI 应用程序框架 - 基础》项目中开发的。
建议:要使用 MonoDevelop 的类库文档快捷键 (F1),必须安装“mono-tools”软件包。
图像显示了使用 XrwTheme.GeneralStyle.WinMidori
的示例应用程序。
示例应用程序的业务功能包括
- 将 Base64 编码的文本(*.txt)解码为二进制图像文件(*.decoded.bmp),
- 将二进制图像文件(*.bmp)编码为 Base64 编码的文本(*.encoded.txt),以及
- 使用系统的默认文本编辑器打开最近编码的文件
以演示对话框界面应用程序的实际用例。
默认图像格式(即默认文件名扩展名)是 BMP。其他文件格式也应该可以工作,但需要事后更改文件名扩展名。
Windows 版本是与实现尽可能接近 Microsoft® 原始 XAML 代码并行开发的,它使用了 System.Convert.FromBase64String()
和 System.Convert.ToBase64String()
。由于 Mono 中这些方法的实现存在 bug,因此此示例应用程序的 Linux/Unix 版本改用了 X11.AlternativeBase64.Decode()
和 X11.AlternativeBase64.Encode()
。
分步说明
项目设置
首先,我们需要一个新的空 C# 项目。不需要额外的功能。推荐使用 MONO / .NET 3.5 或更高版本作为运行时版本。
项目引用必须包括标准程序包 System
、System.Core
、System.Drawing
、System.Xml
以及程序集(或项目)X11Wrapper
和 Xrw
。项目需要七个初始文件,可以任意命名。为了尽可能接近 Microsoft® 原始代码,建议使用
App.xaml
用于应用程序的类 XAML 定义,App.xaml.cs
用于应用程序的类(手动)实现/代码隐藏,MainModel.cs
用于主视图的 MVVM 数据模型,MainView.xaml
用于(对话框窗口)主视图的类 XAML 定义,MainView.xaml.cs
用于(对话框窗口)主视图的类(手动)实现/代码隐藏,MainViewModel.cs
用于主视图的 MVVM ViewModel,以及- 应用程序的图标文件。
最后,必须将 XAML 预处理器包含到项目中。这通过一个用户定义的命令来实现,该命令在编译前执行。
预处理器是解决方案的一部分,位于 XamlDialogApp 项目文件夹的相对路径 '../XamlPreprozessor/bin/Debug/XamlPreprozessor.exe'。可以随意更改位置。必须将执行命令的当前工作目录设置为 XamlDialogApp 项目文件夹,否则预处理器可能会检查错误的文件夹。
目前预处理器不检查子文件夹。但这对于此示例来说没问题。
应用程序文件内容
XAML (App.xaml)
要查看的第一个 XAML 文件是 App.xaml
。
<Application x:Class="XamlDialogApp.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" StartupUri="MainView.xaml" Style="WinMidori"> <!-- Supported styles are: WinClassic, WinLuna, WinRoyale, WinMidori, Gtk2Clearlooks --> <Application.Resources> </Application.Resources> </Application>
除了属性 Style="WinLuna"
之外,XAML 代码完全兼容 Microsoft®。或者,可以在 App.xaml.cs
中设置样式,以避免这种不兼容性。
Application
将由以下方式定义:
- 根节点名为
Application
。此节点是必需的。 XAML 处理依赖于节点名称Application
。 x:Class
属性定义了应用程序的命名空间名称(XamlDialogApp
)和类名(App
)。此属性是必需的。 强烈建议定义命名空间和类。XAML 处理依赖于属性名称x:Class
。属性名称x:Class
是一个 XAML 扩展,定义在http://schemas.microsoft.com/winfx/2006/xaml
命名空间中。xmlns
和xmlns:x
属性目前未被评估,仅作为 Microsoft® 兼容性的一部分包含在 XAML 代码中。StartupUri
属性定义了启动 UI 的 XAML 文件 URI,对于此示例是MainView.xaml
。此属性是必需的。 XAML 处理依赖于属性名称StartupUri
。Style
属性定义了 Xrw 用于显示 UI 的主题。此属性是可选的。Application.Resources
节点目前未被评估。此属性是可选的。
代码隐藏 (App.xaml.cs)
对应的 C# 代码隐藏文件是 App.xaml.cs
。
using System; using Xrw; using XrwXAML; namespace XamlDialogApp { /// <summary>This is the application's main class.</summary> /// <remarks>It must be inherited from XrwXAML.Application and /// contain a Main method to start the application.</remarks> public partial class App : XrwXAML.Application { /// <summary>The application starter method.</summary> /// <returns>Zero on success, nonzero otherwise.<see cref="System.Int32"/></returns> public static int Main () { // Delegate the hard work to the base class. return Main (System.Reflection.Assembly.GetExecutingAssembly()); } /// <summary>The public constructor.</summary> public App () { this.InitializeComponent(); } } }
请注意 App
类定义为 partial class
!这一点很重要,因为 XAML 预处理器将根据 App.xaml
中的 XAML 代码生成 App
类的第二部分。
主视图文件内容
XAML (MainView.xaml)
要查看的第二个 XAML 文件是 MainView.xaml
。
<Window x:Class="XamlDialogApp.MainView" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:src="clr-namespace:XamlDialogApp" DataContext="src:MainViewModel" Name="MainWindow" Title="XAML dialog application" Width="650" Height="400" Icon="XrwIcon16.bmp"> <Window.Resources> </Window.Resources> <Grid Name="MainGrid"> <Grid.Resources> <!-- <src:MainViewModel x:Key="mainViewDataSource" /> --> </Grid.Resources> <Grid.Datacontext> <!-- <Binding Source="{StaticResource mainViewDataSource}"/> --> </Grid.Datacontext> <Grid.ColumnDefinitions> <ColumnDefinition Width="12"/> <ColumnDefinition Width="*"/> <ColumnDefinition Width="20"/> <ColumnDefinition Width="100"/> <ColumnDefinition Width="12"/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="12"/> <RowDefinition Height="28"/> <RowDefinition Height="12"/> <RowDefinition Height="28"/> <RowDefinition Height="50"/> <RowDefinition Height="28"/> <RowDefinition Height="12"/> <RowDefinition Height="28"/> <RowDefinition Height="12"/> <RowDefinition Height="68"/> <RowDefinition Height="*"/> <RowDefinition Height="12"/> <RowDefinition Height="32"/> </Grid.RowDefinitions> <TextBox Name="DecodeSourcePath" Grid.Column="1" Grid.Row="1" Text="" BorderThickness="2" IsEnabled="false" /> <TextBox Name="DecodeTargetPath" Grid.Column="1" Grid.Row="3" Text="" BorderThickness="2" IsEnabled="false" /> <Button Name="Decode" Grid.Column="3" Grid.Row="1" Grid.RowSpan="3" Click="Decode_Click" > <TextBlock Text="Decode Base64 to BMP/PNG" TextWrapping="Wrap"/> </Button> <TextBox Name="EncodeSourcePath" Grid.Column="1" Grid.Row="5" Text="" BorderThickness="2" IsEnabled="false" /> <TextBox Name="EncodeTargetPath" Grid.Column="1" Grid.Row="7" Text="" BorderThickness="2" IsEnabled="false" /> <Button Name="Encode" Grid.Column="3" Grid.Row="5" Grid.RowSpan="3" Click="Encode_Click" > <TextBlock Text="Encode BMP/PNG to Base64" TextWrapping="Wrap"/> </Button> <Label Name="EncodeData" Content="" Grid.Column="1" Grid.Row="9" Grid.RowSpan="2" BorderThickness="2" /> <Button Name="Open" Grid.Column="3" Grid.Row="9" Click="OpenResult_Click" > <TextBlock Text="Open result in a text editor" TextWrapping="Wrap"/> </Button> <Label Name="State" Content="O.K." Grid.Column="0" Grid.Row="12" Grid.ColumnSpan="5" BorderThickness="2" /> </Grid> </Window>
完整的 XAML 代码与 Microsoft® 完全兼容。
Window
将由以下方式定义:
- 根节点名为
Window
。此节点是必需的。 XAML 处理依赖于节点名称Window
。 x:Class
属性定义了主视图的命名空间名称(XamlDialogApp
)和类名(MainView
)。此属性是必需的。 强烈建议定义命名空间和类。XAML 处理依赖于属性名称x:Class
。属性名称x:Class
是一个 XAML 扩展,定义在http://schemas.microsoft.com/winfx/2006/xaml
命名空间中。xmlns
和xmlns:x
属性目前未被评估,仅作为 Microsoft® 兼容性的一部分包含在 XAML 代码中。xmln
s:src
属性定义了本地源代码资源的命名空间。此属性是推荐的,如果任何属性值使用前缀src:
作为引用,则为必需的。 XAML 处理依赖于后缀:src
。属性值的语法必须是clr-namespace:
<namespace name>。这样定义的命名空间可以通过前缀src:
在属性值中引用。DataContext
属性定义了默认/备用数据上下文,对于此示例是src:MainViewModel
。此属性是可选的。DataContext
属性目前未被评估。Name
属性定义了类实例名称,可用于唯一标识类实例。此属性是必需的。 类实例名称不能与类类型名称相同!Title
属性定义了窗口标题。此属性是可选的。Width
属性定义了初始窗口宽度。此属性是可选的。Height
属性定义了初始窗口高度。此属性是可选的。Icon
属性定义了窗口图标。此属性是可选的。Window.Resources
节点目前未被评估。此节点是可选的。
Window
的根(控件几何)管理器控件是 Grid
,将由以下方式定义:
Name
属性定义了类实例的名称,可用于唯一标识该类实例。此属性是推荐的,如果该类实例需要通过 C# 代码访问,则是必需的。Grid.Resources
节点目前未被评估。此节点是可选的。Grid.Datacontext
节点目前未被评估。此节点是可选的。Grid.ColumnDefinitions
节点连接了列定义。此节点是必需的。
-Grid.ColumnDefinition
节点定义了一个网格列。此节点是必需的,至少一次。
-Width
属性定义了网格列宽度。此属性是必需的。 正整数被解释为像素宽度。星号被解释为动态宽度。Grid.RowDefinitions
节点连接了行定义。此节点是必需的。
-Grid.RowDefinition
节点定义了一个网格行。此节点是必需的,至少一次。
-Height
属性定义了网格行高度。此属性是必需的。 正整数被解释为像素高度。星号被解释为动态高度。Grid
节点可以包含定义子控件的节点,这些子控件将由网格布局。
Grid
控件包含 TextBox
控件作为子控件,它们将由以下方式定义:
Name
属性定义了类实例的名称,可用于唯一标识该类实例。此属性是推荐的,如果该类实例需要通过 C# 代码访问,则是必需的。Grid.Column
属性定义了控件在网格中应放置的从零开始的列索引。默认值为 0。此属性对于网格子项是推荐的,但对于放置在网格非第 0 列的控件是必需的。 索引不得超过可用网格列数。Grid.Row
属性定义了控件在网格中应放置的从零开始的行索引。默认值为 0。此属性对于网格子项是推荐的,但对于放置在网格非第 0 行的控件是必需的。 索引不得超过可用网格行数。Grid.ColumnSpan
属性定义了控件在网格中应跨越的列数。此属性对于网格子项是可选的,但对于跨越多个网格列的控件是必需的。 忽略此属性或将其设置为“0”或“1”是等效的。跨度不得超过可用网格列数。Grid.RowSpan
属性定义了控件在网格中应跨越的行数。此属性对于网格子项是可选的,但对于跨越多个网格行的控件是必需的。 忽略此属性或将其设置为“0”或“1”是等效的。跨度不得超过可用网格行数。Text
属性定义了要显示的文本。此属性是可选的。BorderThickness
属性定义了控件边框的宽度。此属性是可选的。IsEnabled
属性定义了控件的敏感度。此属性是可选的。 默认为 true。
Grid
控件包含 Button
控件作为子控件,它们将由以下方式定义:
Name
属性定义了类实例的名称,可用于唯一标识该类实例。此属性是推荐的,如果该类实例需要通过 C# 代码访问,则是必需的。Grid.Column
属性定义了控件在网格中应放置的从零开始的列索引。默认值为 0。此属性对于网格子项是推荐的,但对于放置在网格非第 0 列的控件是必需的。 索引不得超过可用网格列数。Grid.Row
属性定义了控件在网格中应放置的从零开始的行索引。默认值为 0。此属性对于网格子项是推荐的,但对于放置在网格非第 0 行的控件是必需的。 索引不得超过可用网格行数。Grid.ColumnSpan
属性定义了控件在网格中应跨越的列数。此属性对于网格子项是可选的,但对于跨越多个网格列的控件是必需的。 忽略此属性或将其设置为“0”或“1”是等效的。跨度不得超过可用网格列数。Grid.RowSpan
属性定义了控件在网格中应跨越的行数。此属性对于网格子项是可选的,但对于跨越多个网格行的控件是必需的。 忽略此属性或将其设置为“0”或“1”是等效的。跨度不得超过可用网格行数。Content
属性定义了要显示的文本。此属性是可选的。也可以使用嵌套的TextBlock
节点。 此属性的 Microsoft® 原始实现无法提供换行。
- 嵌套的TextBlock
节点定义了要显示的文本。此节点是可选的。 它可以提供换行。
-Text
属性定义了文本字符串。此属性是必需的。
-TextWrapping
属性定义了文本换行。此属性是可选的。Click
属性定义了单击事件委托。此属性是可选的。 目前,委托必须定义在Window
(代码隐藏)的类代码中,该控件是其子/孙控件。
Grid
控件包含 Label
控件作为子控件,它们将由以下方式定义:
Name
属性定义了类实例的名称,可用于唯一标识该类实例。此属性是推荐的,如果该类实例需要通过 C# 代码访问,则是必需的。Grid.Column
属性定义了控件在网格中应放置的从零开始的列索引。默认值为 0。此属性对于网格子项是推荐的,但对于放置在网格非第 0 列的控件是必需的。 索引不得超过可用网格列数。Grid.Row
属性定义了控件在网格中应放置的从零开始的行索引。默认值为 0。此属性对于网格子项是推荐的,但对于放置在网格非第 0 行的控件是必需的。 索引不得超过可用网格行数。Grid.ColumnSpan
属性定义了控件在网格中应跨越的列数。此属性对于网格子项是可选的,但对于跨越多个网格列的控件是必需的。 忽略此属性或将其设置为“0”或“1”是等效的。跨度不得超过可用网格列数。Grid.RowSpan
属性定义了控件在网格中应跨越的行数。此属性对于网格子项是可选的,但对于跨越多个网格行的控件是必需的。 忽略此属性或将其设置为“0”或“1”是等效的。跨度不得超过可用网格行数。Content
属性定义了要显示的文本。此属性是可选的。BorderThickness
属性定义了控件边框的宽度。此属性是可选的。
代码隐藏 (MainView.xaml.cs)
对应的 C# 代码文件是 MainView.xaml.cs
。它包含所有三个 Button
控件的 Click
委托。
using System;
using System.IO;
using X11;
using Xrw;
using XrwXAML;
namespace XamlDialogApp
{
/// <summary>The main window of the application. This class must be derived from XrwXAML.Window.
/// It must be a partial class. The second part of the class will be autogenerated and named
/// '*.generated.cs'.</summary>
public partial class MainView : XrwXAML.Window
{
/// <summary>The default constructor.</summary>
public MainView ()
: base (-1, -1)
{
// InitializeComponent () and InitializeComponentGenerated()
// will be called after construction by generated code!
}
/// <summary>Process the "Decode" button click event.</summary>
/// <param name="sender">The event source.<see cref="System.Object"/></param>
/// <param name="e">The event data.<see cref="RoutedEventArgs"/></param>
private void Decode_Click(object sender, RoutedEventArgs e)
{
...
}
/// <summary>Process the "Encode" button click event.</summary>
/// <param name="sender">The event source.<see cref="System.Object"/></param>
/// <param name="e">The event data.<see cref="RoutedEventArgs"/></param>
private void Encode_Click(object sender, RoutedEventArgs e)
{
...
}
/// <summary>Process the "Open" button click event.</summary>
/// <param name="sender">The event source.<see cref="System.Object"/></param>
/// <param name="e">The event data.<see cref="RoutedEventArgs"/></param>
private void OpenResult_Click(object sender, RoutedEventArgs e)
{
if (!string.IsNullOrEmpty (EncodeTargetPath.Text))
System.Diagnostics.Process.Start (EncodeTargetPath.Text);
}
/// <summary>Decode with Base64.</summary>
/// <param name="sourceStream">The source to decode.<see cref="Stream"/></param>
/// <param name="targetStream">The decoded target.<see cref="Stream"/></param>
/// <param name="message">The message describing the success.<see cref="System.String"/></param>
/// <returns>True on success, or false otherwise.<see cref="System.Boolean"/></returns>
public bool DecodeBase64FromStream(Stream sourceStream, Stream targetStream, out string message)
{
...
}
/// <summary>Encode with Base64.</summary>
/// <param name="sourceStream">The source to encode.<see cref="Stream"/></param>
/// <param name="targetStream">The encoded target.<see cref="Stream"/></param>
/// <param name="message">The message describing the success.<see cref="System.String"/></param>
/// <returns>True on success, or false otherwise.<see cref="System.Boolean"/></returns>
public bool EncodeBase64FromStream(Stream sourceStream, Stream targetStream, out string message)
{
...
}
}
}
请注意 MainView
类定义为 partial class
!这一点很重要,因为 XAML 预处理器将根据 MainView.xaml
中的 XAML 代码生成 MainView
类的第二部分。
预处理器代码生成
在项目首次编译期间,预处理器首先从 App.xaml
和 MainView.xml
生成部分类文件 App.generated.cs
和 MainView.generated.cs
。预处理器将日志消息写入标准输出,可以在构建输出中查看。
要继续进行此示例,必须将生成的初始文件包含到项目中。否则,Mono Develop 将不会将文件包含到编译中,并且部分类 App
和 MainView
的生成部分将丢失。(这就是为什么第一次编译总是会产生错误的原因。)
生成的 *.generated.cs
文件与 Microsoft® 的文件不兼容
- Microsoft® 的文件名为
*.g.cs
。 - Microsoft® 的文件在项目中被隐藏(但 Mono 的文件必须包含在项目中)。
- 文件内容完全不同。
现在可以逐步扩展功能,编译将生成可执行程序集。
关注点
能否使用 XAML 创建基于 MVVM 设计模式的 X11 应用程序?是的,可以!而且很有趣。因此,这不会是最后一篇关于 X11 XAML 编程的文章。
自 2014 年 10 月 29 日起,文章《用 XAML 为 X11 编写功能区应用程序》继续讨论 XAML 主题。
自 2014 年 11 月 23 日起,文章《使用大量数据绑定和零代码用 XAML 为 X11 编写应用程序》继续讨论 XAML 主题。
自2015年2月2日起,文章为 X11 编写 XAML 计算器应用程序延续了 XAML 主题。
历史
第一个版本是 2014 年 10 月 10 日。
第一个审阅版本来自 2014 年 10 月 29 日。
第二个审阅版本来自 2014 年 11 月 23 日。
第三个审阅版本来自 2015 年 2 月 19 日。