dotnet-workshop

科协暑培(.NET 部分)学习型工程

View on GitHub
返回

Guidance for Avalonia

训练目标

意外的训练目标:

背景介绍

这一节的背景应该没什么好介绍的了。上一节我们做了一个控制台的客户端,控制台的客户端简直易用性太差了!因此,我们要做一个好看的 GUI 的客户端!

Avalonia UI 是基于 .NET 的跨平台 GUI 框架,被称作「跨平台的 WPF」(后者是 .NET 官方提供的 Windows 平台的 GUI 框架,其首创的 MVVM 模式目前是一种网站前端极其常用的架构,尤其是著名的 Vue.js 就是基于 MVVM 模式的网站前端框架)。

现在,就让我们开始吧!

知识速递

为防止涉及到暑培的讲解死角,我们在这里先快速回顾一下本节任务用到的一些需要的知识,并对一些额外用到的知识进行补充。

工厂方法模式

我们在 01-basic 一节中学习了简单工厂模式。但简单工厂模式有一个弊端,即所有对象的创建逻辑均由同一个工厂类的同一个方法创建。如果不同的类的创建逻辑完全不同,且需要分布在不同的位置,由不同的角色进行创建,以至于无法写在一起,这时候简单工厂模式就无法应对需求了。我们需要 工厂方法模式(Factory Method Pattern)

我们在前一节 03-async-grpc 中介绍 ILogger 时就提到了工厂方法模式。在工厂方法模式当中,不同的类需要由不同的工厂来实例化对象,不同的工厂也有一个公共的工厂接口。如下例:

abstract class Product {}
class ConcreteProductA : Product {}
class ConcreteProductB : Product {}

interface IFactory {
    Product CreateProduct(string args);
}

class ConcreteFactoryA : IFactory {
    Product CreateProduct(string args) {
        return new ConcreteProductA(args);
    }
}

class ConcreteFactoryB : IFactory {
    Product CreateProduct(string args) {
        return new ConcreteProductB(args);
    }
}

在使用时,我们可以:

class FactoryMethodPatternDemo {
    private readonly IFactory _factory;
    
    public FactoryMethodPatternDemo(IFactory factory) {
        _factory = factory;
    }
    
    public void UseProduct(string args) {
        var product = _factory.CreateProduct(args);
        // 使用 product
    }
}

本节任务

任务描述

本节需要编写一个图形界面(GUI)客户端,来代替上一节编写的 RemoteCli。该客户端需要包含 RemoteCli 的全部功能。

(S4.1)Step 1:图形界面客户端的实现

本步骤的代码均位于 src/LogAnalyzerClient 目录中,代码结构如下:

LogAnalyzerClient
|
+---LogAnalyzerClient            # 该项目用于编写图形界面,我们的几乎全部图形界面均在此项目中编写
|   |   App.axaml                # 应用程序入口类(XAML 部分)
|   |   App.axaml.cs             # 应用程序入口类(C# 部分)
|   |   ViewLocator.cs
|   |
|   +---Services                 # gRPC 服务相关
|   |       IClientFactory.cs    # 用来定义 gRPC Client 工厂的接口
|   |       AppService.cs        # 用于注册 gRPC Client 工厂
|   |
|   +---Views                    # 各种界面(视图)
|   |       MainWindow.axaml     # 应用程序主窗口
|   |       MainWindow.axaml.cs
|   |       MainView.axaml       # 应用程序主窗口所显示的界面
|   |       MainView.axaml.cs
|   |
|   +---Styles                   # 存储样式
|   |       Controls.axaml       # 各种控件的样式
|   |
|   +---ViewModels               # 各种 ViewModel
|   |       ViewModelBase.cs     # 所有 ViewModel 的公共基类
|   |       MainViewModel.cs     # MainView 的 ViewModel
|   |
|   +---Models                   # 用于定义一些存储数据的数据结构
|   |       RemoteModels.cs      # 用于储存日志文件名和日志解析结果的数据结构
|   |
|   +---Dialogs                         # 各种临时弹出的对话框的定义
|   |       ConnectDialog.axaml         # 用于连接 Agent 的对话框
|   |       ConnectDialog.axaml.cs
|   |       MessageDialog.axaml         # 用于弹出临时消息的消息框
|   |       MessageDialog.axaml.cs
|   \---Helpers                         # 一些工具类
|           DialogHelper.cs             # 用于处理弹出对话框的相关逻辑,方便使用 Dialogs 中定义的对话框
|           ClientInternalException.cs  # 用于表示客户端内部发生的异常
|
\---LogAnalyzerClient.Desktop           # Desktop 平台(Windows、Linus、macOS)客户端
        Program.cs                      # Desktop 程序总入口
                                        # 1. 注册 gRPC Client 工厂,用于创建 gRPC 客户端
                                        # 2. 调用 LogAnalyzerClient 开启图形界面

首先介绍我们是如何创建 gRPC Client 并连接到 Agent 的。本 workshop 虽然仅在 Desktop 上运行(Windows、Linux、macOS),但为了软件的可扩展性,依然保留增加多平台的能力(如 Android、iOS、浏览器,等等)。由于 gRPC Client 在不同平台创建的方式不尽相同(有兴趣可以参看 开发札记 中 「关于 AvaloniaUI 的网页端和移动端」和「关于编写 WebAssembly 目标平台客户端的额外难题」两节),因此我们将创建 Desktop 端 gRPC Client 的逻辑放到专门生成 Desktop 平台程序的 LogAnalyzerClient.Desktop 项目中,而非用于全平台的 LogAnalyzerClient 项目中。

由于 gRPC Client 可能需要多次创建,例如我们的客户端支持切换远程 Agent 的地址,等等,我们需要做的是将创建 gRPC Client 的逻辑在 LogAnalyzerClient.Desktop 中写成一个通用的可以被随时调用的方法。因此,我们选取 工厂方法 来做到这一点。

我们在 LogAnalyzerClient/Services/IClientFactory.cs 中定义了工厂具有的接口:

using LogAnalyzerAgentServiceClient = LogAnalyzerAgentService.LogAnalyzerAgentServiceClient;

public interface IClientFactory {
    LogAnalyzerAgentServiceClient CreateClient(string address);
}

即输入远程 Agent 地址,返回一个 gRPC Client。同时,我们在 LogAnalyzerClient/Services/AppService.cs 定义了用于注册 Client 工厂的属性:

public static class AppService
{
    public static IClientFactory ClientFactory { get; set; } = new NullClientFactory();
}

LogAnalyzerClient.Desktop 项目中,我们于 Program.cs 中实现创建 Desktop 端的 gRPC Client 的工厂:

internal class ClientFactory : IClientFactory {
    public LogAnalyzerAgentServiceClient CreateClient(string address) {
        var channel = GrpcChannel.ForAddress(address);
        var client = new LogAnalyzerAgentServiceClient(channel);
        return client;
    }
}

我们在程序启动后且在 GUI 窗口启动前,在 Main 方法中将该工厂实例化并注册给 AppService

[STAThread]
public static void Main(string[] args) {
    AppService.ClientFactory = new ClientFactory();  // 注册工厂
    // 启动 GUI 窗口
}

LogAnalyzerClient 已经写好的部分界面。目前已经写好的界面中已经完成的功能如下:

阅读已经完成的实现,以其作为参考,你需要完善的剩余的功能。

[!IMPORTANT]

  • 图形界面程序的 UI 渲染以及对用户操作的响应等仅有单一的 UI 线程负责,因此为了防止 UI 线程阻塞导致程序看似卡死,你需要让所有的 gRPC 请求均为异步请求并进行 await!!! 具体原理参考上一节 03-async-grpc 中「知识速递」一节的讲解
  • 本项目作为图形界面程序, 绝对不应该因为用户输入的非法,或是一些内部错误而崩溃。因此,你应当做好输入检查以及异常处理,即使通过消息框向用户弹出出错信息

你需要完成的功能如下:

[!NOTE]

任务 4.1(T4.1)

你需要实现以上提到的全部功能。

当你完成你的实现后,请在 docs/04-avalonia 目录中新建一个名为 report.md 的文本文件,在其中介绍你实现的功能,并给出完整功能的截图,以及你程序的鲁棒性测试截图(各种非法输入的情况)。

提示: 你可以参考你上一节实现的 RemoteCli 的代码,将 RemoteCli 的 gRPC 调用相关代码移过来,并把你在 RemoteCli 中的输出错误信息修改为弹出消息框,你将会节省相当多的时间和精力。

[!IMPORTANT]

Agent 作为常驻的服务,一定要注意 绝对不应该 因为用户请求的非法,或是一些内部错误而崩溃,否则就会造成服务的不可用(我们通常说的网站挂掉了)。因此, 一定要做好 异常处理,注意捕获异常。可以参考 AgentSession 给出的样例。

可能用到的接口:

CommunityToolkit.Mvvm 是方便我们编写 MVVM 的工具。例如,我们要在 ViewModel 中创建一个公有属性,可以直接给私有字段加上 [ObservableProperty] 修饰:

using CommunityToolkit.Mvvm.ComponentModel;

partial class MainViewModel {
    [ObservableProperty]
    private string _greeting = "Welcome to Avalonia!";
}

此时将会自动为我们生成代码:

partial class MainViewModel {
    public partial string Greeting {
        get => _greeting;
        set => SetProperty(ref _greeting, value);
    }
}

其中 SetProperty 的内容大致意思如下:

bool SetProperty<T>(ref T field, T newValue, string? propertyName = null)
{
    OnPropertyChanging(propertyName);
    field = newValue;
    OnPropertyChanged(propertyName);
    return true;
}

当我们要创建一个响应函数时,只需要用 [ReplayCommand] 修饰即可:

using CommunityToolkit.Mvvm.Input;

partial class MainViewModel {
    [RelayCommand]
    private async Task PressAsync() {
        // ...
    }
}

此时也会为我们自动生成代码:

partial class MainViewModel {
    private RelayCommand? pressCommand;
    public IRelayCommand PressCommand => pressCommand ??= new RelayCommand(PressAsync);
}

问答题

问答题的提交方式是在 docs/04-avalonia 中的 report.md 文件中进行你对问题的解答。本节问答题均为开放题,回答个人的真实感受即可。

(Q4.1)

你认为,你在开发 GUI 应用程序,与你在以往开控制台应用程序的区别在哪里?GUI 应用程序的开发存在哪些额外的难点?存在哪些额外的复杂之处?你是否有通过编写 GUI 应用程序对异步 asyncawait 有了更进一步的理解?异步编程是否又给你带来的额外的困扰?说说你的看法。

(Q4.2)

本次作业中,你是否使用了 AI?根据你的使用情况,在以下 (Q4.2.a) (Q4.2.b) 两个问题中选择一题作答:

(Q4.2.a)

如果没有使用 AI,你花了大约多长时间完成了整个 04-avalonia?你是否借助了传统搜索引擎来完成本节?你认为本节的难度是是否显著高于编写普通的控制台应用程序的难度?你是否卡在某一处花费较长时间(如果有,是哪处)?

(Q4.2.b)

如果使用了 AI,你给予 AI 的提示词是什么?你对 AI 的使用是询问 AI 一些接口的用法、gRPC 的使用,或是在某处的写法,还是让 AI 帮你写一部分作业代码,又或是让 AI 给你讲解代码框架?AI 的解答是否出现过错误(如果有,是哪些)?你从 AI 那里是否得知了一些关于异步,或是 gRPC 等原本你不知道或是难以理解的知识?

关于本节的任务分值等信息,参看 tasks.md

拓展阅读

暂无,待补充

前进 / 后退