# 前言

之前提到过想在 llamap.cpp 基础上搞一个方便自己用的程序,但是又担心跨平台,毕竟现在公司使用 Mac,家里是 Windows 系统,就不好只想着单平台了,所以需要一个跨平台的框架。

开始想着先直接上控制台,找到两个控制台 UI 框架:

  • Terminal.Gui
  • spectre.console

其中 spectre.console 自带的一个 spectre.console.cli 做控制台应用还挺方便的,Terminal.Gui 没怎么研究过。
但是控制台一般只适合单线程显示信息,启用 llamap.cpp server 同时需要输出日志的话,就不够用了,虽然其中的 Terminal.Gui 似乎可以支持这种模式,但是感觉不大好看...

最后决定还是上正常的 GUI ,跨平台的 C# GUI 主要的似乎也就是 MAUI、Avalonia、Uno 了(开始还考虑过 Unity,但是 Unity 似乎太重了)

Avalonia 还有个 awesome-avalonia 项目

MAUI 基本上不考虑,虽然是微软官方支持,但是风评和支持似乎都不大好,加上官方一直有放弃老项目的历史

于是准备从 avalonia、uno 两者选择,结果好不容易一个双休(指 2024 年 05 月 19 日,恰逢五一后连续上班之后),周末的第一天,也就是周六基本上一天都是对比测试 Avalonia、Uno 两者。最后选了 Avalonia。

主要原因:

  • 启动:因为 uno 启动时有个控制台,而且有一闪而过的黑屏,感觉启动体感比 avalonia 还,这个信息官方文档好像也没看到说明
  • 发布:avalonia 的程序集明显要干净一些,而 uno 则带了很大一堆官方的其它程序集。可能这跟 uno 倾向于复用有关

于是继续研究了一番,整理了一些知识记录了一番。

# 样式选择器

在 Avalonia 中,样式选择器有点类似 CSS,可以根据类型、名字、类选择器来应用样式,不像 WPF,选择器是定义在外边而不是控件内部。

# 类型选择器

  • <Style Selector="Button">
  • 默认为类型的精确选择
  • 如果想应用于子类,则可使用 "is:(Button)"

# 名字选择器

  • <Style Selector="Button#customTargetName">
  • 应用于指定类型,且设置了指定名字的对象,即定义了属性 Name="customTargetName"

# Classes 选择器

  • <Style Selector="Button.Red">
  • 即生效于制定了 Classes="Red" 的对象
  • 可以在控制在拥有指定 Classes 属性后生效的样式

# MVVM

# 简介

在 MVVM 中按钮事件貌似是用的 Command,Click 事件和 Command 区别:

  • Click:界面本身中的指定方法,适用于简单的事件处理
  • Command:数据源中注册的指定方法,用于 MVVM 模式,适用于复杂的交互逻辑;Command 还可以通过 CommandParameter 属性传递参数

# 主要功能

  • ObservableObject:一个基类,实现了 INotifyPropertyChanged 接口,简化了属性更改通知。
  • RelayCommand 和 AsyncRelayCommand:用于处理同步和异步命令。
  • ObservableCollection:一个集合类型,当其内容发生变化时会通知视图。
  • 属性生成器:通过使用 ObservableProperty 特性,可以自动生成属性更改通知代码。

# Command

内置主要有三个 Command:

  • RelayCommand:最常见的同步处理,还带了 CanExcute 在接收事件时检测
  • RelayCommand:同步处理命令,带参数
  • AsyncRelayCommand:异步方法处理的命令,使用 AsyncRelayCommand 处理异步操作,可以避免阻塞 UI 线程。

在 Model (ObservableObject) 中,绑定对象变动自动通知 UI,需要使用 SetProperty 设置值
同里,可产生通知的列表为:ObservableCollection

# CommandParameter

  • 传递值,可传递固定值、动态值,或 xaml 选定值
  • 允许在 XAML 中指定绑定源相对于当前元素的位置。通常用于在控件层次结构中查找特定的祖先或同级元素,并从这些元素中获取数据进行绑定。
    • 查找特定类型的祖先元素:例如,从某个控件向上查找最近的 Window 或 UserControl。
    • 查找同级元素:例如,在 DataTemplate 中查找同级元素的数据上下文。
    • 查找自身:例如,绑定到自身的属性。

# 高级用法

# ObservableProperty

使用 ObservableProperty 特性可以自动生成属性和属性更改通知代码。

public partial class MainViewModel : ObservableObject
{
    [ObservableProperty]
    private string _title;
    [ObservableProperty]
    private bool _isBusy;
}

注:如果需要配合 RelayCommand CanExcute,可额外使用 NotifyCanExecuteChangeFor 特性

# [NotifyCanExecuteChangedFor(nameof(Title))]

调用命令的 NotifyCanExecuteChanged 方法,使其重新验证是否可以在当前上下文中执行命令,但是暂时没试出来干啥的

  • 更新:似乎需要命令设置过 CanExecute 方法才有效,否则不会生成代码

# [NotifyPropertyChangedFor(nameof(Title))]

一个特性,通知某个属性发生改变时,同时可以指定通知另外一个属性发生变化

# ObservableRecipient

  • 继承自 ObservableObject,增加了消息接收功能。
  • 可以与 IMessenger 一起使用,简化组件之间的通信。

# Messenger

  • 实现了松耦合的消息传递机制,简化组件之间的通信。
  • 一般实例为:WeakReferenceMessenger.Default

# RelayCommand 特性

  • 在方法上添加该特性,自动生成 RelayCommand 代码
  • 参数 CanExcute,可以作为特性参数,例如:[RelayCommand (CanExcute=nameof (方法))]
    • 异步方法甚至会在执行时,自动将 CanExcute 方法设置为 false
    • Command.IsRunning 代表是否处于运行中
  • 参数 IncludeCancelCommand
    • 当设置为 True 时,它会自动生成一个带有所有必要样板的取消命令。唯一的要求是用 ICommand 属性修饰的方法需要有一个 CancellationToken 参数,该参数是取消异步任务所必需的

参考文档

  • https://learn.microsoft.com/en-us/dotnet/communitytoolkit/mvvm/observablerecipient

# 数据绑定

  • Mode=OneWay
    • 单向绑定:数据从源(通常是 ViewModel 中的属性)流向目标(通常是视图中的控件),但不会反向更新。
    • 典型场景:用于只读数据展示,例如标签、文本块等不需要用户输入的控件。
  • Mode=TwoWay
    • 双向绑定:数据在源和目标之间双向流动。当源属性改变时,目标会更新;当目标(例如用户输入)改变时,源属性也会更新。
    • 典型场景:用于需要用户输入的控件,如文本框、复选框等。
  • OneTime
    • 数据仅在绑定初始化时从源流向目标,不会再进行后续更新。
    • 适用于静态或初始加载的数据。
  • OneWayToSource
    • 数据从目标流向源,但不会反向更新。
    • 适用于需要将用户输入的数据传递回 ViewModel,但不需要 ViewModel 更新视图的场景。
  • Default
    • 取决于目标属性的默认绑定行为。如果目标属性没有特定的默认行为,则与 OneWay 类似。

# NavMenu(Ursa)

<u:NavMenu Name="menu" ItemsSource="{Binding Menus.MenuItems}"
                   ExpandWidth="300"
                   CommandBinding="{Binding ActivateCommand}"
                   IsHorizontalCollapsed="True"
                   HeaderBinding="{Bindin}"
                   IconBinding="{Binding MenuHeader}">
  • HeaderBinding="{Binding Header}":表示绑定数据结构中的指定名字字段
  • HeaderBinding="{Binding}":绑定当前自定义模板
    • 这个绑定后,自定义模板中传入数据类型为 MenuItems 中的一项
    • 注:应该是因为数据源设置的 ItemsSource="{Binding Menus.MenuItems}"
  • HeaderBinding 如果不设置,会直接显示类的全名

# 图标

普通的图片类型可以直接放 Assets 目录,然后通过 <Image Source="../Assets/avalonia-logo.ico" /> 类似代码使用。
如果是 StreamGeometry 类型的,则可以新建 Style 样式的 axaml :

<Style>
    <Style.Resources>
        <StreamGeometry x:Key="Sticker"></StreamGeometry>
    </Style.Resources>
</Style>

然后可通过 Data="{StaticResource Sticker}" 在其它地方引用。

注 1:代码中可通过 Application.Current!.TryFindResource("Sticker", out var res) 查找指定 key 的资源
注 2:StreamGeometry 也可以通过代码 StreamGeometry.Parse 动态创建


Fluent 样式的 Icon:

  • https://avaloniaui.github.io/icons.html