65.9K
CodeProject 正在变化。 阅读更多。
Home

原型驱动开发 (PDD)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (21投票s)

2022 年 2 月 3 日

MIT

19分钟阅读

viewsIcon

19649

原型驱动开发 - 一种快速开发高质量软件的新方法

引言

什么是原型驱动软件开发 (PDD)

原型驱动软件开发 (PDD) 类似于 测试驱动开发 (TDD),但通常用于 TDD 不可行时(例如,对于可视化开发)或用于比单元测试涵盖的更大功能(在这种情况下,可以同时进行 PDD 和 TDD - PDD 用于较大功能,TDD 用于更精细的功能)。

原型驱动开发是一种开发类型,在此类型下,对于每个新的重要功能,开发人员需要经历以下步骤

  1. 创建一个新的 Visual Studio 项目,甚至是一个包含 Main(...) 方法的解决方案来运行原型。原型项目应引用主应用程序项目中的项目和程序包的子集。
  2. 在 Main Prototype 项目中创建原型代码。
  3. 将原型代码打磨至完美。
  4. 通过将可重用部分的代码移至其引用的可重用项目来重构代码,以便主应用程序代码也可以访问它们。
  5. 在主项目中利用可重用代码。

第 3 步和第 4 步可以互换进行 - 可以将一些代码移至可重用项目,然后继续处理原型,然后再将更多代码移至可重用项目。

稍后,可以使用原型项目或解决方案来测试、调试或修改创建的功能。

PDD 周期图

PDD 的优势

PDD 的主要优势在于,它可以更快地进行原型设计、实验和调试,因为我们只编译和重新启动一个精简的原型,而不是一个庞大而沉重的应用程序。

PDD 的全部好处列表

  1. 原型允许您快速地对功能进行实验和调试,而无需等待整个解决方案/应用程序启动。
  2. 通常,为功能原型模拟和注入所需部分比为整个应用程序进行模拟要容易。例如,创建后端数据的模拟来测试单个控件,可能比创建用于使整个应用程序正常工作的模拟要容易。这个想法将在未来关于 IoC(控制反转)和注入的文章中探讨。
  3. PDD 有助于将代码分解为小巧、易于管理的代码块,从而提高关注点分离。
  4. 在 PDD 下,每个重要功能在开发或修改时都会进行测试,从而提高了代码的整体质量。
  5. 在创建功能后,在能够快速编译和运行的小型原型中修改或修复它,比在庞大、笨重且启动缓慢的应用程序中修改或修复它更容易。
  6. 原型和单元测试对于代码分析也很有用。事实上,如果您有新代码需要学习,从使用角度理解某个复杂方法或类的最佳方法是使用一些原型或测试来实际操作该方法或类。如果原型和测试尚不存在,您可以随时为分析新代码而创建它们。

PDD 的缺点(或缺乏缺点)

与 TDD 不同,PDD 基本没有缺点。

测试驱动开发 (TDD) 文章中,我写了一些关于 TDD 的潜在问题 - 特别是,团队可能会忘记他们的主要目的是满足客户或客户代理的要求,而编写测试只是为了获得更好的代码,而不是目的本身。我看到人们为所有方法编写测试,包括一些非常简单的方法,这些方法的测试不会降低任何错误的发生率。我看到项目实际上因为测试数量庞大,特别是由于构建速度非常慢,几乎陷入停顿 - 我看到了运行测试需要 2 天的构建。

与 TDD 不同,PDD 应仅用于重要功能,而不是用于自动化测试,而是用于加速功能开发、调试和修改,并提高结果代码的质量。原型不必为每次构建都进行编译和运行。如果某些原型与可重用项目中的代码不同步,也不会有什么大问题。下次使用原型来修改或修复其功能时,可以更新原型以进行编译。

PDD 的起源

PDD 是我在大约 15-13 年前提出的,从那时起一直独自实践,有时也在我担任架构师的项目中向整个团队介绍。

根据我的经验,它对于快速、高质量的开发非常有用,并在团队和个人层面都提供了出色的关注点分离。

我不会惊讶地发现其他架构师和开发人员(尤其是那些从事桌面 UI 应用程序开发的人)可能也想出了类似的发展过程。

据我所知,本文是首次尝试系统化和公开分享这种开发方法。

PDD 示例代码

编码示例本质上是一个练习,必须通过该练习才能理解本文顶部图表中指定的 PDD 过程。

示例的所有代码均使用 Visual Studio 2022、C# 10 和 .NET 6 构建,但只要对项目和解决方案文件以及代码进行一些不重要的修改,完全可以使用早期版本的 VS、C# 和 .NET。

对于示例的可视化开发,我使用了 Avalonia 包。Avalonia 是一个开源的跨平台 WPF++,我强烈推荐用于桌面或 Blazor UI 开发。要了解有关 Avalonia 的更多信息,请在 codeproject.com 上搜索我的 Avalonia 文章,从 《使用 AvaloniaUI 进行跨平台 UI 编码的简单示例。第一部分 - AvaloniaUI 构建块》开始。

熟悉 WPF 或其他 XAML 框架的人应该能够轻松理解本文中使用的所有 Avalonia 代码。如果您没有任何 XAML 经验,仍然可以阅读并完成本文中解释的示例 - 即使您不理解示例的某些部分,只要您理解 PDD 过程即可。

代码位于 NP.PrototypeDrivenDevelopmentSample github 存储库中。

该存储库包含两个文件夹

  • EmptyApp - 包含仅包含解决方案/项目和文件夹结构的空原型应用程序 - 这是可以用来开始 PDD 练习的解决方案。

  • CompleteApp - 包含原型应用程序,即您完成 PDD 练习后的样子。

两个应用程序 EmptyAppCompleteApp 的结构非常相似,只是 EmptyApp 除了 Visual Studio 在项目创建时生成的代码外,没有其他代码。因此,为了详细说明初始文件目录和项目结构,我们将使用 EmptyApp 中的代码。

应用程序的基文件夹(例如 EmptyApp)包含三个子文件夹

  1. src - 用于特定于此应用程序的项目,包括主应用程序项目(在我们的例子中,它只包含带有解决方案的主项目,没有其他内容)。

  2. Core - 用于可以与其他应用程序共享的可重用项目和通用代码。为简单起见,我们将只有一个项目 NP.Visuals,它(在我们的例子中)应该包含任何可重用的视觉代码。如果您使用 Git,可以将这些可重用项目放在单独的存储库中,然后将其作为 git 子模块引入 Core 文件夹。

  3. Prototypes - 包含原型项目的文件夹。最初(在 EmptyApp 文件夹中),它只包含一个原型 NP.PDD.PrototypeToCloneNP.PDD.PrototypeToClone 解决方案具有所有必需的项目和包以及必要的引用。

可以通过复制 NP.PDD.PrototypeToClone 并重命名来创建一个新原型项目的解决方案。这样,所有必需的项目、包和引用都会被保留,您无需手动添加它们。如何在 PDD 演练部分中将对此进行详细描述。

这是 MainApp 项目的 **解决方案资源管理器** 视图

您可以看到 NP.PDD.MainAppNP.Visuals 项目都引用了 Avalonia 包。

主项目包含一些标准的 Avalonia 启动文件:Program.csApp.axamlApp.axaml.csMainWindow.axamlMainWindow.axaml.cs(Avalonia 中使用“*.axaml”扩展名表示 XAML 文件,使用“*.axaml.cs”双扩展名表示代码隐藏)。

如果尝试启动 NP.PDD.MainApp 项目,您将看到一个空窗口。

重要提示:PDD 方法的许多优势只有在主应用程序庞大且启动和编译缓慢时才会显现,而这通常是任何非微不足道的 UI 应用程序的情况。但是,在我们的演示中,为了简单起见,我们从一个空的应用程序开始。

PDD 周期演练示例

初步信息和步骤

本节内容

在本节中,我们将详细介绍如何完成完整的 PDD 周期。此周期的目的是创建一个非常简单的 Avalonia LabeledTextBox 自定义控件,该控件将标签文本与 Avalonia TextBox 结合起来。

如果您想理解并内化 PDD,请务必按照本节详细介绍的步骤进行操作。

什么是 Avalonia

对于 Avalonia 新手来说 - Avalonia 与 WPF 非常相似,但功能更强大,bug 更少,并且可以在三个主要平台(Windows、Linux 和 Mac)以及通过 Blazor 在浏览器中运行。对于不想切换到 Avalonia 的人来说,可以使用 WPF 构建非常相似的示例。

这是一篇关于 Avalonia 的优秀入门文章 - 《使用 AvaloniaUI 进行跨平台 UI 编码的简单示例。第一部分 - AvaloniaUI 构建块》

用于 Avalonia 的 Visual Studio 设置

为了能够使用 Avalonia,您需要通过点击 Visual Studio 中的 **EXTENSIONS** -> **Manage Extensions** 菜单项来安装 Avalonia 的 Visual Studio 扩展,然后选择 Online 选项卡,找到“**Avalonia for Visual Studio ...**”扩展并按“**Install**”按钮。

PDD 周期

步骤 1 - 通过克隆现有项目创建新的原型项目

我们将原型项目命名为 NP.PDD.LabeledTextBoxPrototype

一般来说,应该将一个旧原型复制到一个新文件夹,然后重命名解决方案、项目和命名空间。这比创建新原型项目并设置所有引用要快得多。

我们已经有一个空的原型项目 NP.PDD.PrototypeToClone,我将其放在那里是专门为了克隆。

以下是从 NP.PDD.PrototypeToClone 创建新原型项目 NP.PDD.LabeledTextBoxPrototype 的详细步骤

  1. NP.PDD.PrototypeToClone 文件夹复制到 NP.PDD.LabeledTextPrototype 文件夹。
  2. 将原型 VS 解决方案文件重命名为 NP.PDD.LabeledTextBoxPrototype.sln
  3. 在 Visual Studio 中打开新的原型解决方案。
  4. 在解决方案资源管理器中,将原型的名称与解决方案和文件夹名称(NP.PDD.LabeledTextBoxPrototype)相匹配。

  5. 通过执行以下步骤,将主原型项目中的旧命名空间“Sample”重命名为“LabeledTextBoxPrototype
    1. 打开主项目中的一个 .cs 文件,例如 Program.cs
    2. 选择命名空间中的“Sample”部分。
    3. 使用 **Ctrl-Shift-F** 打开搜索窗口,在“Replace in Files”选项卡中,将“Replace text box”设置为“LabeledTextBoxPrototypestring。将“Look in”选项设置为“Current Project”,将 File types 设置为“*.axaml;*.cs”并按“ReplaceAll”按钮:

现在原型项目已准备好进行编码。

步骤 2 和 3 - 在原型的主要项目中创建 LabeledTextBox 控件并将其打磨至完美

右键单击 NP.PDD.LabeledTextBoxPrototype 项目,选择 **Add** -> **New Item** 菜单项。在打开的对话框中,选择左侧的“Code”选项卡,选择右侧的“Class”,将类命名为 LabeledTextBox,然后按“Add”按钮。

将创建新的 LabeledTextBox 类。将该类设置为 public 并使其继承自 Avalonia 的 TemplatedControl 类。

using Avalonia.Controls.Primitives;

namespace NP.PDD.LabeledTextBoxPrototype
{
    public class LabeledTextBox : TemplatedControl
    {
    }
}  

现在向类中添加两个 string 类型的 Styled Properties - TextLabel

提醒:Avalonia Styled Properties 本质上与 WPF 依赖属性相同。要快速创建 Avalonia Styled Property,可以使用 avsp 代码片段,该代码片段来自 Avalonia Snippets。代码片段安装说明可在同一 URL 找到。如果您想了解更多关于 Avalonia 附加属性和样式属性的信息,请阅读 《使用简单示例进行跨平台 Avalonia .NET 框架编程基础概念》文章。

using Avalonia;
using Avalonia.Controls.Primitives;

namespace NP.PDD.LabeledTextBoxPrototype
{
    public class LabeledTextBox : TemplatedControl
    {
        #region Label Styled Avalonia Property
        public string Label
        {
            get { return GetValue(LabelProperty); }
            set { SetValue(LabelProperty, value); }
        }

        public static readonly StyledProperty<string> LabelProperty =
            AvaloniaProperty.Register<LabeledTextBox, string>
            (
                nameof(Label)
            );
        #endregion Label Styled Avalonia Property

        #region Text Styled Avalonia Property
        public string Text
        {
            get { return GetValue(TextProperty); }
            set { SetValue(TextProperty, value); }
        }

        public static readonly StyledProperty<string> TextProperty =
            AvaloniaProperty.Register<LabeledTextBox, string>
            (
                nameof(Text)
            );
        #endregion Text Styled Avalonia Property
    }
}

不要被仅定义两个 Styled 属性的行数吓到 - 大部分代码是由 avsp 代码片段生成的,使用此代码片段,您可以在一两秒内创建 Styled Property。

我们已完成 C# 文件。现在让我们创建 ControlTemplate 以提供此自定义控件的视觉表示。我们将首先在项目中的 MainWindow.axaml 文件中完成此操作,然后将控件及其模板移至通用项目 NP.Visuals

打开几乎为空的 MainWindow.axaml 文件。它最初看起来是这样的(从 NP.PDD.ProjectToClone 项目复制后)

<Window xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Width="600"
        Height="400"
        x:Class="NP.PDD.LabeledTextBoxPrototype.MainWindow"
        Title="NP.PDD.LabeledTextBoxPrototype">

</Window>  

首先,让我们添加一个指向主原型项目中的 NP.PDD.LabeledTextBoxPrototype C# 命名空间的 XML 命名空间 - 将以下行添加到 <Window ... 标签中

  xmlns:local="clr-namespace:NP.PDD.LabeledTextBoxPrototype"

这样 XAML 代码就变成

<Window xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:NP.PDD.LabeledTextBoxPrototype"
        Width="600"
        Height="400"
        x:Class="NP.PDD.LabeledTextBoxPrototype.MainWindow"
        Title="NP.PDD.LabeledTextBoxPrototype">
</Window>  

(添加的行是粗体)。

现在将新控件添加到主窗口的中心。

<Window xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:NP.PDD.LabeledTextBoxPrototype"
        ...>
    <local:LabeledTextBox Label="The Text"
                          Text="Hello World!"
                          HorizontalAlignment="Center"
                          VerticalAlignment="Center"/>
</Window>  

尝试运行应用程序 - 窗口仍然是空的,因为我们的控件没有控件模板,相应地也没有生成视觉树。

现在(这难道不令人兴奋吗),让我们在 <local:LabeledTextBox.Template> 标签内内联构建控件的模板。

<local:LabeledTextBox Label="The Text"
                      Text="Hello World!"
                      HorizontalAlignment="Center"
                      VerticalAlignment="Center">
    <local:LabeledTextBox.Template>
        <ControlTemplate TargetType="local:LabeledTextBox">
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="{Binding Label, 
                           RelativeSource={RelativeSource TemplatedParent}}"
                           VerticalAlignment="Center"/>
                <TextBlock Text=": "
                           VerticalAlignment="Center"/>
                <TextBox Text="{Binding Text, Mode=TwoWay, 
                         RelativeSource={RelativeSource TemplatedParent}}"
                         VerticalAlignment="Center"
                         Width="200"/>
            </StackPanel>
        </ControlTemplate>
    </local:LabeledTextBox.Template>
</local:LabeledTextBox>  

仅控件模板的代码如下

<ControlTemplate TargetType="local:LabeledTextBox">
    <StackPanel Orientation="Horizontal">
        <TextBlock Text="{Binding Label, RelativeSource={RelativeSource TemplatedParent}}"
                   VerticalAlignment="Center"/>
        <TextBlock Text=": "
                   VerticalAlignment="Center"/>
        <TextBox Text="{Binding Text, Mode=TwoWay, 
                 RelativeSource={RelativeSource TemplatedParent}}"
                 VerticalAlignment="Center"
                 Width="200"/>
    </StackPanel>
</ControlTemplate>  

指定控件模板的 TargetType 允许在模板中使用 Binding,将模板中的属性绑定到 TemplatedParent 控件类中定义的属性(在我们的例子中,该类是 LabeledTextBox)。

RelativeSource={RelativeSource TemplatedParent} 部分的绑定意味着我们绑定到 TemplatedParent 上定义的属性(表示使用当前 ControlTemplate 作为其模板的对象)。

模板中有一个水平方向的 StackPanel,它水平排列着包含 TextBlock 的标签,后面跟着一个冒号“:”,再后面是 TextBoxTextBlock 的文本绑定到包含模板的 LabeledTextBox 控件的 Label Styled Property,TextBox 的文本绑定到同一控件的 Text Styled Property。

现在启动应用程序,您会看到标签“The Text”,后面跟着一个包含“Hello World!stringTextBox

如果您知道如何使用 Avalonia 类似 snoop 的工具,甚至可以检查当您更改 TextBox 中的文本时,LabeledTextBox 上的 Text 属性也会改变(因为是双向绑定)。

提醒:要启动 Avalonia 类似 snoop 的工具 - 点击应用程序的主窗口并按 F12 键。您可以在此处了解更多关于 Avalonia Tool 的信息。

注意:我们创建的自定义控件仅用于 PDD 演示 - 真实世界的自定义控件应暴露和绑定更多属性,例如,应该可以独立更改标签的样式和 TextBox 中文本的样式等。

步骤 2 已完成 - 我们有了一个可工作的原型,代码位于原型解决方案的主项目中,并且我们已经测试了它的工作情况。

由于我们的应用程序非常简单,我们也假设我们完成了步骤 3 - 将原型代码打磨至完美,然后我们直接进入步骤 4 - 通过将可重用部分的代码移至通用项目来重构代码。

步骤 4 - 通过将可重用部分的代码移至通用项目来重构代码

在我们的例子中,我们将把 LabeledTextBox 控件实现和控件模板移到 NP.Visuals 项目中。

首先,将 LabeledTextBox.cs 文件从 NP.PDD.LabeledTextBoxPrototype 项目拖动到 **解决方案资源管理器** 中的 NP.Visuals 项目。然后,从原型的项目的主项目中删除 LabeledTextBox.cs 文件。

现在,将定义在 NP.Visuals 项目中的 LabeledTextBox 类的命名空间更改为 NP.Visuals

namespace NP.Visuals
{
    public class LabeledTextBox : TemplatedControl
    {
        ...
    }
}  

再次重申,LabeledTextBox.cs 文件不应再存在于原型的主项目 NP.PDD.LabeledTextBoxPrototype 下,而应存在于 NP.Visuals 下,并且其命名空间应更改为 NP.Visuals

现在更新 MainWindow.axaml 文件中对我们 LabeledTextBox 类的引用。首先,我们需要创建一个新的 XML 命名空间,指向 NP.Visuals 程序集中的 NP.Visuals 命名空间。我们将以下行添加到 <Window ... 标签中

xmlns:visuals="clr-namespace:NP.Visuals;assembly=NP.Visuals"

这样 Window 标签现在看起来像

<Window xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:visuals="clr-namespace:NP.Visuals;assembly=NP.Visuals"
        xmlns:local="clr-namespace:NP.PDD.LabeledTextBoxPrototype"
        ...>  

添加的命名空间行是粗体的。

现在我们将 MainWindow.axaml 文件中的 local: 前缀更改为 visuals: 前缀。这是我们得到的结果

<Window xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:visuals="clr-namespace:NP.Visuals;assembly=NP.Visuals"
        ...>
    <visuals:LabeledTextBox Label="The Text"
                            Text="Hello World!"
                            HorizontalAlignment="Center"
                            VerticalAlignment="Center">
        <visuals:LabeledTextBox.Template>
            <!-- TargetType allows to use Binding to bind to the 
                 Styled Properties defined on the control of the templated parent class
                 -->
            <ControlTemplate TargetType="visuals:LabeledTextBox">
                <!-- this StackPanel arranges the items contained within it horizontally -->
                <StackPanel Orientation="Horizontal">
                    <!-- TextBlock whose text is bound to the Label Styled Property
                         of its template parent control -->
                    <TextBlock Text="{Binding Label, 
                               RelativeSource={RelativeSource TemplatedParent}}"
                               VerticalAlignment="Center"/>
					
                    <!-- TextBlock displaying the colon followed by a space - ": "-->
                    <TextBlock Text=": "
                               VerticalAlignment="Center"/>

                    <!-- TextBox whose Text is two-way bound to the Text Styled Property
                         of its template parent control -->
                    <TextBox Text="{Binding Text, Mode=TwoWay, 
                             RelativeSource={RelativeSource TemplatedParent}}"
                             VerticalAlignment="Center"
                             Width="200"/>
                </StackPanel>
            </ControlTemplate>
        </visuals:LabeledTextBox.Template>
    </visuals:LabeledTextBox>
</Window>  

添加的前缀是粗体的。您可以看到我添加了一些文档到 MainWindow.axaml 文件,但除此之外,代码与之前完全相同 - 唯一的区别是我们现在引用了定义在不同程序集和不同命名空间下的 LabeledTextBox 类。

尝试运行应用程序 - 它应该产生与之前完全相同的结果。

正如您记得的,为了方便起见,我们将控件模板内联定义。我们希望将其移至 NP.Visuals 项目中的 XAML 文件。作为第一个子步骤,将其从 <visuals:LabeledTextBox... 标签中移出,并将其转换为 XAML 资源,但仍保留在同一个 MainWindow.axaml 文件中。

我们在 XAML 代码中创建 <Window.Resources 部分,并将模板放在那里,同时为其添加 x:Key="LabeledTextBoxTemplate" 属性。我们在 StaticResource 标记扩展中指定此 x:Key 值,以便在我们的 LabeledTextBox 控件实例中引用模板。

<Window xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:visuals="clr-namespace:NP.Visuals;assembly=NP.Visuals"
        ...>
    <Window.Resources>
        <ControlTemplate x:Key="LabeledTextBoxTemplate"
                         TargetType="visuals:LabeledTextBox">
            <!-- this StackPanel arranges the items contained within it horizontally -->
            <StackPanel Orientation="Horizontal">
                <!-- TextBlock whose text is bound to the Label Styled Property
                     of its template parent control -->
                <TextBlock Text="{Binding Label, 
                           RelativeSource={RelativeSource TemplatedParent}}"
                           VerticalAlignment="Center"/>

                <!-- TextBlock displaying the colon followed by a space - ": "-->
                <TextBlock Text=": "
                           VerticalAlignment="Center"/>

                <!-- TextBox whose Text is two-way bound to the Text Styled Property
                     of its template parent control -->
                <TextBox Text="{Binding Text, Mode=TwoWayTemplate, 
                         RelativeSource={RelativeSource TemplatedParent}}"
                         VerticalAlignment="Center"
                         Width="200"/>
            </StackPanel>
        </ControlTemplate>
    </Window.Resources>
    <visuals:LabeledTextBox Label="The Text"
                            Text="Hello World!"
                            Template="{StaticResource LabeledTextBoxTemplate}"
                            HorizontalAlignment="Center"
                            VerticalAlignment="Center"/>
</Window>

新行一如既往地是粗体的。

应用程序仍然应该产生完全相同的 UI 和完全相同的行为。

下一个子步骤是将我们的 ControlTemplate 放在 Style 中(样式比模板更适合用于创建视觉效果,因为使用样式,您还可以定义使用样式本身的视觉控件上的属性的默认值,而使用 Templates 时,您只能定义视觉控件内部的内容)。

目前,我们将把 Style 保留在 MainWindow.axaml 文件中。

为了定义样式,我们需要创建一个 <Window.Styles 部分来放置样式(这与 WPF 不同,WPF 中的样式只是 XAML 资源)。新代码如下所示。

<Window xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:visuals="clr-namespace:NP.Visuals;assembly=NP.Visuals"
        ...>
    <Window.Styles>
        <Style Selector="visuals|LabeledTextBox.HorizontalLabeledTextBox">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="visuals:LabeledTextBox">
                        <!-- this StackPanel arranges the items contained 
                             within it horizontally -->
                        <StackPanel Orientation="Horizontal">
                            <!-- TextBlock whose text is bound to the Label Styled Property
                         of its template parent control -->
                            <TextBlock Text="{Binding Label, 
                            RelativeSource={RelativeSource TemplatedParent}}"
                                       VerticalAlignment="Center"/>

                            <!-- TextBlock displaying the colon followed by a space - ": "-->
                            <TextBlock Text=": "
                                       VerticalAlignment="Center"/>

                            <!-- TextBox whose Text is 
                                 two-way bound to the Text Styled Property
                         of its template parent control -->
                            <TextBox Text="{Binding Text, Mode=TwoWay, 
                            RelativeSource={RelativeSource TemplatedParent}}"
                                     VerticalAlignment="Center"
                                     Width="200"/>
                        </StackPanel>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </Window.Styles>
    <visuals:LabeledTextBox Label="The Text"
                            Text="Hello World!"
                            Classes="HorizontalLabeledTextBox"
                            HorizontalAlignment="Center"
                            VerticalAlignment="Center"/>
</Window>  

关于 Avalonia Styles 的一些解释:Avalonia 样式的重要部分是 Selector,它是一个 string,用于定义样式应用的范围。请注意,选择器中的 XML 命名空间前缀用竖线“|”分隔,而不是冒号。这是因为冒号可以在选择器中用于一些不同的目的(超出了本文的范围)。

选择器 visuals|LabeledTextBox.HorizontalLabeledTextBox 意味着该样式将应用于所有类为 HorizontalLabeledTextBoxLabeledTextBox 控件。现在,在控件本身中,我们可以指定其 Classes 属性(在我们的例子中,它是 Classes="HorizontalLabeledTextBox")。一般来说,可以指定多个类,然后,与 WPF 不同,我们可以一次将多个样式应用于同一个控件。

重新运行应用程序,它应该仍然运行得完全相同。

下一个子步骤是在 NP.Visuals 项目下创建一个名为 Styles 的项目文件夹,并在其中创建一个 Avalonia LabeledTextBoxStyles.axaml Styles 文件。为了创建 Avalonia Styles 文件,在 **解决方案资源管理器** 中右键单击新创建的 Styles 文件夹,选择 **Add** -> **New Items** 菜单项,选择左侧的“Avalonia”选项卡,选择中间的“Styles (Avalonia)”选项,将文件名更改为“LabeledTextBoxStyles.axaml”,然后按“Add”按钮。

将命名空间 Visuals 添加到新创建的 LabeledTextBoxStyles.axaml 文件中,以指向同一项目中的 NP.Visuals 命名空间,并将样式从 MainWindow.axaml 文件移到同一个 LabeledTextBoxStiles.axaml 文件中。LabeledTextBoxStyles.axaml 现在应该如下所示。

<Styles xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:visuals="clr-namespace:NP.Visuals">
    ...

    <Style Selector="visuals|LabeledTextBox.HorizontalLabeledTextBox">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="visuals:LabeledTextBox">
                    <!-- this StackPanel arranges the 
                         items contained within it horizontally -->
                    <StackPanel Orientation="Horizontal">
                        <!-- TextBlock whose text is bound to the Label Styled Property
                         of its template parent control -->
                        <TextBlock Text="{Binding Label, 
                        RelativeSource={RelativeSource TemplatedParent}}"
                                   VerticalAlignment="Center"/>

                        <!-- TextBlock displaying the colon followed by a space - ": "-->
                        <TextBlock Text=": "
                                   VerticalAlignment="Center"/>

                        <!-- TextBox whose Text is two-way bound to the Text Styled Property
                         of its template parent control -->
                        <TextBox Text="{Binding Text, Mode=TwoWay, 
                        RelativeSource={RelativeSource TemplatedParent}}"
                                 VerticalAlignment="Center"
                                 Width="200"/>
                    </StackPanel>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</Styles>  

我们还需要在 MainWindow.axaml 文件中添加对新 LabeledTextBoxStyles.axaml 的引用。

<StyleInclude Source="avares://NP.Visuals/Styles/LabeledTextBoxStyles.axaml"/>

avares 是一个魔法词 - 它是 Avalonia Resources 的缩写,后面跟着程序集名称(NP.Visuals),然后是程序集中的文件路径(/Styles/LabeledTextBoxStyles.axaml)。

我们的 MainWindow.axaml 文件现在应该看起来像这样。

<Window xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:visuals="clr-namespace:NP.Visuals;assembly=NP.Visuals"
        ...>
    <Window.Styles>
        <StyleInclude Source="avares://NP.Visuals/Styles/LabeledTextBoxStyles.axaml"/>
    </Window.Styles>
    <visuals:LabeledTextBox Label="The Text"
                            Text="Hello World!"
                            Classes="HorizontalLabeledTextBox"
                            HorizontalAlignment="Center"
                            VerticalAlignment="Center"/>
</Window>

再次运行原型,应用程序仍然看起来和行为完全相同。

我们基本上完成了原型。我建议您保留它,以便将来可能修改和修复 LabeledTextBox 控件及其样式。

步骤 5 - 在需要的地方将对可重用代码的调用添加到主应用程序

回到主应用程序并打开解决方案文件 /src/NP.PDD.MainApp.sln

该解决方案已经包含 NP.Visuals 项目,因此,在该项目下,您应该能够看到新添加的 LabeledTextBox.csStyles/LabeledTextBoxStyles.axaml 文件。

主项目已经引用了 NP.Visuals 可重用项目。

对于我们的演示,添加一个可重用的 LabeledTextBox 控件就足以证明它在主应用程序中也能正常工作。我们可以将其添加到 MainWindow 的中心,方法是复制原型中 MainWindow.axaml 文件的代码。我们也可以复制添加 Avalonia 样式文件引用的代码。

主应用程序中结果的 MainWindow.axaml 文件现在看起来与原型非常相似,唯一的区别在于应用程序的 C# 命名空间。

<Window xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:visuals="clr-namespace:NP.Visuals;assembly=NP.Visuals"
        Width="600"
        Height="400"
        x:Class="NP.PDD.MainApp.MainWindow"
        Title="NP.PDD.MainApp">
    <Window.Styles>
        <StyleInclude Source="avares://NP.Visuals/Styles/LabeledTextBoxStyles.axaml"/>
    </Window.Styles>
    <visuals:LabeledTextBox Label="The Text"
                            Text="Hello World!"
                            Classes="HorizontalLabeledTextBox"
                            HorizontalAlignment="Center"
                            VerticalAlignment="Center"/>
</Window> 

启动主应用程序 - 它应该看起来和行为与我们的原型完全相同。

结论

本文描述了原型驱动开发 (PDD) - 一种快速开发高质量软件(尤其是 UI 软件)的新方法,我在过去 13 年多以来广泛而成功地使用了这种方法。

PDD 可以由任何个人开发人员以及整个团队进行实践。

PDD 的主要优势在于,它允许快速重新编译、重新启动和调试一个小型轻量级原型,而不是一个庞大而沉重的应用程序。

本文还介绍了在 Avalonia 中创建自定义控件以及重构代码,使其成为可重用项目的一部分并可供其他项目使用的步骤。

然而,主要重点是 PDD,因此如果您不理解与 Avalonia 相关的所有内容,请不用担心。

历史

  • 2022 年 2 月 3 日:初始版本
© . All rights reserved.