Avalonia 学习笔记05. Dependency Injection Factory Pattern(依赖注入与工厂模式)
视频链接:https://youtu.be/22Rj_sMyv88?si=czG5YX_6tDsWMMp8
本节课核心目标:
- 引入依赖注入 (Dependency Injection, DI) 来管理项目中各个类(特别是ViewModel)的创建和生命周期,避免在代码中到处使用
new关键字,实现解耦。 - 解决一个常见问题:我们不希望在程序一启动时就创建所有页面的ViewModel并一直保存在内存中,而是希望在用户导航到某个页面时才创建它。
- 为了解决上述问题,引入工厂模式 (Factory Pattern),创建一个专门负责“按需生产”页面ViewModel的工厂。
视频作者说了,依赖注入和工厂模式会让人很困惑,但是为了接下来的课程还是要引入,并且会慢慢得到回报。
我现在也不太懂,不过还是要慢慢学习这些额外的知识。
5.0 核心概念深入讲解
在分析具体代码修改之前,需要先理解本节课所应用的几个核心编程概念。这些概念是构建大型、可维护软件的工程基础。
5.0.1 依赖注入 (Dependency Injection, DI)
-
比喻: 厨师需要番茄来做菜。如果厨师需要自己去种番茄,那么他的工作就和“种番茄”这个具体行为绑定了。DI 相当于建立了一个完善的采购和物流系统,厨师只需要在菜谱上写明“需要5个番茄”,采购员(DI容器)就会自动把番茄送到他手上。
-
描述:
依赖注入是一种软件设计模式,其核心思想是控制反转(Inversion of Control, IoC)。一个对象(客户端)不应负责创建它所依赖的对象(服务),而是通过外部来提供(注入)这些依赖。DI 是实现 IoC 的一种具体技术。 -
图示
-
没有DI(紧密耦合):
+-------------------+ 创建 +-------------------+
| MainViewModel | *-------------> | HomePageViewModel |
|-------------------| +-------------------+
| new HomePageVM() |
+-------------------+
*MainViewModel 直接依赖并创建 HomePageViewModel,关系固定。
- 使用 DI (松散耦合):
+-------------------+ 请求依赖 +-------------------+
| MainViewModel | <----------------- | DI容器 |
|-------------------| 注入依赖 | (ServiceProvider) |
| Ctor(HomePageVM) | -----------------> |-------------------|
+-------------------+ | 知道如何创建 |
| HomePageViewModel |
+-------------------+
*MainViewModel 只声明需要什么,DI容器 负责创建并提供。
- 基本用法(三步曲):
-
注册服务 (Register): 在应用程序的启动入口(如
App.axaml.cs),使用ServiceCollection来“注册”服务和它们的生命周期。AddSingleton<T>(): 单例模式。服务在第一次被请求时创建,之后所有请求都返回同一个实例。适用于全局配置、主视图模型等。AddTransient<T>(): 瞬态模式。每次请求服务时,都会创建一个全新的实例。适用于轻量级、无状态的服务,如本课中的页面ViewModel。AddScoped<T>(): 作用域模式。在同一个作用域(如一次Web请求)内,所有请求返回同一个实例。在桌面应用中不常用。
// 示例: var services = new ServiceCollection(); services.AddSingleton<IMainWindowViewModel, MainWindowViewModel>(); // 注册单例 services.AddTransient<IPageViewModel, HomePageViewModel>(); // 注册瞬态 -
声明依赖 (Declare): 在需要使用服务的类中,通过构造函数参数来声明依赖。这是最推荐的方式,称为“构造函数注入”。
public class MainWindowViewModel { private readonly IPageViewModel _initialPage; // 通过构造函数声明,我需要一个 IPageViewModel public MainWindowViewModel(IPageViewModel initialPage) { _initialPage = initialPage; } } -
解析服务 (Resolve): DI容器会自动处理服务的创建和注入。我们通常只在程序的“根”部(Composition Root,如
App.axaml.cs)手动解析一次服务来启动整个应用。var provider = services.BuildServiceProvider(); var mainViewModel = provider.GetRequiredService<IMainWindowViewModel>();
- 本章节用法详解:
- 在
App.axaml.cs中,我们注册了MainViewModel为Singleton,因为它代表了整个主窗口的状态,全局唯一。 - 所有页面ViewModel(如
HomePageViewModel)被注册为Transient,因为我们希望每次导航到一个页面时,都得到一个全新的、干净的实例,并且在离开页面后,旧的实例可以被垃圾回收,从而节省内存。 - 由于页面 ViewModel 被注册为 Transient(瞬态),并且 MainViewModel 中只持有对
_currentPage的引用,当用户导航到新页面时,_currentPage 会被赋予一个新的 ViewModel 实例。旧的那个实例如果没有其他地方引用它,就会在下一次垃圾回收(GC)时被自动清理,从而实现了内存的有效管理。
- 在
5.0.2 工厂模式 (Factory Pattern)
-
比喻: 去快餐店点餐。顾客(客户端)不需要知道汉堡(产品)的具体制作流程,只需要到柜台(工厂)说出自己想要的汉堡名称即可。
-
描述:
工厂模式是一种创造型设计模式,它提供了一种封装对象创建过程的方法。客户端与具体的产品创建过程解耦,只与工厂接口交互。 -
图示:
+---------------+ 请求页面 +---------------+ 请求创建 +-------------------+
| MainViewModel | *-------------> | PageFactory | *-----------------> | DI容器 |
+---------------+ +---------------+ +-------------------+
| | 创建
| 使用注入的 "Func" 委托 |
| v
+---------------------------------> +-------------------+
| HomePageViewModel |
+-------------------+
*MainViewModel 向 PageFactory "点餐"。
*PageFactory 不自己“做菜”,而是使用DI容器给它的一个特殊工具(Func委托),让DI容器这个“中央厨房”来制作。
-
基本用法:
通常会创建一个工厂类,其中包含一个或多个根据输入参数创建不同类型产品的方法。// 简化的工厂示例 public class SimplePageFactory { public IPageViewModel CreatePage(string pageName) { switch (pageName) { case "Home": return new HomePageViewModel(); case "Settings": return new SettingsPageViewModel(); default: throw new ArgumentException("Invalid page name"); } } } ``` 这个简单工厂的问题在于,它内部使用了 `new`,与我们DI的目标相悖。 -
本章节用法详解:
我们创建了PageFactory类,但它自身并不使用new来创建ViewModel。相反,它依赖于DI容器注入的一个“生产方法”(即Func<>委托)。这使得PageFactory成为了一个连接客户端(MainViewModel)和DI容器(ServiceProvider)的桥梁。MainViewModel通过工厂实现了“按需创建”,而工厂通过注入的委托,利用了DI容器的能力来完成实际的创建工作,保证了所有ViewModel的创建依然在DI的掌控之中。
5.0.3 委托 (Delegate)、Func<> 和 Action<>
-
比喻: 一个指向“紧急联系人电话号码”的便签。便签本身不是人,但它记录了如何联系到那个人。这个便签的格式是固定的(只能写电话号码)。
-
描述:
委托是C#中的一种引用类型,它封装了对具有特定方法签名(即返回值类型和参数列表)的方法的引用。它允许将方法作为参数传递,是实现回调机制和事件处理的基础。 -
图示:
+------------------+ 指向 +----------------------+
| 委托对象 | *----------------> | 方法() |
| (例如 "op") | |----------------------|
+------------------+ | ...一些逻辑... |
| +----------------------+
|
| 可作为参数传递
v
+-------------------------+
| 另一个方法(Delegate d) |
+-------------------------+
-
Func<> 和 Action<>:
Func<> 和 Action<> 是.NET提供的内置泛型委托,让我们无需为每种签名都手动声明委托类型。
- Action<>: 封装了没有返回值 (void) 的方法。
- Func<>: 封装了有返回值的方法,最后一个泛型参数始终代表返回值类型。
-
基本用法:
- 声明委托类型:
public delegate int MathOperation(int a, int b); - 创建委托实例并指向方法:
public int Add(int x, int y) => x + y; MathOperation op = Add; - 调用委托:
int result = op(3, 5); // result is 8 // 或使用Invoke() int result2 = op.Invoke(3, 5);
- 声明委托类型:
-
Func<>和Action<>:
Func<>和Action<>是.NET提供的内置泛型委托,让我们无需为每种签名都手动声明委托类型。Action<>: 封装了没有返回值 (void) 的方法。Func<>: 封装了有返回值的方法,最后一个泛型参数始终代表返回值类型。
-
本章节用法详解:
我们使用了Func<ApplicationPageNames, PageViewModel>。这个委托精确地描述了我们工厂需要的能力:一个接收ApplicationPageNames枚举作为输入,并能返回一个PageViewModel对象作为输出的方法。通过在DI容器中注册这个Func,我们实际上是注册了一个“符合该描述的生产方法”,并让PageFactory来使用它。
5.0.4 Lambda表达式和闭包 (Closure)
-
Lambda表达式 (
=>):- 描述: Lambda表达式是一种用于创建匿名函数的简洁语法。它使得我们可以直接在需要委托实例的地方以内联的方式编写函数体。
- 基本用法:
// (input-parameters) => {statement-or-expression-body} Func<int, int> square = x => x * x; // 表达式体 Action<string> print = message => // 语句体 { Console.WriteLine(message); };
-
闭包 (Closure):
- 比喻: 一个带着“魔法背包”的旅行者。旅行者是一个函数,背包是他的创建环境。背包里装着他家乡的物品(外部变量)。即使他离家远行,依然能使用背包里的东西。
- 描述: 闭包是一个函数以及其创建时所在的词法作用域(Lexical Scope)的组合。该函数可以“捕获”(Capture)并访问其外部作用域中的变量,即使外部作用域的生命周期已经结束。
- 基本用法:
public Func<int> CreateCounter() { int count = 0; // 这个变量将被闭包捕获 return () => { count++; return count; }; } var counter1 = CreateCounter(); Console.WriteLine(counter1()); // 输出 1 Console.WriteLine(counter1()); // 输出 2counter1这个Func实例"记住"了它自己私有的count变量。
-
本章节用法详解 (
x => name => name switch):
这是闭包在本章节中的核心应用,也是最复杂的部分。这种方式被称为“委托工厂”(Delegate Factory)。它的一个巨大优势是,PageFactory 类本身对 IServiceProvider (DI 容器) 完全无知。它只依赖于一个 Func 接口,这使得 PageFactory 的可测试性变得极高,因为它不与任何具体的 DI 容器实现耦合。 -
我们把它分解来看:
collection.AddSingleton<...>( x => ... ): 注册一个单例服务。DI容器在创建这个服务时,会执行括号内的Lambda,并把容器自身 (IServiceProvider) 作为参数x传进去。x => name => ...: 这是一个返回另一个函数的函数。- 外层函数
x => ...: 它的任务是接收IServiceProvider(即x),并创建一个内层函数。 - 内层函数
name => ...: 这个函数就是我们最终要注入到工厂里的那个Func的实体。
- 外层函数
- 闭包的形成: 内层函数
name => ...在被创建时,它所在的“家乡”环境中有变量x。于是它通过闭包机制,把x“打包”带走了。 - 最终效果: 我们成功地创建并注册了一个
Func<ApplicationPageNames, PageViewModel>委托实例。这个实例在未来的任何时候被调用时,都能访问到被它捕获的IServiceProvider(x),从而可以调用x.GetRequiredService<T>()来从DI容器中动态地、按需地创建任何已注册的服务。这完美地解决了工厂需要与DI容器交互的问题,同时又没有让工厂直接依赖于IServiceProvider。
- 图示 (x => name => ... 详解):
第一步:定义环境 (在 App.axaml.cs 中)
+-------------------------------------------------+
| 外部作用域 (AddSingleton 调用) |
| |
| 变量 'x' 在此存在 (= IServiceProvider) |
| |
| +-------------------------------------------+ |
| | 内部函数 (name => ...) 在此创建 | |
| | | |
| | * 它看到并“捕获”了 'x' | |
| | +---------------------+ | |
| | | 闭包的“魔法背包” | | |
| | |---------------------| | |
| | | - 对 'x' 的引用 | | |
| | +---------------------+ | |
| +-------------------------------------------+ |
+-------------------------------------------------+
第二步:函数被传递和调用 (在 PageFactory 中)
+-------------------------------------------------+
| PageFactory (一个完全不同的作用域) |
| |
| 注入的 Func<> (我们的内部函数) 在此 |
| |
| 当被调用时: |
| 1. 它打开它的“魔法背包”(闭包) |
| 2. 它找到并使用对 'x' 的引用 |
| 3. 它调用 x.GetRequiredService<T>() |
| |
+-------------------------------------------------+
5.1 App.axaml.cs
修改。
这是本节课最核心的修改。App.axaml.cs 是整个应用程序的入口点,因此我们将在这里配置我们的“依赖注入容器”。可以把它想象成一个中央“服务台”,我们在这里注册所有程序需要的“服务”(在这里主要是各种ViewModel),并定义如何创建它们。当程序其他地方需要某个服务时,就向这个服务台申请,而不是自己动手创建。
using System;
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using AvaloniaApplication2.Data;
using AvaloniaApplication2.Factories;
using AvaloniaApplication2.ViewModels;
using Microsoft.Extensions.DependencyInjection;
namespace AvaloniaApplication2;
public partial class App : Application
{
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
// 1. 初始化服务集合 (ServiceCollection)
// ServiceCollection 是一个DI容器的配置清单,用于注册服务及其生命周期。
var collection = new ServiceCollection();
// 2. 注册服务
// AddSingleton: 注册为单例生命周期。该服务在第一次被请求时创建,后续所有请求都返回同一个实例。
collection.AddSingleton<MainViewModel>();
// AddTransient: 注册为瞬态生命周期。每次请求该服务时,都会创建一个全新的实例。
collection.AddTransient<ActionsPageViewModel>();
collection.AddTransient<HomePageViewModel>();
collection.AddTransient<MacrosPageViewModel>();
collection.AddTransient<ProcessPageViewModel>();
collection.AddTransient<ReporterPageViewModel>();
collection.AddTransient<HistoryPageViewModel>();
collection.AddTransient<SettingsPageViewModel>();
// 3. 注册工厂函数
// 这里注册了一个Func委托,用于按需创建PageViewModel。这是一个典型的闭包应用。
// - 外层 x => ...: 参数'x'是DI容器在构建此委托时自动注入的IServiceProvider实例。
// - 内层 name => ...: 这是返回的实际函数,它捕获了变量'x'。
// - name switch: C# 8.0的switch表达式,根据传入的'name',使用捕获的'x'来解析并返回对应的ViewModel服务。
collection.AddSingleton<Func<ApplicationPageNames, PageViewModel>>(x => name => name switch
{
ApplicationPageNames.Home => x.GetRequiredService<HomePageViewModel>(),
ApplicationPageNames.Process => x.GetRequiredService<ProcessPageViewModel>(),
ApplicationPageNames.Macros => x.GetRequiredService<MacrosPageViewModel>(),
ApplicationPageNames.Actions => x.GetRequiredService<ActionsPageViewModel>(),
ApplicationPageNames.Reporter => x.GetRequiredService<ReporterPageViewModel>(),
ApplicationPageNames.History => x.GetRequiredService<HistoryPageViewModel>(),
ApplicationPageNames.Settings => x.GetRequiredService<SettingsPageViewModel>(),
_ => throw new InvalidOperationException()
});
// 4. 注册PageFactory
// PageFactory本身也被注册为单例。DI容器在创建它时,会自动解析并注入其构造函数所需的Func<>委托。
collection.AddSingleton<PageFactory>();
// 5. 构建服务提供程序 (ServiceProvider)
// 调用BuildServiceProvider()方法,根据ServiceCollection中的配置,创建一个ServiceProvider实例。
// ServiceProvider是实际用于解析和提供服务的对象。
var services = collection.BuildServiceProvider();
// 6. 启动主窗口并注入DataContext
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
/*
通常在 Avalonia 项目中,MainView.axaml.cs 的构造函数里会有一行 InitializeComponent()。当 MainView 被创建时,它的 DataContext 还没有被设置。
如果 MainView.axaml 中有任何控件试图在 InitializeComponent() 期间绑定到DataContext 的属性,可能会产生绑定错误(虽然通常程序不会崩溃)。
更稳妥的做法是先创建 MainView,再设置 DataContext,也就是下面正在做的事情。
*/
desktop.MainWindow = new MainView
{
// 从DI容器中获取MainViewModel实例,并设置为MainWindow的数据上下文。
// 此处不再使用`new MainViewModel()`,实现了控制反转。
DataContext = services.GetService<MainViewModel>()
};
}
base.OnFrameworkInitializationCompleted();
}
}
5.2 ViewModels\HomePageViewModel.cs和ViewModels\ProcessPageViewModel.cs等
修改,其他的MacrosPageViewMode.cs、HistoryPageViewModel.cs、ActionsPageViewModel.cs、ReporterPageViewModel.cs、SettingsPageViewModel.cs参照下面的修改即可,这里就不重复了。
之前,我们无法从一个ViewModel实例得知它对应的是哪个页面。为了解决这个问题,我们创建了一个新的基类PageViewModel(见5.7节),它有一个PageName属性。 现在,我们让所有的页面ViewModel都继承自PageViewModel,并在各自的构造函数中,明确地设置自己的PageName。这就像给每个ViewModel发了一个“身份证”,让我们可以随时识别它。
HomePageViewModel.cs
using AvaloniaApplication2.Data;
namespace AvaloniaApplication2.ViewModels;
// 继承自 PageViewModel,而不是 ViewModelBase
public partial class HomePageViewModel : PageViewModel
{
public HomePageViewModel()
{
// 在构造函数中,设置自己的身份标识
PageName = ApplicationPageNames.Home;
}
}
ProcessPageViewModel.cs
using AvaloniaApplication2.Data;
namespace AvaloniaApplication2.ViewModels;
public partial class ProcessPageViewModel : PageViewModel
{
public ProcessPageViewModel()
{
PageName = ApplicationPageNames.Process;
}
}
5.3 Views\ProcessPageView.axaml
修改,这是一个连锁反应。因为我们在ProcessPageViewModel.cs中删除了之前用于测试的Test属性,所以绑定到这个属性的UI元素(比如一个Label)必须被修改或删除,否则程序在编译时会因为找不到绑定的属性而报错。这提醒我们,ViewModel和View是紧密关联的。
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
Foreground="White"
xmlns:vm="clr-namespace:AvaloniaApplication2.ViewModels"
x:DataType="vm:ProcessPageViewModel"
x:Class="AvaloniaApplication2.Views.ProcessPageView">
<Design.DataContext><vm:ProcessPageViewModel></vm:ProcessPageViewModel></Design.DataContext>
Welcome to Process!
</UserControl>
5.4 ViewModels\MainViewModel.cs
修改。
MainViewModel是这次重构的最大受益者。它变得更加“干净”和“职责单一”。
- 它不再负责创建任何页面的ViewModel。
- 它不再持有每个页面的ViewModel实例。
- 它只需要知道一个东西:PageFactory。当需要页面时,它就向工厂“下单”。
- 通过检查CurrentPage.PageName,它就能知道当前是哪个页面,从而更新侧边栏按钮的选中状态。
using Avalonia.Svg.Skia;
using AvaloniaApplication2.Data;
using AvaloniaApplication2.Factories;
using AvaloniaApplication2.Views;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Extensions.DependencyInjection;
namespace AvaloniaApplication2.ViewModels;
public partial class MainViewModel : ViewModelBase
{
// 之前这里有一大堆各个页面的ViewModel属性,现在全部删除了。
// 只保留一个对PageFactory的引用。
private PageFactory _pageFactory;
[ObservableProperty]
private bool _sideMenuExpanded = true;
// 当前页面的类型从 ViewModelBase 变成了 PageViewModel,
// 这样我们就可以访问到它的 PageName 属性。
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(HomePageIsActive))]
[NotifyPropertyChangedFor(nameof(ProcessPageIsActive))]
[NotifyPropertyChangedFor(nameof(ActionsPageIsActive))]
[NotifyPropertyChangedFor(nameof(MacrosPageIsActive))]
[NotifyPropertyChangedFor(nameof(ReporterPageIsActive))]
[NotifyPropertyChangedFor(nameof(HistoryPageIsActive))]
[NotifyPropertyChangedFor(nameof(SettingsPageIsActive))]
private PageViewModel _currentPage;
// 判断按钮是否选中的逻辑,从比较ViewModel实例,
// 变成了比较当前页面的PageName枚举值。
public bool HomePageIsActive => CurrentPage.PageName == ApplicationPageNames.Home;
public bool ProcessPageIsActive => CurrentPage.PageName == ApplicationPageNames.Process;
public bool ActionsPageIsActive => CurrentPage.PageName == ApplicationPageNames.Actions;
public bool MacrosPageIsActive => CurrentPage.PageName == ApplicationPageNames.Macros;
public bool ReporterPageIsActive => CurrentPage.PageName == ApplicationPageNames.Reporter;
public bool HistoryPageIsActive => CurrentPage.PageName == ApplicationPageNames.History;
public bool SettingsPageIsActive => CurrentPage.PageName == ApplicationPageNames.Settings;
// 构造函数注入:
// MainViewModel不再自己new任何东西,它在构造函数中声明“我需要一个PageFactory”。
// DI容器在创建MainViewModel时,会自动把之前注册好的PageFactory实例传给它。
public MainViewModel(PageFactory pageFactory)
{
_pageFactory = pageFactory;
GoToHome();
}
[RelayCommand]
private void SideMenuResize()
{
SideMenuExpanded = !SideMenuExpanded;
}
// 导航方法:
// 不再是 CurrentPage = _homePageViewModel;
// 而是向工厂请求一个新的页面ViewModel实例。
[RelayCommand]
private void GoToHome()
{
CurrentPage = _pageFactory.GetPageViewModel(ApplicationPageNames.Home);
}
[RelayCommand]
private void GoToProcess()
{
CurrentPage = _pageFactory.GetPageViewModel(ApplicationPageNames.Process);
}
[RelayCommand]
private void GoToMacros()
{
CurrentPage = _pageFactory.GetPageViewModel(ApplicationPageNames.Macros);
}
[RelayCommand]
private void GoToActions()
{
CurrentPage = _pageFactory.GetPageViewModel(ApplicationPageNames.Actions);
}
[RelayCommand]
private void GoToReporter()
{
CurrentPage = _pageFactory.GetPageViewModel(ApplicationPageNames.Reporter);
}
[RelayCommand]
private void GoToHistory()
{
CurrentPage = _pageFactory.GetPageViewModel(ApplicationPageNames.History);
}
[RelayCommand]
private void GoToSettings()
{
CurrentPage = _pageFactory.GetPageViewModel(ApplicationPageNames.Settings);
}
}
5.5 Data\ApplicationPageNames.cs
新建Data文件夹和ApplicationPageNames.cs文件。
这是一个新建的枚举(enum)类型。它的作用是为我们应用程序中所有的页面定义一组固定的、有意义的名称。
为什么要用枚举而不是字符串?
- 类型安全: 防止在代码中出现拼写错误。如果你写"Hmoe",编译器不会报错,但程序会出错。如果你写ApplicationPageNames.Hmoe,编译器会立刻提示你没有这个成员。
- 代码清晰: GoToPage(ApplicationPageNames.Home)比GoToPage("Home")意图更明确。
- 智能提示: 在编写代码时,IDE会提示所有可用的页面名称,非常方便。
namespace AvaloniaApplication2.Data;
// 定义一个枚举来表示所有的页面名称
public enum ApplicationPageNames
{
Unknown, // 一个默认值,用于处理未知状态
Home,
Process,
Actions,
Macros,
Reporter,
History,
Settings
}
5.6 ViewModels\PageViewModel.cs
新建。
这是一个新建的基类(Base Class)。它的目的是为所有“页面级别”的ViewModel提供一个共同的规范。任何继承自PageViewModel的类,都将自动拥有一个PageName属性。
这使得我们可以在MainViewModel中用一个_currentPage字段来持有任何类型的页面,并且都能安全地访问它的PageName属性来判断它到底是哪个页面。
using AvaloniaApplication2.Data;
using CommunityToolkit.Mvvm.ComponentModel;
namespace AvaloniaApplication2.ViewModels;
// 这个类继承自ViewModelBase,所以它也具备INotifyPropertyChanged的功能
public partial class PageViewModel : ViewModelBase
{
// 定义了一个所有页面ViewModel都将拥有的属性:PageName
[ObservableProperty]
private ApplicationPageNames _pageName;
}
5.7 Factories\PageFactory.cs
新建Factories文件夹和PageFactory.cs文件
这就是我们的工厂(Factory)。它的职责非常单一:根据给定的页面名称,生产出对应的页面ViewModel。
它自己并不知道具体如何创建ViewModel,而是通过构造函数注入了一个“生产方法”(也就是我们在App.axaml.cs里注册的那个复杂的Func)。当外部调用GetPageViewModel时,它只是简单地调用(Invoke)这个被注入的方法来完成生产任务。
using System;
using AvaloniaApplication2.Data;
using AvaloniaApplication2.ViewModels;
namespace AvaloniaApplication2.Factories;
// 使用了C# 新版本的主构造函数(Primary Constructor)语法,非常简洁,这是 C# 12 (.NET 8) 中正式引入的语言特性。
// `(Func<ApplicationPageNames, PageViewModel> factory)` 直接定义了构造函数和私有字段。
public class PageFactory(Func<ApplicationPageNames, PageViewModel> factory)
{
// 当调用此方法时,它会执行(Invoke)构造函数传入的factory委托。
// `factory.Invoke(pageNames)` 最终会执行我们在App.axaml.cs中定义的switch表达式。
public PageViewModel GetPageViewModel(ApplicationPageNames pageNames) => factory.Invoke(pageNames);
}
// 上面的是简洁的写法,和下面等价。
/*
public class PageFactory
{
// 这是一个私有只读字段,用来存储“生产方法”
private readonly Func<ApplicationPageNames, PageViewModel> _pageFactory;
// 通过构造函数,DI容器会将那个Func注入进来
public PageFactory(Func<ApplicationPageNames, PageViewModel> factory)
{
_pageFactory = factory;
}
// 调用方法,实际上是调用存储的那个Func
public PageViewModel GetPageViewModel(ApplicationPageNames pageNames)
{
return _pageFactory.Invoke(pageNames);
}
}
*/
5.8 当前目录结构
去除/bin、/obj,让显示简洁。
│ App.axaml
│ App.axaml.cs
│ app.manifest
│ AvaloniaApplication2.csproj
│ Program.cs
│ ViewLocator.cs
│
├─Assets
│ ├─Fonts
│ │ AkkoPro-Bold.ttf
│ │ AkkoPro-Regular.ttf
│ │ Phosphor-Fill.ttf
│ │ Phosphor.ttf
│ │
│ └─Images
│ icon.svg
│ logo.svg
│
├─Data
│ ApplicationPageNames.cs
│
├─Factories
│ PageFactory.cs
│
├─Styles
│ AppDefaultStyles.axaml
│
├─ViewModels
│ ActionsPageViewModel.cs
│ HistoryPageViewModel.cs
│ HomePageViewModel.cs
│ MacrosPageViewModel.cs
│ MainViewModel.cs
│ PageViewModel.cs
│ ProcessPageViewModel.cs
│ ReporterPageViewModel.cs
│ SettingsPageViewModel.cs
│ ViewModelBase.cs
│
└─Views
ActionsPageView.axaml
ActionsPageView.axaml.cs
HistoryPageView.axaml
HistoryPageView.axaml.cs
HomePageView.axaml
HomePageView.axaml.cs
MacrosPageView.axaml
MacrosPageView.axaml.cs
MainView.axaml
MainView.axaml.cs
ProcessPageView.axaml
ProcessPageView.axaml.cs
ReporterPageView.axaml
ReporterPageView.axaml.cs
SettingsPageView.axaml
SettingsPageView.axaml.cs
浙公网安备 33010602011771号