跨平台WPF框架Avalonia教程 八

构建跨平台应用程序

本指南介绍了Avalonia,并概述了如何构建跨平台应用程序,以最大程度地重用代码,并在所有主要平台(Windows、Linux、macOS、iOS、Android和WebAssembly)上提供高质量的用户界面体验。

与Xamarin.Forms和MAUI方法不同,这些方法往往会产生具有最低公共特性集和通用外观的应用程序界面,Avalonia UI鼓励利用其绘制UI的能力。它允许开发人员只编写一次数据存储和业务逻辑代码,同时在所有平台上提供响应式和高性能的用户界面。本文讨论了实现这一目标的一般架构方法。

以下是创建Avalonia跨平台应用程序的关键要点摘要:

  1. 使用.NET - 使用C#、F#或VB.NET开发应用程序。使用Avalonia可以无缝地将现有的.NET代码移植到Windows、Linux、macOS、iOS、Android和WebAssembly。
  2. 利用MVVM设计模式 - 使用"模型/视图/视图模型"模式开发应用程序的用户界面。这种方法促进了"模型"和"视图"之间的明确分离,而"视图模型"充当中间人。这确保了您的UI逻辑与底层平台无关,从而促进了代码重用和可维护性。
  3. 利用Avalonia的绘图能力 - Avalonia不依赖于本地UI控件,而是类似于Flutter,绘制整个UI。这不仅确保了在所有平台上的一致外观和体验,还提供了无与伦比的自定义水平,使您能够根据实际需求定制UI。
  4. 平衡核心代码和特定平台代码 - 实现高代码重用的关键是在平台无关的核心代码和特定平台代码之间取得适当的平衡。核心代码包括与底层操作系统没有直接交互的所有内容。

架构

使用Avalonia构建跨平台应用程序的一个关键方面是创建一种架构,可以在不同平台之间实现最大程度的代码共享。通过遵循面向对象编程的基本原则,您可以建立一个结构良好的应用程序:

  1. 封装 - 这涉及确保类和架构层只公开执行其必要功能的最小API,同时隐藏内部实现细节。在实际应用中,这意味着对象作为"黑盒"运行,使用它们的代码不需要理解其内部工作原理。在架构上,这意味着实现促进简化API的模式,如外观模式,代表更高抽象层中的代码进行更复杂的交互。因此,UI代码应仅关注显示屏幕和接受用户输入,而不直接与数据库或其他低级操作进行交互。
  2. 责任分离 - 每个组件,无论是在架构层还是类级别,都应具有明确和定义的目的。每个组件应执行其指定的任务,并通过API将该功能暴露给其他需要使用它的类。
  3. 多态性 - 编程到支持多个实现的接口(或抽象类)允许核心代码在不同平台之间编写和共享,同时与Avalonia提供的特定于平台的功能进行交互。

这些原则的结果是一个模拟现实世界或抽象实体的应用程序,具有明确的逻辑层次结构。

将代码分离为层使应用程序更易于理解、测试和维护。建议将每个层中的代码物理上分开(可以在不同的目录中或者对于较大的应用程序甚至是不同的项目中)以及逻辑上分开(使用命名空间)。使用Avalonia,您不仅可以共享业务逻辑,还可以跨平台共享UI代码,从而减少了对多个UI项目的需求,并进一步增强了代码重用。

典型的应用程序层次结构

在本文档和相关案例研究中,我们引用以下五个应用程序层:

  1. 数据层 - 这是非易失性数据持久化发生的地方,通常通过像SQLite或LiteDB这样的数据库,但也可以使用XML文件或其他适当的机制来实现。
  2. 数据访问层 - 这一层是围绕数据层的包装器,提供对数据的创建、读取、更新、删除(CRUD)操作,而不向调用者透露实现细节。例如,数据访问层可能包含与数据交互的SQL查询,但引用它的代码不需要知道这一点。
  3. 业务层 - 有时也称为业务逻辑层或BLL,该层包含业务实体定义(模型)和业务逻辑。它是业务外观模式的一个主要候选者。
  4. 服务访问层 - 这一层用于访问云中的服务,从复杂的Web服务(REST,JSON)到从远程服务器检索数据和图像的简单操作。它封装了网络行为,并提供了一个简化的API,供应用程序和UI层使用。
  5. 应用程序层 - 这一层包含通常是特定于平台的代码或特定于应用程序的代码(通常不可重用)。在Avalonia框架中,这一层决定了是否利用特定于平台的功能。在Avalonia中,由于UI代码可以在各个平台之间共享,因此这一层与UI层之间的区别变得更加清晰。
  6. 用户界面(UI)层 - 这个面向用户的层包含视图和管理它们的视图模型。与传统架构不同,Avalonia使得这一层可以在每个支持的平台上共享,而不是特定于平台。

一个应用程序可能不包含所有的层 - 例如,如果一个应用程序不访问网络资源,那么服务访问层就不会存在。一个更简单的应用程序可能会合并数据层和数据访问层,因为操作非常基本。使用Avalonia,您可以根据自己的特定需求灵活地塑造应用程序架构,享受在不同平台上高度可重用的代码。

常见的架构模式

模式是一种既定的方法,用于解决重复出现的常见问题。在使用Avalonia构建可维护和可理解的应用程序时,理解以下几个关键模式非常有价值。

Model-View-ViewModel(MVVM)

MVVM是一种常用且经常被误解的模式,主要用于构建用户界面,促进了UI屏幕的实际定义(视图)、背后的逻辑(视图模型)以及填充它的数据(模型)之间的分离。视图模型充当视图和模型之间的中介。模型虽然很重要,但它是一个独立且可选的部分,因此理解这种模式的本质在于理解视图和视图模型之间的关系。

信息

了解更多关于MVVM的信息

业务外观

也称为管理器模式,它为复杂操作提供了一个简化的入口点。例如,在任务跟踪应用程序中,您可能会有一个TaskManager类,其中包含诸如GetAllTasks()、GetTask(taskID)、SaveTask(task)等方法。TaskManager类为保存/检索任务对象的内部机制提供了一个外观。

单例

单例模式确保只能存在一个特定对象的实例。例如,在应用程序中使用SQLite时,通常只希望有一个数据库实例。单例模式是强制执行这一点的有效方法。

提供程序

这是微软最初为促进在Silverlight、WPF和WinForms应用程序之间重用代码而创造的一种模式。可以针对接口或抽象类编写共享代码,并在使用代码时编写和传递特定于平台的具体实现。在Avalonia中,由于我们可以共享UI和应用程序逻辑,这种模式可以帮助处理特定于平台的异常或利用特定于平台的功能。

异步

不要将异步模式与Async关键字混淆,异步模式用于在不阻塞UI或当前处理的情况下执行长时间运行的任务。在其最简单的形式中,异步模式描述了长时间运行的任务应在另一个线程(或类似的线程抽象,如任务)中启动,而当前线程继续处理并等待来自后台进程的响应,在返回数据和/或状态时更新UI。这对于在Avalonia应用程序中保持响应式UI至关重要。


上述每种模式都将在我们的案例研究中进行深入探讨,以展示它们的实际应用。为了更全面地了解外观模式单例模式提供程序以及设计模式的一般情况,您可能希望深入研究维基百科等平台上提供的资源。

建立跨平台解决方案

尽管平台多样化,Avalonia项目都使用相同的解决方案文件格式(Visual Studio的.SLN文件格式)。解决方案可以在开发环境之间共享,为多平台应用程序开发提供统一的方法。

创建新的跨平台应用程序的第一步是创建一个解决方案。本节将详细介绍接下来的步骤:使用Avalonia构建跨平台应用程序的项目设置过程。

填充解决方案

Avalonia Cross Platform Application模板创建了一个解决方案结构,其中包括以下项目,以无缝地实现在多个平台上共享和重用代码:

信息

确保已安装Avalonia模板。

核心项目

这是应用程序的核心,旨在与平台无关。它包含应用程序的所有可重用组件,包括业务逻辑、视图模型和视图。所有其他项目都引用此核心项目。大部分开发工作应该在这里进行。

桌面项目

该项目使应用程序能够在Windows、macOS和Linux平台上运行,输出类型为WinExe

Android项目

这是一个基于.NET-Android的项目,它引用了核心项目。它包含一个从AvaloniaMainActivity继承的MainActivity,作为Android应用程序的入口点。

iOS项目

这是一个专为iOS和iPadOS平台定制的.NET-iOS项目。该项目的入口点是继承自AvaloniaAppDelegateAppDelegate

浏览器项目

这个WebAssembly(WASM)项目允许您的Avalonia应用程序在Web浏览器中运行。它的RuntimeIdentifierbrowser-wasm

核心项目

共享代码项目只应引用在所有平台上普遍可用的程序集。通常包括常见的框架命名空间,如SystemSystem.CoreSystem.Xml

这些共享项目旨在尽可能实现应用程序的大部分功能,包括UI组件,从而最大限度地提高代码的可重用性。

通过将功能分离到不同的层中,代码变得更容易管理、测试和在多个平台上重用。Avalonia UI项目中的这种分层架构方法促进了应用程序开发的效率和可扩展性。

特定平台的应用程序项目

特定平台的项目必须引用核心项目。特定平台的项目存在的目的是使应用程序能够在包括iOS、Android和WASM在内的独特平台上运行。

虽然桌面平台可以共享一个项目,但为macOS创建一个单独的项目可能更有益,使用Xamarin.Mac目标框架。这将使您的应用程序更容易分发和打包。

处理多个平台

管理平台差异和功能

平台差异不仅存在于跨平台开发中,即使在同一平台的设备中也可能具有不同的功能。

最明显的是屏幕尺寸的差异,但还有许多其他设备特性可能会有所不同,需要应用程序验证某些功能并根据其存在(或不存在)来调整其行为。这在设计跨范式情况下尤为重要,桌面和移动操作系统提供非常不同的交互模型。

因此,所有应用程序必须能够优雅地降低功能,否则可能会呈现出仅具有最小功能集的情况,无法充分利用底层平台的全部潜力。

平台差异的示例

有一些应用程序固有的基本特征是普遍适用的。这些是高级概念,在所有设备和平台上都适用,因此可以构成应用程序设计的核心:

  • 屏幕,可以显示应用程序的用户界面。
  • 某种形式的输入设备,通常是移动设备的触摸和桌面的鼠标和键盘。
  • 显示数据的视图。
  • 编辑数据。
  • 导航功能。

平台特定功能

除了通用应用程序特征之外,您还需要在设计中解决关键的平台差异。您可能需要考虑并可能需要编写或调整代码来处理这些差异:

  • 屏幕尺寸:某些平台(如iOS)具有标准化的屏幕尺寸,相对容易定位,而其他平台(如桌面和WebAssembly)则可以支持各种屏幕尺寸,这需要更多的工作来支持您的应用程序。

  • 导航隐喻:这些在平台之间可能会有所不同(例如,硬件的"返回"按钮),甚至在平台内部也可能有所不同(例如,Android 2和4之间的差异,iPhone与iPad之间的差异)。

  • 键盘:某些设备可能配备物理键盘,而其他设备只有软键盘。检测软键盘遮挡屏幕部分时的代码需要对这些差异进行敏感处理。

在设计Avalonia应用程序时,应仔细考虑这些平台特定差异,以确保在所有平台上实现无缝的用户体验。虽然您应该努力最大程度地重用代码,但也应避免尝试在所有支持的平台上100%重用代码。相反,为每个平台的用户界面定制以适应设备的感觉。

处理平台差异

通过抽象平台功能,可以从相同的代码库支持多个平台。

  • 平台抽象:此方法利用业务外观模式,在各个平台上提供统一的访问。它将独特的平台实现抽象为一个单一的、连贯的API。其主要优点是能够编写与平台无关的代码,增强了代码的可重用性和可维护性。然而,这种方法可能无法充分利用每个平台的独特功能和能力。

平台抽象

在Avalonia中,您可以使用类抽象来简化在不同平台上的开发过程。这可以通过在共享代码中定义接口或基类,然后在特定平台的项目中实现或扩展来实现。

接口

使用接口可以创建特定于平台的类,这些类可以并入共享库以实现代码重用。

工作原理

接口在共享代码中定义,并作为参数或属性传递到共享库中。然后,特定于平台的应用程序可以实现接口,使共享代码能够有效处理它。

优点

这种方法的主要优点是实现可以包含特定于平台的代码,甚至可以引用特定于平台的外部库,提供了很高的灵活性。

缺点

潜在的缺点是需要创建和传递实现到共享代码中。如果接口在共享代码中被深度使用,可能需要通过多个方法参数传递,这可能导致更复杂的调用链。如果共享代码使用了多个不同的接口,所有这些接口都必须在共享代码中创建和设置。

继承

您的共享代码可以实现抽象或虚拟类,这些类可以在一个或多个特定平台的项目中进行扩展。这种技术类似于使用接口,但提供了一些已经实现的行为。

工作原理

通过使用继承,您可以在共享代码中创建基类,可以选择在特定平台的项目中进行扩展。然而,由于C#只允许单继承,这种方法可能会影响您未来的API设计。因此,要谨慎使用继承。

优点和缺点

使用接口的优点和缺点同样适用于继承。然而,继承的一个额外优点是基类可以包含一些实现代码。这可能提供一个完整的平台无关实现,可以根据需要进行扩展。

如何在 Avalonia 中实现依赖注入

依赖注入(DI) 允许开发人员编写更简洁、模块化和可测试的代码。它通过创建分立的服务来实现这一点,这些服务会根据需要被传递/创建。

本指南将逐步向您介绍如何在 Avalonia UI 和 MVVM 模式中使用依赖注入(DI)。

步骤 0:背景与初始代码

假设您有一个包含 MainViewModel、BusinessService 和 Repository 的应用程序。MainViewModel 依赖于 IBusinessService,而 BusinessService 依赖于 IRepository。简单的实现过程如下:

public partial class MainViewModel
{
    private readonly IBusinessService _businessService;

    public MainViewModel(IBusinessService businessService)
    {
        _businessService = businessService;
    }
}

public class BusinessService : IBusinessService
{
    private readonly IRepository _repository;

    public BusinessService(IRepository repository)
    {
        _repository = repository;
    }
}

public class Repository : IRepository
{
}

通常情况下,您会直接实例化 Repository 并将其传入 BusinessService 然后再传入 MainViewModel,就像这样:

var window = new MainWindow
{
    DataContext = new MainViewModel(new BusinessService(new Repository()))
}

对于不经常使用且不会改变的简单构造函数,这种方法非常有效。但这种技术的扩展性并不好,因为:

  • 构造函数的依赖关系越多,需要实例化和传入的东西就越多。在本地实例化依赖项(例如通过执行 new MainViewModel(new MyService()))会导致与依赖项的特定实例直接刚性耦合。
  • 同样,如果 MainViewModel 自己创建其依赖关系(如在构造函数主体中),它也会直接与依赖关系的创建耦合,从而导致大部分相同的问题。
  • 此外,如果对象在许多地方被实例化,当 MainViewModel 的依赖关系发生变化时(如需要额外的依赖关系或需要依赖关系的不同实现),需要对每个引用都进行更新。

依赖注入通过抽象对象的创建及其依赖关系解决了这些问题。这样就可以使用封装良好的服务,这些服务会自动传递给注册使用它们的任何其他服务。

步骤 1:安装 DI 的 NuGet 软件包

目前有许多依赖注入(DI)容器提供商(DryIoCAutofacPure.DI),但本指南只关注 "Microsoft.Extensions.DependencyInjection",它是一个轻量级、可扩展的依赖注入容器。它为.NET 应用程序(包括基于 Avalonia 的桌面应用程序)添加依赖注入提供了一种易于使用且基于约定的方法。

在项目目录下的终端中运行以下命令安装 DI 软件包:

dotnet add package Microsoft.Extensions.DependencyInjection

步骤 2:添加服务集合扩展

下面的代码将为 IServiceCollection创建一个扩展方法,该方法将把服务注册到我们的服务集合,并使它们可用于注入。

public static class ServiceCollectionExtensions {
    public static void AddCommonServices(this IServiceCollection collection) {
        collection.AddSingleton<IRepository, Repository>();
        collection.AddTransient<BusinessService>();
        collection.AddTransient<MainViewModel>();
    }
}

步骤 3:修改 App.axaml.cs

接下来,应修改 App.xaml.cs 类以使用 DI 容器。这将能通过依赖注入容器解析先前注册的视图模型。然后就可以将完全实现的视图模型设置为主视图的数据上下文。

public class App : Application
{
    public override void Initialize()
    {
        AvaloniaXamlLoader.Load(this);
    }

    public override void OnFrameworkInitializationCompleted()
    {
        // 如果使用 CommunityToolkit,则需要用下面一行移除 Avalonia 数据验证。
        // 如果没有这一行,数据验证将会在 Avalonia 和 CommunityToolkit 中重复。
        BindingPlugins.DataValidators.RemoveAt(0);

        // 注册应用程序运行所需的所有服务
        var collection = new ServiceCollection();
        collection.AddCommonServices();

        // 从 collection 提供的 IServiceCollection 中创建包含服务的 ServiceProvider
        var services = collection.BuildServiceProvider();

        var vm = services.GetRequiredService<MainViewModel>();
        if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
        {
            desktop.MainWindow = new MainWindow
            {
                DataContext = vm
            };
        }
        else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform)
        {
            singleViewPlatform.MainView = new MainView
            {
                DataContext = vm
            };
        }

        base.OnFrameworkInitializationCompleted();
    }
}

开发者工具

Avalonia 内置了一个开发工具窗口,可以通过在 Window 构造函数中调用附加的 AttachDevTools() 方法来启用。在程序以 DEBUG 模式编译时,默认模板已启用此功能:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }
}

// 在由 Avalonia.NameGenerator 自动生成的 MainWindow.g.cs中:
partial class MainWindow
{
    // ...
    public void InitializeComponent(bool loadXaml = true, bool attachDevTools = true)
    {
        // ...
#if DEBUG
        if (attachDevTools)
        {
            this.AttachDevTools();
        }
#endif
        // ...
    }
}

要打开 DevTools,请按 F12 键,或向 this.AttachDevTools() 方法传递不同的 Gesture 手势。

信息

要使用 DevTools,必须添加 Avalonia.Diagnostics nuget 软件包。

dotnet add package Avalonia.Diagnostics --version 11.0.0

但默认情况下,它已经安装好了。

在 .NET core 2.1 下运行时存在一个已知问题,即按 F12 会导致程序退出。在这种情况下,要么切换到 .NET core 2.0 或 3.0+,要么将打开手势改为其他手势,如 "Ctrl+F12"。

逻辑树和视觉树

Logical TreeVisual Tree 选项卡显示窗口逻辑树和视觉树中的控件。选择一个控件后,右侧窗格中将显示该控件的属性,可以对其进行编辑。

属性

允许快速检查和编辑控件的属性。还可以搜索属性(按名称或使用正则表达式)。

描述
Property 属性名
Value 属性的当前值
Type 当前值的类型
Priority 值的优先级

布局

允许检查和编辑常见的布局属性(MarginBorderPadding)。

控件的尺寸和尺寸约束也会显示。

信息

如果 WidthHeight 带有下划线,这表明该属性属于活动约束。悬停在值上会显示包含相关信息的工具提示。

样式

属性面板显示属性的当前活动值,而样式面板则显示所有值及其来源。

此外,还可以查看可能与该控件匹配的所有样式(通过切换 Show inactive 选项)。

按下 Snapshot 按钮或将鼠标悬停在目标窗口上时按下 Alt+S,即可快照当前样式。快照意味着样式面板不会更新以反映控件的新状态。这在排除与 :pointerover:pressed 选择器有关的问题时很有用。

信息

如果设置值与资源绑定,则会以一个圆圈表示,后面跟着资源键。

信息

如果给定值带有删除线,则表示该值被优先级更高的样式值覆盖。

Setters 有一个上下文菜单,允许快速将名称和值复制到剪贴板。

事件

事件选项卡可用于跟踪 事件 的传播。在左侧窗格中选择要跟踪的事件类型,所有此类型的事件将显示在中上方窗格中。选择其中一个事件以查看事件路由。

信息

事件名称或控件类型下的虚线表示可以进行快速导航。

  • 双击事件类型将选择并滚动到给定的事件类型
  • 双击控件类型(和/或名称)将导航到视觉树选项卡并选择该控件。

热键

组合键 功能
Alt+S 启用快照样式
Alt+D 禁用快照样式
CTRL+Shift 检查指针指向的控件
CTRL+Alt+F 切换弹出窗口冻结
F8 对逻辑树或视觉树中的选定项目截图

示例

更改属性值

更改布局属性

如何使用 INotifyPropertyChanged

介绍

INotifyPropertyChanged 接口是模型-视图-视图模型(MVVM)设计模式中的关键组件,有助于创建可扩展和易于维护的应用程序。通过通知属性已更改,它允许视图自动更新,改善应用程序组件之间的通信。

什么是 INotifyPropertyChanged?

INotifyPropertyChanged 是 .NET 提供的一个接口,类可以实现该接口以表示属性已更改其值。这在数据绑定场景中特别有用,当绑定的数据发生变化时,可以自动更新用户界面(UI)。

INotifyPropertyChanged 接口具有一个事件成员,即 PropertyChanged。当属性的值更改时,对象会引发 PropertyChanged 事件,以通知任何已绑定的元素属性已更改。

为什么在 MVVM 中 INotifyPropertyChanged 很重要?

在 MVVM 模式中,视图模型(ViewModel)封装了视图的交互逻辑,并封装了来自模型的数据。视图绑定到视图模型中的属性,而视图模型则公开了模型对象中包含的数据。

为了使 MVVM 模式正常工作,当基础数据发生更改时,视图需要得到更新。这就是 INotifyPropertyChanged 的作用。通过在视图模型中实现此接口,您可以通知视图模型中的视图有关模型更改的信息,从而自动更新用户界面。

实现 INotifyPropertyChanged

以下是如何实现 INotifyPropertyChanged 的示例:

public class MyViewModel : INotifyPropertyChanged
{
    private string _name;

    public string Name
    {
        get { return _name; }
        set
        {
            _name = value;
            OnPropertyChanged(nameof(Name));
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

在此代码中,每当将 Name 属性设置为新值时,将调用 OnPropertyChanged 方法,该方法会引发 PropertyChanged 事件。与此属性绑定的任何用户界面元素将会更新以反映新值。

使用 MVVM Toolkit 简化 INotifyPropertyChanged

尽管实现 INotifyPropertyChanged 并不是特别复杂,但如果视图模型中有许多属性,可能会变得乏味。幸运的是,.NET 社区工具包的 MVVM 库通过使用其 ObservableObject 类以及 Source Generators 功能结合 [ObservableProperty] Attribute,提供了更高效的实现 INotifyPropertyChanged 的方式。

以下是如何使用 ObservableObject 实现相同结果的示例:

using CommunityToolkit.Mvvm.ComponentModel;

public partial class MyViewModel : ObservableObject
{
    [ObservableProperty]
    private string _name;
}

在此代码中,ObservableObject 类实现了 INotifyPropertyChanged[ObservableProperty] Attribute 用于指示 _name 是可观察的属性。Source Generator 将在幕后生成必要的样板代码,包括属性的 getter 和 setter,并在属性更改时自动调用 OnPropertyChanged 方法。这使得实现更加清晰且更不容易出错。

MVVM Toolkit 提供了一系列工具,可帮助简化 .NET 应用程序中 MVVM 模式的实现,包括简化使用 INotifyPropertyChanged。使用 Source Generators 可使您的代码更高效和可读,同时保持相同的功能。

如何绑定样式类

本指南将向您展示如何根据数据绑定的布尔值为控件应用样式类。

要做到这一点,您需要在一个 <Styles> 集合中定义一些针对您正在使用的控件类的类。

然后,您可以使用特殊的类语法和数据绑定有条件地将类应用于控件。语法如下:

<SomeControl Classes.class1="{Binding IsClass1Active}">

示例

在这个示例中,已经定义了两个带有类选择器的样式。这些样式为文本块提供了红色或绿色的背景。当项目的 IsClass1 属性为真时,样式类绑定将分配 class1。使用否定运算符,当 IsClass1 属性为假时,将分配 class2

XAML

<StackPanel Margin="20">
  <ListBox ItemsSource="{Binding ItemList}">
    <ListBox.Styles>
      <Style Selector="TextBlock.class1">
        <Setter Property="Background" Value="OrangeRed" />
      </Style>
      <Style Selector="TextBlock.class2">
        <Setter Property="Background" Value="PaleGreen" />
      </Style>
    </ListBox.Styles>
    <ListBox.ItemTemplate>
      <DataTemplate>
        <StackPanel>
          <TextBlock
              Classes.class1="{Binding IsClass1 }"
              Classes.class2="{Binding !IsClass1 }"
              Text="{Binding Title}"/>
        </StackPanel>
      </DataTemplate>
    </ListBox.ItemTemplate>
  </ListBox>
</StackPanel>

C#

public class MainWindowViewModel : ViewModelBase
{
    public ObservableCollection<ItemClass> ItemList { get; set; }

    public MainWindowViewModel()
    {
        ItemList = new ObservableCollection<ItemClass>(new List<ItemClass>
        {
            new ItemClass("Item 1", false),
            new ItemClass("Item Two", false),
            new ItemClass("Third Item", true),
            new ItemClass("Item #4", false),
               
        });
    }
}

ItemClass.cs

public class ItemClass
{
    public string Title { get; set; }
    public bool IsClass1 { get; set; }

    public ItemClass(string title, bool isClass1 )
    {
        Title = title;
        IsClass1 = isClass1;
    }
}

如何绑定到控件

Avalonia UI 中,除了绑定到数据上下文(DataContext)外,您还可以直接将一个控件绑定到另一个控件。

信息

请注意,这种技术完全不使用数据上下文。在执行此操作时,您是直接将一个控件绑定到另一个控件本身。

绑定到命名控件

如果要绑定到另一个命名控件上的属性,可以使用以 # 字符为前缀的控件名称。

<TextBox Name="other">

<!-- 绑定到命名为 other 控件的 Text 属性 -->
<TextBlock Text="{Binding #other.Text}"/>

这相当于 WPF 和 UWP 开发者熟悉的长格式绑定:

<TextBox Name="other">
<TextBlock Text="{Binding Text, ElementName=other}"/>

Avalonia UI 支持这两种语法。

绑定到祖先控件

您可以使用 $parent 语法绑定到目标的(逻辑控件树)父级:

<Border Tag="Hello World!">
  <TextBlock Text="{Binding $parent.Tag}"/>
</Border>

或者使用带有 $parent 语法的索引绑定到任何级别的祖先:

<Border Tag="Hello World!">
  <Border>
    <TextBlock Text="{Binding $parent[1].Tag}"/>
  </Border>
</Border>

索引从0开始,因此 $parent[0] 等同于 $parent

您还可以绑定到指定类型的最近祖先,如下所示:

<Border Tag="Hello World!">
  <Decorator>
    <TextBlock Text="{Binding $parent[Border].Tag}"/>
  </Decorator>
</Border>

最后,您可以结合索引和类型:

<Border Tag="Hello World!">
  <Border>
    <Decorator>
    <TextBlock Text="{Binding $parent[Border;1].Tag}"/>
    </Decorator>
  </Border>
</Border>

如果需要在祖先类型中包含 XAML 命名空间,则使用冒号分隔命名空间和类名,如下所示:

<local:MyControl Tag="Hello World!">
  <Decorator>
    <TextBlock Text="{Binding $parent[local:MyControl].Tag}"/>
  </Decorator>
</local:MyControl>

注意

Avalonia UI 还支持 WPF/UWP 的 RelativeSource 语法,类似但并不相同。RelativeSource视觉 树上起作用,而此处给出的语法在 逻辑 树上起作用。

如何绑定到集合

在 Avalonia UI 中绑定到集合是一种有效的显示动态数据的方法。本指南将演示如何将 ObservableCollection 绑定到控件,比如 ListBoxItemsControl,以显示一系列项目。

绑定到简单的 ObservableCollection

首先,假设您有一个 ObservableCollection<string>,您希望将其绑定到一个 ListBox 以显示字符串项目的列表。

以下是一个带有 ObservableCollection<string> 的示例 ViewModel

public class ViewModel : ObservableObject
{
    private ObservableCollection<string> _items;

    public ObservableCollection<string> Items
    {
        get { return _items; }
        set { SetProperty(ref _items, value); }
    }

    public ViewModel()
    {
        Items = new ObservableCollection<string> { "Item 1", "Item 2", "Item 3" };
    }
}

在您的视图中,您可以这样将这个 ObservableCollection 绑定到 ListBox

<ListBox ItemsSource="{Binding Items}"/>

绑定到一个包含复杂对象的 ObservableCollection

但如果您的 ObservableCollection 包含复杂对象,这些对象本身也需要传播更改怎么办?让我们修改我们的 ViewModel 来适应这种情况。

考虑一个 Person 类:

public class Person : ObservableObject
{
    private string _name;
    private int _age;

    public string Name
    {
        get { return _name; }
        set { SetProperty(ref _name, value); }
    }

    public int Age
    {
        get { return _age; }
        set { SetProperty(ref _age, value); }
    }
}

以及在我们的 ViewModel 中的一个 ObservableCollection<Person>

public class ViewModel : ObservableObject
{
    private ObservableCollection<Person> _people;

    public ObservableCollection<Person> People
    {
        get { return _people; }
        set { SetProperty(ref _people, value); }
    }

    public ViewModel()
    {
        People = new ObservableCollection<Person> 
        {
            new Person { Name = "John Doe", Age = 30 },
            new Person { Name = "Jane Doe", Age = 28 }
        };
    }
}

您可以在视图中将这个 ObservableCollection 绑定到 ListBox,并使用 DataTemplate 来指定每个 Person 应该如何呈现:

<ListBox ItemsSource="{Binding People}">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="{Binding Name}" Margin="0,0,10,0"/>
                <TextBlock Text="{Binding Age}"/>
            </StackPanel>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

在这种情况下,列表中的每个 Person 将以其 NameAge 分开的方式显示,并带有一个小间距。如果任何项目的属性发生更改,ListBox 项目将自动更新。

如何从代码中绑定

在Avalonia中,从代码中绑定与WPF/UWP中的方式有些不同。在底层,Avalonia的绑定系统基于Reactive Extensions的 IObservable,然后由XAML绑定进行构建(这些绑定也可以在代码中实例化)。

订阅属性的更改

您可以通过调用 GetObservable 方法来订阅属性的更改。这将返回一个 IObservable<T>,可用于监听属性的更改:

var textBlock = new TextBlock();
var text = textBlock.GetObservable(TextBlock.TextProperty);

每个可订阅的属性都有一个静态只读字段,称为 [PropertyName]Property,该字段在 GetObservable 中传递以订阅属性的更改。

IObservable(是Reactive Extensions的一部分,简称为rx)超出了本指南的范围,但以下是一个示例,该示例使用返回的可观察对象将更改的属性值打印到控制台:

var textBlock = new TextBlock();
var text = textBlock.GetObservable(TextBlock.TextProperty);
text.Subscribe(value => Console.WriteLine(value + " Changed"));

当订阅返回的可观察对象时,它将立即返回属性的当前值,然后在每次属性更改时推送一个新值。如果您不想要当前值,可以使用 rx 的 Skip 运算符:

var text = textBlock.GetObservable(TextBlock.TextProperty).Skip(1);

绑定到可观察对象

您可以使用 AvaloniaObject.Bind 方法将属性绑定到可观察对象:

// 在这里我们使用Rx Subject,以便我们可以使用OnNext推送新值
var source = new Subject<string>();
var textBlock = new TextBlock();

// 将TextBlock.Text绑定到source
var subscription = textBlock.Bind(TextBlock.TextProperty, source);

// 将textBlock.Text设置为"hello"
source.OnNext("hello");
// 将textBlock.Text设置为"world!"
source.OnNext("world!");

// 终止绑定
subscription.Dispose();

请注意,Bind 方法返回一个 IDisposable,可用于终止绑定。如果您从不调用此方法,那么当可观察对象通过 OnCompletedOnError 结束时,绑定将自动终止。

在对象初始化器中设置绑定

在对象初始化器中设置绑定通常很有用。您可以使用索引器来实现此目的:

var source = new Subject<string>();
var textBlock = new TextBlock
{
    Foreground = Brushes.Red,
    MaxWidth = 200,
    [!TextBlock.TextProperty] = source.ToBinding(),
};

使用此方法,您还可以轻松地将一个控件的属性绑定到另一个控件的属性:

var textBlock1 = new TextBlock();
var textBlock2 = new TextBlock
{
    Foreground = Brushes.Red,
    MaxWidth = 200,
    [!TextBlock.TextProperty] = textBlock1[!TextBlock.TextProperty],
};

当然,索引器也可以在对象初始化器之外使用:

textBlock2[!TextBlock.TextProperty] = textBlock1[!TextBlock.TextProperty];

这种语法的唯一缺点是不会返回 IDisposable。如果您需要手动终止绑定,则应使用 Bind 方法。

转换绑定值

因为我们使用的是可观察对象,所以可以很容易地转换我们绑定的值!

var source = new Subject<string>();
var textBlock = new TextBlock
{
    Foreground = Brushes.Red,
    MaxWidth = 200,
    [!TextBlock.TextProperty] = source.Select(x => "Hello " + x).ToBinding(),
};

从代码中使用 XAML 绑定

有时,当您想要使用XAML绑定提供的附加功能时,从代码中使用 XAML 绑定会更加容易。例如,仅使用可观察对象,您可以像这样绑定到 DataContext 上的属性:

var textBlock = new TextBlock();
var viewModelProperty = textBlock.GetObservable(TextBlock.DataContext)
    .OfType<MyViewModel>()
    .Select(x => x?.Name);
textBlock.Bind(TextBlock.TextProperty, viewModelProperty);

然而,在这种情况下,使用 XAML 绑定可能更可取:

var textBlock = new TextBlock
{
    [!TextBlock.TextProperty] = new Binding("Name")
};

或者,如果您需要一个 IDisposable 来终止绑定:

var textBlock = new TextBlock();
var subscription = textBlock.Bind(TextBlock.TextProperty, new Binding("Name"));

subscription.Dispose();

订阅任何对象的属性

GetObservable 方法返回一个可观察对象,用于跟踪单个实例上属性的更改。但是,如果您正在编写一个控件,可能希望实现一个与对象实例无关的 OnPropertyChanged 方法。

要做到这一点,您可以订阅 AvaloniaProperty.Changed,这是一个可观察对象,每次在任何实例上更改属性时都会触发该对象

在 WPF 中,通过将静态的 PropertyChangedCallback 传递给 DependencyProperty 注册方法来完成此操作,但这只允许控件作者注册属性更改回调。

此外,还有一个 AddClassHandler 扩展方法,可以自动将事件路由到控件上的方法。

例如,如果您想要监听对控件的 Foo 属性的更改,可以像这样做:

static MyControl()
{
    FooProperty.Changed.AddClassHandler<MyControl>(FooChanged);
}

private static void FooChanged(MyControl sender, AvaloniaPropertyChangedEventArgs e)
{
    // 'e' 参数描述了发生的更改。
}

绑定到实现了 INotifyPropertyChanged 的对象

也可以绑定到实现了 INotifyPropertyChanged 的对象。

var textBlock = new TextBlock();

var binding = new Binding 
{ 
    Source = someObjectImplementingINotifyPropertyChanged, 
    Path = nameof(someObjectImplementingINotifyPropertyChanged.MyProperty)
}; 

textBlock.Bind(TextBlock.TextProperty, binding);

如何创建自定义数据绑定转换器

当内置的数据绑定转换器不满足您的转换需求时,您可以根据 IValueConverter 接口编写自定义转换器。本指南将向您展示如何进行操作。

信息

若要查看 IValueConverter 接口的 Microsoft 文档,请点击此处

信息

由于在 .NET标准2.0 中无法使用 IValueConverter 接口,Avalonia UI 在 Avalonia.Data.Converters命名空间中提供了该接口的副本。您可以在这里查看有关此接口的API文档:这里

在使用自定义转换器之前,您必须在某些资源中引用它。这可以在应用程序的任何级别进行。在此示例中,自定义转换器 myConverter 被引用在 Window 资源中:

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

  <Window.Resources>
    <local:MyConverter x:Key="myConverter"/>
  </Window.Resources>

  <TextBlock Text="{Binding Value, Converter={StaticResource myConverter}}"/>
</Window>

示例

此示例数据绑定转换器可以将文本转换为特定的大小写形式,使用参数进行控制:

<TextBlock Text="{Binding TheContent, 
    Converter={StaticResource textCaseConverter},
    ConverterParameter=lower}" />

上述XAML假设已在资源中引用了 textCaseConverter

public class TextCaseConverter : IValueConverter
{
    public static readonly TextCaseConverter Instance = new();

    public object? Convert(object? value, Type targetType, object? parameter, 
                                                            CultureInfo culture)
    {
        if (value is string sourceText && parameter is string targetCase
            && targetType.IsAssignableTo(typeof(string)))
        {
            switch (targetCase)
            {
                case "upper":
                case "SQL":
                    return sourceText.ToUpper();
                case "lower":
                    return sourceText.ToLower();
                case "title": // Every First Letter Uppercase
                    var txtinfo = new System.Globalization.CultureInfo("en-US",false)
                                    .TextInfo;
                    return txtinfo.ToTitleCase(sourceText);
                default:
                    // invalid option, return the exception below
                    break;
            }
        }
        // converter used for the wrong type
        return new BindingNotification(new InvalidCastException(), 
                                                BindingErrorType.Error);
    }

    public object ConvertBack(object? value, Type targetType, 
                                object? parameter, CultureInfo culture)
    {
      throw new NotSupportedException();
    }
}

目标属性类型

您可能希望编写一个自定义转换器,根据目标属性的要求切换输出类型。您可以通过 Convert 方法接收的 targetType 参数进行测试,使用 IsAssignableTo 函数来实现这一点。

在这个示例中,animalConverter 可以为绑定的 Animal 类对象查找图像或文本名称:

XAML

<Image Width="42" 
       Source="{Binding Animal, Converter={StaticResource animalConverter}}"/>
<TextBlock 
       Text="{Binding Animal, Converter={StaticResource animalConverter}}" />

AnimalConverter.cs

public class AnimalConverter : IValueConverter
{
    public static readonly AnimalConverter Instance = new();

    public object? Convert( object? value, Type targetType, 
                                    object? parameter, CultureInfo culture )
    {
        if (value is Animal animal)
        {
            if (targetType.IsAssignableTo(typeof(IImage)))
            {
                img = @"icons/generic-animal-placeholder.png"
                switch (animal)
                {
                    case Dog d:
                      img = d.IsGoodBoy ? @"icons/dog-happy.png" 
                                                      : @"icons/dog.png";
                      break;
                    case Cat:
                      img = @"icons/cat.png";
                      break;
                    // etc. etc.
                }
                // see https://docs.avaloniaui.net/docs/guides/data-binding/how-to-create-a-custom-data-binding-converter
                return BitmapAssetValueConverter.Instance
                    .Convert(img, typeof(Bitmap), parameter, culture);
            }
            else if (targetType.IsAssignableTo(typeof(string)))
            {
                return !string.IsNullOrEmpty(animal.NickName) ? 
                    $"{animal.Name} \"{animal.NickName}\"" : animal.Name;
            }
        }
        // converter used for the wrong type
        return new BindingNotification(new InvalidCastException(), 
                                                    BindingErrorType.Error);
        
    }

    public object ConvertBack( object? value, Type targetType, 
                                    object? parameter, CultureInfo culture )
    {
      throw new NotSupportedException();
    }
}

如何绑定多个属性

MultiBinding

如果需要将多个属性的运算结果绑定到目标属性,一个合理的解决方案是使用 MultiBindingMultiBinding 通过 IMultiValueConverter 来聚合多个 Binding 对象并产生运算结果。 当被聚合的任意一个 Binding 属性发生通知更改时,都会调用 Convert 方法。 与 Binding 类似,MultiBinding 也可以用于绑定视图模型(ViewModels)、Control 或其它源属性。

注意

MultiBinding 只支持 BindingMode.OneTimeBindingMode.OneWay.

IMultiValueConverter

IValueConverter 相似,它定义了转换到目标属性的方式。 由于聚合是不可逆的,因此它没有 ConvertBack 方法。

public interface IMultiValueConverter
{
    object? Convert(IList<object?> values, Type targetType, object? parameter, CultureInfo culture);
}

MultiBinding Example

假设如下场景:存在红、绿、蓝三个颜色通道的输入,目标是将三个输入聚合,运算得出一个 IBrush 以供另一控件绑定并进行前景绘制。 在这个场景下,需要通过 NumericUpDown 来限制颜色通道值在允许范围内([0, 255])。 由于没有目标属性,BindingMarkupExtension 将无法正确应用,因此必须将绑定对象创建为 <Binding>

<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" Spacing="8">
    <NumericUpDown x:Name="red" Minimum="0" Maximum="255" Increment="20" Value="0" Foreground="Red" />
    <NumericUpDown x:Name="green" Minimum="0" Maximum="255" Increment="20" Value="0" Foreground="Green" />
    <NumericUpDown x:Name="blue" Minimum="0" Maximum="255" Increment="20" Value="0" Foreground="Blue" />

    <TextBlock Text="MultiBinding Text Color!" FontSize="24">
        <TextBlock.Foreground>
            <MultiBinding Converter="{StaticResource RgbToBrushMultiConverter}">
                <Binding Path="Value" ElementName="red" />
                <Binding Path="Value" ElementName="green" />
                <Binding Path="Value" ElementName="blue" />
            </MultiBinding>
        </TextBlock.Foreground>
    </TextBlock>
</StackPanel>

随后,我们来创建 IMultiValueConverter。参数的类型检查非常重要。 在当前场景中,NumericUpDown.Valuedecimal? 类型,因此必须检查 decimalnull。在绑定初始化时,其值也有可能是 UnsetValueType。 为了使转换器广泛兼容各种数字类型,还可以进行进一步的数据转换。

转换器实现

public sealed class RgbToBrushMultiConverter : IMultiValueConverter
{
    public object? Convert(IList<object?> values, Type targetType, object? parameter, CultureInfo culture)
    {
        // 确保提供了所有绑定,并且绑定到了正确的目标类型
        if (values?.Count != 3 || !targetType.IsAssignableFrom(typeof(ImmutableSolidColorBrush)))
            throw new NotSupportedException();

        // 确保所有绑定都是正确的类型
        if (!values.All(x => x is decimal or UnsetValueType or null))
            throw new NotSupportedException();

        // 提取值,如果有任何值未设置则返回 DoNothing。
        // 在绑定初始化期间,Convert会被多次调用,
        // 因此一些属性最初会是未设置的状态。
        if (values[0] is not decimal r ||
            values[1] is not decimal g ||
            values[2] is not decimal b)
            return BindingOperations.DoNothing;

        byte a = 255;
        var color = new Color(a, (byte)r, (byte)g, (byte)b);
        return new ImmutableSolidColorBrush(color);
    }
}

提示

  • 可以考虑创建一个 MarkupExtension 以简化 XAML 的语法,例如经常重用 MultiBinding 的场景。
  • 可以考虑使用 FuncMultiValueConverter 来减少较为简易的转换器的代码量。

如何绑定选项卡

绑定支持示例

您可以使用数据绑定 来动态创建选项卡项。为此,请将选项卡控件的 Items 属性绑定到一个代表选项卡标题和内容的对象数组。

然后,您可以使用**数据模板(DataTemplate)**来显示这些对象。

此示例使用从 TabItemModel 类创建的对象数组:

public class TabItemModel
{
    public string Header { get; }
    public string Content { get; }
    public TabItemModel(string header, string content)
    {
        Header = header;
        Content = content;
    }
}

创建一个包含两个 TabItemModel 实例的数组,并将其绑定到数据上下文。

DataContext = new TabItemModel[] { 
    new TabItemModel("One", "Some content on first tab"),
    new TabItemModel("Two", "Some content on second tab"),
};

TabStrip 头部内容由 ItemTemplate 属性定义,而 TabItem 的内容由 ContentTemplate 属性定义。

最后,创建一个 TabControl,并将其 Items 属性绑定到数据上下文(DataContext)。

<TabControl ItemsSource="{Binding}">
    <TabControl.ItemTemplate>
      <DataTemplate>
        <TextBlock Text="{Binding Header}" />
      </DataTemplate>
    </TabControl.ItemTemplate>
    <TabControl.ContentTemplate>
      <DataTemplate>
        <DockPanel LastChildFill="True">
          <TextBlock Text="This is content of selected tab" DockPanel.Dock="Top" FontWeight="Bold" />
          <TextBlock Text="{Binding Content}" />
        </DockPanel>
      </DataTemplate>
    </TabControl.ContentTemplate>
  </TabControl>

如何绑定图像文件

信息

要在实际操作中查看这些概念的完整可运行示例,请查看示例应用程序

在Avalonia UI中,绑定图像文件为在应用程序中显示动态图像内容提供了机会。本指南提供了有关如何从各种来源绑定图像文件的概述。

从不同来源绑定图像文件

假设您有来自不同来源(例如,本地资源或Web URL)的图像,您希望在视图中显示这些图像,下面是如何实现的:

首先,在您的 ViewModel 中,您需要定义表示这些图像来源的属性。这些属性可以是 Bitmap 类型或 Task<Bitmap> 类型(如果加载图像涉及异步操作)。使用 ImageHelper 类来加载这些图像。

public class MainWindowViewModel : ViewModelBase
{
    public Bitmap? ImageFromBinding { get; } = ImageHelper.LoadFromResource(new Uri("avares://LoadingImages/Assets/abstract.jpg"));
    public Task<Bitmap?> ImageFromWebsite { get; } = ImageHelper.LoadFromWeb(new Uri("https://upload.wikimedia.org/wikipedia/commons/4/41/NewtonsPrincipia.jpg"));
}

您需要一个名为 ImageHelper 的辅助类,该类提供从资源和Web URL加载图像的方法。下面是如何实现这个类:

using System;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Media.Imaging;
using Avalonia.Platform;

namespace ImageExample.Helpers
{
    public static class ImageHelper
    {
        public static Bitmap LoadFromResource(Uri resourceUri)
        {
            return new Bitmap(AssetLoader.Open(resourceUri));
        }

        public static async Task<Bitmap?> LoadFromWeb(Uri url)
        {
            using var httpClient = new HttpClient();
            try
            {
                var response = await httpClient.GetAsync(url);
                response.EnsureSuccessStatusCode();
                var data = await response.Content.ReadAsByteArrayAsync();
                return new Bitmap(new MemoryStream(data));
            }
            catch (HttpRequestException ex)
            {
                Console.WriteLine($"An error occurred while downloading image '{url}' : {ex.Message}");
                return null;
            }
        }
    }
}

LoadFromResource 方法接受资源URI并使用Avalonia提供的 AssetLoader 类加载图像。LoadFromWeb 方法使用 HttpClient 类从Web URL加载图像。

然后,在您的视图中,您可以将这些图像来源绑定到 Image 控件:

<Grid ColumnDefinitions="*,*,*" RenderOptions.BitmapInterpolationMode="HighQuality">
    <Image Grid.Column="0" Source="avares://LoadingImages/Assets/abstract.jpg" MaxWidth="300" />
    <Image Grid.Column="1" Source="{Binding ImageFromBinding}" MaxWidth="300" />
    <Image Grid.Column="2" Source="{Binding ImageFromWebsite^}" MaxWidth="300" />
</Grid>

Image 控件的 Source 属性可以接受各种类型的图像来源,包括文件路径、URL或资源。请注意,对于异步图像来源,必须在绑定表达式后使用 ^ 字符,以告诉Avalonia这是一个异步绑定。

请确保本地图像文件路径准确,图像文件可访问,并且如果它是应用程序资源的一部分,则已正确包含在您的项目中。如果要绑定到Web图像,请确保URL可访问。

如何绑定 Can Execute

控件是否处于启用状态,可以响应用户交互并触发操作,是用户体验设计(UX)中"功能可见性"部分的重要原则。通过禁用无法运行的命令,可以增强用户的信心。例如,如果按钮或菜单项由于应用程序的当前状态而无法运行,则应呈现为不活动状态。

本示例假设您正在使用带有 ReactiveUI 框架的 MVVM 实现模式。这种(推荐的)方法在视图和视图模型之间提供了非常清晰的分离。

在此示例中,只有在消息不为空时,才能单击按钮。一旦输出显示出来,消息就会重置为空字符串,从而再次禁用按钮。

XAML

<StackPanel Margin="20">
  <TextBox Margin="0 5" Text="{Binding Message}"
           Watermark="Add a message to enable the button"/>
  <Button Command="{Binding ExampleCommand}">    
    Run the example
  </Button>
  <TextBlock Margin="0 5" Text="{Binding Output}" />
</StackPanel>

MainWindowViewModel.cs

namespace AvaloniaGuides.ViewModels
{
    public class MainWindowViewModel : ViewModelBase
    {
        private string _message = string.Empty;
        private string _output = "Waiting...";

        public string Message 
        { 
            get => _message; 
            set => this.RaiseAndSetIfChanged(ref _message, value); 
        }

        public string Output
        {
            get => _output;
            set => this.RaiseAndSetIfChanged(ref _output, value);
        }

        public ReactiveCommand<Unit, Unit> ExampleCommand { get; }

        public MainWindowViewModel()
        {
            var isValidObservable = this.WhenAnyValue(
                x => x.Message,
                x => !string.IsNullOrWhiteSpace(x));
            ExampleCommand = ReactiveCommand.Create(PerformAction, 
                                                    isValidObservable);
        }

        private void PerformAction()
        {
             Output = $"The action was called. {_message}";
             Message = String.Empty;
        }
    }
}

ViewModelBase.cs

using ReactiveUI;

namespace AvaloniaGuides.ViewModels
{
    public class ViewModelBase : ReactiveObject
    {
    }
}

在视图模型的构造函数中,使用两个参数创建了响应式命令。第一个参数是执行操作的私有方法。第二个参数是由视图模型(来自 ViewModelBase 类)的 ReactiveObjectWhenAnyValue 方法创建的可观察对象。

信息

在使用 "Avalonia MVVM Application" 解决方案模板时,将会向您的项目中添加 ViewModelBase 类。

在这里,WhenAnyValue 方法接受两个参数,第一个参数收集验证函数参数的值,第二个参数是返回布尔结果的验证函数。

信息

实际上,WhenAnyValue 方法有多个重载,最多可以接受 10 个不同的值获取器(用于验证函数参数),以及验证函数本身。

如何使用ReactiveUI绑定命令

本指南将向您展示如何将视图模型方法(执行操作)绑定到可以响应用户交互而启动操作的控件(例如按钮)。此绑定在XAML中使用Command属性进行定义,例如:

<Window xmlns="https://github.com/avaloniaui">
    ...
  <StackPanel Margin="20">
      <Button Command="{Binding ExampleCommand}">Run the example</Button>
  </StackPanel>

本指南假设您正在使用MVVM实现模式,并且您的视图模型基于 ReactiveUI 框架。

信息

要了解MVVM实现模式背后的概念,请参阅此处

如果您使用Avalonia MVVM Application 解决方案模板创建了应用程序,那么您的解决方案已经包含 ReactiveUI 框架包,您可以像这样引用它:

using ReactiveUI;

可以通过实现 ICommand 接口来执行操作的视图模型通过该接口来实现。ReactiveUI 框架提供了实现 ICommandReactiveCommand 类。

信息

有关 ICommand 接口定义的详细信息,请参阅此处

Command 属性数据绑定将通过其 ICommand.Execute 接口调用绑定的视图模型方法,在绑定的控件被激活时。在这个例子中:当按钮被点击时。

要创建带有 ReactiveCommand 的视图模型,请按照以下示例:

  • 在您的视图模型中,声明一个命令,例如:

    public ReactiveCommand<Unit, Unit> ExampleCommand { get; }

  • 在视图模型中创建一个私有方法来执行操作。

  • 初始化Reactive命令,传递执行操作的方法的名称。

您的视图模型代码现在将如下所示:

namespace AvaloniaGuides.ViewModels
{
    public class MainWindowViewModel 
    {
        public ReactiveCommand<Unit, Unit> ExampleCommand { get; }

        public MainWindowViewModel()
        {
            ExampleCommand = ReactiveCommand.Create(PerformAction);
        }
        private void PerformAction()
        {
            Debug.WriteLine("The action was called.");
        }
    }
}
  • 运行应用程序并监视调试输出。

当与Reactive命令绑定的控件被激活时(在本例中:当按钮被点击时),将通过Reactive命令调用执行操作的私有方法。

命令参数

通常需要向绑定到控件的Reactive命令传递参数。您可以在XAML中使用 CommandParameter 属性来实现这一点。例如:

<Window xmlns="https://github.com/avaloniaui">
   ...
   <StackPanel Margin="20">
      <Button Command="{Binding ExampleCommand}"
              CommandParameter="From the button">Run the example</Button>
   </StackPanel>
</Window>

现在,您必须修改视图模型,以便Reactive命令期望一个字符串参数,初始化期望一个字符串参数,执行操作的私有方法期望一个字符串参数。如下所示:

namespace AvaloniaGuides.ViewModels
{
    public class MainWindowViewModel 
    {
        public ReactiveCommand<string, Unit> ExampleCommand { get; }

        public MainWindowViewModel()
        {
            ExampleCommand = ReactiveCommand.Create<string>(PerformAction);
        }
        private void PerformAction(string msg)
        {
            Debug.WriteLine($"The action was called. {msg}");
        }
    }
}

请注意,CommandParameter 属性上不会执行任何类型转换,因此,如果您需要使用不是字符串的类型参数,则必须在XAML中定义该类型。您还需要使用扩展的XAML语法来定义参数。

例如,要传递整数参数:

<Window xmlns="https://github.com/avaloniaui"
        xmlns:sys="clr-namespace:System;assembly=mscorlib">
 ...   
    <Button Command="{Binding ExampleIntegerCommand}">
        <Button.CommandParameter>
            <sys:Int32>42</sys:Int32>
        </Button.CommandParameter>
        What is the answer?
    </Button>
</Window>

危险

如果参数定义缺失或类型不正确,将会出现错误。

相关推荐
咩咩觉主27 分钟前
C# VS的常用功能(一) 视图篇
visualstudio·c#·vs是宇宙第一好用的编辑器
翔云API1 小时前
C#文字识别API场景解析、表格识别提取
开发语言·c#
code_shenbing2 小时前
跨平台WPF框架Avalonia教程 十二
microsoft·ui·c#·wpf·应用程序
凌霜残雪2 小时前
WPF+MVVM案例实战、自定义控件和特效实现
wpf
code_shenbing3 小时前
跨平台WPF框架Avalonia教程 十三
microsoft·ui·c#·wpf·界面设计
CN.LG3 小时前
浅谈C#之内存管理
jvm·算法·c#
sukalot4 小时前
windows C#-LINQ查询
开发语言·c#·linq
Crazy Struggle7 小时前
100 款支持 .NET 多版本的强大 WPF 控件库
.net·wpf·控件库
追逐时光者9 小时前
.NET 9 中 LINQ 新增功能实操
后端·c#·.net