13 changed files with 800 additions and 37 deletions
@ -0,0 +1,75 @@ |
|||
using Microsoft.Extensions.Configuration; |
|||
using Microsoft.Extensions.Hosting; |
|||
using Serilog; |
|||
using Serilog.Events; |
|||
using System; |
|||
using System.IO; |
|||
|
|||
namespace AuroraDesk.Infrastructure.Extensions; |
|||
|
|||
/// <summary>
|
|||
/// HostBuilder 扩展方法(基础设施层)
|
|||
/// 用于配置日志记录功能
|
|||
/// </summary>
|
|||
public static class HostBuilderExtensions |
|||
{ |
|||
/// <summary>
|
|||
/// 配置文件日志记录
|
|||
/// </summary>
|
|||
/// <param name="hostBuilder">HostBuilder</param>
|
|||
/// <returns>HostBuilder</returns>
|
|||
/// <remarks>
|
|||
/// 配置优先级说明:
|
|||
/// 1. 代码中的配置(WriteTo.Console、WriteTo.File)优先级最高,会覆盖配置文件中的相同配置
|
|||
/// 2. serilog.json 文件中的 "Serilog" 节点配置次之,会被 ReadFrom.Configuration 读取
|
|||
///
|
|||
/// 注意:Serilog 配置已提取到独立的 serilog.json 文件中,便于管理和维护
|
|||
/// </remarks>
|
|||
public static IHostBuilder ConfigureFileLogging(this IHostBuilder hostBuilder) |
|||
{ |
|||
return hostBuilder.UseSerilog((context, services, configuration) => |
|||
{ |
|||
var environment = context.HostingEnvironment; |
|||
|
|||
// 配置日志目录
|
|||
var logDirectory = Path.Combine( |
|||
Directory.GetCurrentDirectory(), |
|||
"logs" |
|||
); |
|||
|
|||
// 确保日志目录存在
|
|||
if (!Directory.Exists(logDirectory)) |
|||
{ |
|||
Directory.CreateDirectory(logDirectory); |
|||
} |
|||
|
|||
// 配置 Serilog
|
|||
// 注意:ReadFrom.Configuration 会读取 serilog.json 文件中的 "Serilog" 节点
|
|||
// 然后代码中的配置会在此基础上添加或覆盖
|
|||
configuration |
|||
.ReadFrom.Configuration(context.Configuration) // 读取 serilog.json 中的 "Serilog" 节点
|
|||
.ReadFrom.Services(services) |
|||
.Enrich.FromLogContext() |
|||
.Enrich.WithProperty("Environment", environment.EnvironmentName) |
|||
// 以下代码配置会覆盖配置文件中相同类型的配置(如果存在)
|
|||
.WriteTo.Console( |
|||
outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] {Message:lj}{NewLine}{Exception}", |
|||
restrictedToMinimumLevel: LogEventLevel.Information |
|||
) |
|||
.WriteTo.File( |
|||
path: Path.Combine(logDirectory, "auroradesk-.log"), |
|||
rollingInterval: RollingInterval.Day, |
|||
retainedFileCountLimit: 30, |
|||
outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}", |
|||
restrictedToMinimumLevel: LogEventLevel.Debug |
|||
) |
|||
.WriteTo.File( |
|||
path: Path.Combine(logDirectory, "auroradesk-error-.log"), |
|||
rollingInterval: RollingInterval.Day, |
|||
retainedFileCountLimit: 60, |
|||
outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}", |
|||
restrictedToMinimumLevel: LogEventLevel.Error |
|||
); |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,82 @@ |
|||
using Avalonia.Media; |
|||
using HeroIconsAvalonia.Enums; |
|||
using ReactiveUI; |
|||
|
|||
namespace AuroraDesk.Presentation.ViewModels; |
|||
|
|||
/// <summary>
|
|||
/// 关闭确认对话框的 ViewModel
|
|||
/// </summary>
|
|||
public class CloseConfirmViewModel : ReactiveObject |
|||
{ |
|||
private string _title = "确认关闭"; |
|||
private string _message = "确定要退出程序吗?"; |
|||
private IconType _icon = IconType.QuestionMarkCircle; |
|||
private IBrush _accentBrush = new SolidColorBrush(Color.FromRgb(59, 130, 246)); |
|||
private string _primaryButtonText = "确认"; |
|||
private string _secondaryButtonText = "取消"; |
|||
|
|||
/// <summary>
|
|||
/// 对话框标题
|
|||
/// </summary>
|
|||
public string Title |
|||
{ |
|||
get => _title; |
|||
set => this.RaiseAndSetIfChanged(ref _title, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 对话框消息
|
|||
/// </summary>
|
|||
public string Message |
|||
{ |
|||
get => _message; |
|||
set => this.RaiseAndSetIfChanged(ref _message, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 对话框图标
|
|||
/// </summary>
|
|||
public IconType Icon |
|||
{ |
|||
get => _icon; |
|||
set => this.RaiseAndSetIfChanged(ref _icon, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 对话框强调色
|
|||
/// </summary>
|
|||
public IBrush AccentBrush |
|||
{ |
|||
get => _accentBrush; |
|||
set => this.RaiseAndSetIfChanged(ref _accentBrush, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 主操作按钮文本
|
|||
/// </summary>
|
|||
public string PrimaryButtonText |
|||
{ |
|||
get => _primaryButtonText; |
|||
set => this.RaiseAndSetIfChanged(ref _primaryButtonText, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 次操作按钮文本
|
|||
/// </summary>
|
|||
public string SecondaryButtonText |
|||
{ |
|||
get => _secondaryButtonText; |
|||
set |
|||
{ |
|||
this.RaiseAndSetIfChanged(ref _secondaryButtonText, value); |
|||
this.RaisePropertyChanged(nameof(HasSecondaryButton)); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 是否显示次操作按钮
|
|||
/// </summary>
|
|||
public bool HasSecondaryButton => !string.IsNullOrWhiteSpace(_secondaryButtonText); |
|||
} |
|||
|
|||
@ -0,0 +1,73 @@ |
|||
<Window 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" |
|||
xmlns:vm="using:AuroraDesk.Presentation.ViewModels" |
|||
xmlns:heroicons="clr-namespace:HeroIconsAvalonia.Controls;assembly=HeroIconsAvalonia" |
|||
mc:Ignorable="d" d:DesignWidth="380" d:DesignHeight="200" |
|||
x:Class="AuroraDesk.Presentation.Views.Dialogs.CloseConfirmDialog" |
|||
x:DataType="vm:CloseConfirmViewModel" |
|||
Title="确认关闭" |
|||
Width="380" |
|||
SizeToContent="Height" |
|||
WindowStartupLocation="CenterOwner" |
|||
ShowInTaskbar="False" |
|||
CanResize="False" |
|||
SystemDecorations="None" |
|||
Background="Transparent"> |
|||
|
|||
<Design.DataContext> |
|||
<vm:CloseConfirmViewModel /> |
|||
</Design.DataContext> |
|||
|
|||
<Border Background="{StaticResource BackgroundWhite}" |
|||
CornerRadius="16" |
|||
Padding="24" |
|||
Width="380"> |
|||
<StackPanel Spacing="18"> |
|||
<Grid ColumnDefinitions="Auto,*" ColumnSpacing="12"> |
|||
<Border Grid.Column="0" |
|||
Width="40" |
|||
Height="40" |
|||
CornerRadius="20" |
|||
Background="{Binding AccentBrush}" |
|||
VerticalAlignment="Center"> |
|||
<heroicons:HeroIcon Type="{Binding Icon}" |
|||
Width="22" |
|||
Height="22" |
|||
Foreground="White" |
|||
HorizontalAlignment="Center" |
|||
VerticalAlignment="Center"/> |
|||
</Border> |
|||
<StackPanel Grid.Column="1" Spacing="6"> |
|||
<TextBlock Text="{Binding Title}" |
|||
FontSize="18" |
|||
FontWeight="SemiBold" |
|||
Foreground="{StaticResource TextPrimary}"/> |
|||
<TextBlock Text="{Binding Message}" |
|||
TextWrapping="Wrap" |
|||
FontSize="14" |
|||
Foreground="{StaticResource SecondaryGrayDark}" |
|||
MaxWidth="260"/> |
|||
</StackPanel> |
|||
</Grid> |
|||
|
|||
<StackPanel Orientation="Horizontal" |
|||
HorizontalAlignment="Right" |
|||
Spacing="10"> |
|||
<Button Content="{Binding SecondaryButtonText}" |
|||
MinWidth="96" |
|||
Name="CancelButton" |
|||
IsVisible="{Binding HasSecondaryButton}"/> |
|||
<Button Content="{Binding PrimaryButtonText}" |
|||
MinWidth="96" |
|||
Name="ConfirmButton" |
|||
Background="{Binding AccentBrush}" |
|||
Foreground="White" |
|||
BorderThickness="0" |
|||
CornerRadius="8"/> |
|||
</StackPanel> |
|||
</StackPanel> |
|||
</Border> |
|||
</Window> |
|||
|
|||
@ -0,0 +1,46 @@ |
|||
using Avalonia.Controls; |
|||
using Avalonia.Markup.Xaml; |
|||
using Avalonia.ReactiveUI; |
|||
using AuroraDesk.Presentation.ViewModels; |
|||
using ReactiveUI; |
|||
|
|||
namespace AuroraDesk.Presentation.Views.Dialogs; |
|||
|
|||
/// <summary>
|
|||
/// 关闭确认对话框
|
|||
/// </summary>
|
|||
public partial class CloseConfirmDialog : ReactiveWindow<CloseConfirmViewModel> |
|||
{ |
|||
public CloseConfirmDialog() |
|||
{ |
|||
InitializeComponent(); |
|||
SetupButtons(); |
|||
} |
|||
|
|||
public CloseConfirmDialog(CloseConfirmViewModel viewModel) : this() |
|||
{ |
|||
ViewModel = viewModel; |
|||
} |
|||
|
|||
private void InitializeComponent() |
|||
{ |
|||
AvaloniaXamlLoader.Load(this); |
|||
} |
|||
|
|||
private void SetupButtons() |
|||
{ |
|||
var confirmButton = this.FindControl<Button>("ConfirmButton"); |
|||
var cancelButton = this.FindControl<Button>("CancelButton"); |
|||
|
|||
if (confirmButton != null) |
|||
{ |
|||
confirmButton.Click += (s, e) => Close(true); |
|||
} |
|||
|
|||
if (cancelButton != null) |
|||
{ |
|||
cancelButton.Click += (s, e) => Close(false); |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,25 @@ |
|||
{ |
|||
"Serilog": { |
|||
"Using": [ "Serilog.Sinks.File", "Serilog.Sinks.Console" ], |
|||
"MinimumLevel": { |
|||
"Default": "Information", |
|||
"Override": { |
|||
"Microsoft": "Warning", |
|||
"Microsoft.Hosting.Lifetime": "Information", |
|||
"AuroraDesk": "Debug" |
|||
} |
|||
}, |
|||
"WriteTo": [ |
|||
{ |
|||
"Name": "Console", |
|||
"Args": { |
|||
"outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] {Message:lj}{NewLine}{Exception}" |
|||
} |
|||
} |
|||
], |
|||
"Enrich": [ "FromLogContext", "WithThreadId", "WithMachineName" ], |
|||
"Properties": { |
|||
"Application": "AuroraDesk" |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,215 @@ |
|||
# MainWindow 关闭确认框实现分析 |
|||
|
|||
## 当前实现方式 |
|||
|
|||
当前实现将 `DialogHost` 包裹整个 `MainWindow.axaml`,结构如下: |
|||
|
|||
```xml |
|||
<reactive:ReactiveWindow> |
|||
<dialogHost:DialogHost IsOpen="{Binding IsCloseConfirmDialogOpen}"> |
|||
<dialogHost:DialogHost.DialogContent> |
|||
<!-- 对话框内容 --> |
|||
</dialogHost:DialogHost.DialogContent> |
|||
|
|||
<Grid> |
|||
<!-- 窗口主要内容 --> |
|||
</Grid> |
|||
</dialogHost:DialogHost> |
|||
</reactive:ReactiveWindow> |
|||
``` |
|||
|
|||
## 主要缺点分析 |
|||
|
|||
### 1. **架构设计问题** |
|||
|
|||
#### ❌ 职责混淆 |
|||
- **问题**:`MainWindowViewModel` 既负责窗口导航和标签页管理,又负责关闭确认对话框的 UI 状态 |
|||
- **影响**:违反了单一职责原则,ViewModel 职责过多 |
|||
- **示例**:`MainWindowViewModel` 中包含了 8 个关闭确认框相关的属性 |
|||
|
|||
#### ❌ 窗口级别污染 |
|||
- **问题**:DialogHost 包裹整个窗口,窗口的所有内容都成为 DialogHost 的子元素 |
|||
- **影响**:窗口结构被对话框控件"绑架",不利于后续扩展 |
|||
|
|||
### 2. **可扩展性问题** |
|||
|
|||
#### ❌ 无法添加多个对话框 |
|||
- **问题**:一个 DialogHost 只能管理一个对话框内容 |
|||
- **影响**:如果未来需要在窗口级别添加其他对话框(如设置对话框、关于对话框等),需要创建多个 DialogHost,结构会变得复杂 |
|||
|
|||
#### ❌ 对话框内容硬编码 |
|||
- **问题**:对话框的 UI 结构直接写在 MainWindow.axaml 中 |
|||
- **影响**:无法在其他地方复用相同的对话框组件 |
|||
|
|||
### 3. **可维护性问题** |
|||
|
|||
#### ❌ XAML 文件过长 |
|||
- **问题**:MainWindow.axaml 现在包含窗口布局(400+ 行)和对话框定义(50+ 行) |
|||
- **影响**:文件可读性下降,维护成本增加 |
|||
|
|||
#### ❌ 对话框逻辑分散 |
|||
- **问题**:对话框的状态管理在 ViewModel,UI 定义在 View,事件处理在 Code-Behind |
|||
- **影响**:逻辑分散,难以追踪和维护 |
|||
|
|||
### 4. **代码质量问题** |
|||
|
|||
#### ❌ 事件处理方式不够优雅 |
|||
```csharp |
|||
// 当前实现 |
|||
ViewModel.CloseWindowRequested += OnCloseWindowRequested; |
|||
``` |
|||
- **问题**:使用传统的事件机制,而不是 ReactiveUI 的响应式编程模式 |
|||
- **影响**:与项目中其他地方的 ReactiveCommand 风格不一致 |
|||
|
|||
#### ❌ 标志位管理 |
|||
```csharp |
|||
private bool _isClosingConfirmed = false; |
|||
``` |
|||
- **问题**:使用标志位来区分用户确认关闭和普通关闭 |
|||
- **影响**:状态管理不够清晰,容易出现逻辑错误 |
|||
|
|||
### 5. **性能问题** |
|||
|
|||
#### ❌ 不必要的属性绑定 |
|||
- **问题**:关闭确认框的所有属性(标题、消息、图标、颜色等)都在 ViewModel 中定义,即使对话框很少显示 |
|||
- **影响**:增加了 ViewModel 的内存占用和属性通知开销 |
|||
|
|||
#### ❌ DialogHost 始终存在 |
|||
- **问题**:DialogHost 包裹整个窗口,即使对话框不显示时也存在 |
|||
- **影响**:虽然影响很小,但理论上可以优化 |
|||
|
|||
### 6. **测试性问题** |
|||
|
|||
#### ❌ 难以单元测试 |
|||
- **问题**:对话框逻辑与窗口逻辑耦合,无法单独测试对话框功能 |
|||
- **影响**:测试覆盖困难 |
|||
|
|||
### 7. **与项目其他部分不一致** |
|||
|
|||
#### ❌ 风格不一致 |
|||
- **问题**:`DialogHostPageView` 中 DialogHost 包裹的是页面内容,而 `MainWindow` 中包裹的是窗口内容 |
|||
- **影响**:虽然都能工作,但结构不一致,可能造成理解困难 |
|||
|
|||
## 改进建议 |
|||
|
|||
### 方案 1:使用独立的对话框服务(推荐) |
|||
|
|||
**优点**: |
|||
- 职责分离清晰 |
|||
- 可复用性强 |
|||
- 易于测试和维护 |
|||
|
|||
**实现**: |
|||
```csharp |
|||
// 创建 IDialogService 接口 |
|||
public interface IDialogService |
|||
{ |
|||
Task<bool> ShowConfirmDialogAsync(string title, string message); |
|||
} |
|||
|
|||
// 在 ViewModel 中使用 |
|||
var result = await _dialogService.ShowConfirmDialogAsync("确认关闭", "确定要退出程序吗?"); |
|||
if (result) Close(); |
|||
``` |
|||
|
|||
### 方案 2:使用 UserControl 封装对话框 |
|||
|
|||
**优点**: |
|||
- 对话框内容可复用 |
|||
- XAML 文件更简洁 |
|||
|
|||
**实现**: |
|||
```xml |
|||
<!-- 创建 CloseConfirmDialog.axaml --> |
|||
<UserControl> |
|||
<Border> |
|||
<!-- 对话框内容 --> |
|||
</Border> |
|||
</UserControl> |
|||
|
|||
<!-- MainWindow.axaml --> |
|||
<dialogHost:DialogHost> |
|||
<dialogHost:DialogHost.DialogContent> |
|||
<local:CloseConfirmDialog /> |
|||
</dialogHost:DialogHost.DialogContent> |
|||
<!-- 窗口内容 --> |
|||
</dialogHost:DialogHost> |
|||
``` |
|||
|
|||
### 方案 3:使用 ReactiveUI 的交互(推荐用于简单场景) |
|||
|
|||
**优点**: |
|||
- 符合 ReactiveUI 最佳实践 |
|||
- 代码更简洁 |
|||
|
|||
**实现**: |
|||
```csharp |
|||
// ViewModel |
|||
public Interaction<CloseConfirmViewModel, bool> CloseConfirmInteraction { get; } |
|||
|
|||
// View |
|||
this.WhenActivated(disposables => |
|||
{ |
|||
ViewModel.CloseConfirmInteraction |
|||
.RegisterHandler(async interaction => |
|||
{ |
|||
var dialog = new CloseConfirmDialog { DataContext = interaction.Input }; |
|||
var result = await dialog.ShowDialog<bool>(this); |
|||
interaction.SetOutput(result); |
|||
}) |
|||
.DisposeWith(disposables); |
|||
}); |
|||
``` |
|||
|
|||
### 方案 4:将对话框移到窗口顶层(最小改动) |
|||
|
|||
**优点**: |
|||
- 改动最小 |
|||
- 保持当前架构 |
|||
|
|||
**实现**: |
|||
```xml |
|||
<!-- 将 DialogHost 放在 Grid 内部,而不是包裹整个 Grid --> |
|||
<Grid> |
|||
<!-- 窗口内容 --> |
|||
|
|||
<!-- 对话框放在最上层 --> |
|||
<dialogHost:DialogHost IsOpen="{Binding IsCloseConfirmDialogOpen}" |
|||
Panel.ZIndex="9999"> |
|||
<!-- 对话框内容 --> |
|||
</dialogHost:DialogHost> |
|||
</Grid> |
|||
``` |
|||
|
|||
## 对比总结 |
|||
|
|||
| 维度 | 当前实现 | 方案1(服务) | 方案2(UserControl) | 方案3(Interaction) | 方案4(顶层) | |
|||
|------|---------|-------------|---------------------|---------------------|--------------| |
|||
| 职责分离 | ❌ 差 | ✅ 优秀 | ⚠️ 一般 | ✅ 优秀 | ⚠️ 一般 | |
|||
| 可扩展性 | ❌ 差 | ✅ 优秀 | ⚠️ 一般 | ⚠️ 一般 | ⚠️ 一般 | |
|||
| 可维护性 | ❌ 差 | ✅ 优秀 | ✅ 良好 | ✅ 良好 | ⚠️ 一般 | |
|||
| 代码质量 | ⚠️ 一般 | ✅ 优秀 | ✅ 良好 | ✅ 优秀 | ⚠️ 一般 | |
|||
| 实现难度 | ✅ 简单 | ⚠️ 中等 | ✅ 简单 | ⚠️ 中等 | ✅ 简单 | |
|||
| 与项目一致性 | ⚠️ 一般 | ✅ 优秀 | ✅ 良好 | ✅ 优秀 | ✅ 良好 | |
|||
|
|||
## 推荐方案 |
|||
|
|||
**短期(快速改进)**:使用方案 4,将 DialogHost 移到 Grid 内部,减少对现有架构的影响 |
|||
|
|||
**长期(最佳实践)**:采用方案 1 或方案 3,创建独立的对话框服务或使用 ReactiveUI Interaction,实现更好的职责分离和代码复用 |
|||
|
|||
## 当前实现的优点 |
|||
|
|||
虽然存在上述缺点,但当前实现也有其优点: |
|||
|
|||
1. ✅ **快速实现**:改动最小,快速实现功能 |
|||
2. ✅ **功能完整**:能够正常工作,满足需求 |
|||
3. ✅ **视觉一致**:与 DialogHostPageView 的对话框样式保持一致 |
|||
4. ✅ **简单直观**:代码结构相对简单,容易理解 |
|||
|
|||
## 结论 |
|||
|
|||
当前实现虽然可以工作,但在架构设计、可扩展性和可维护性方面存在明显不足。建议根据项目的发展阶段和团队规模,选择合适的改进方案。 |
|||
|
|||
对于小型项目或快速原型,当前实现可以接受;但对于需要长期维护和扩展的项目,建议采用更优雅的架构方案。 |
|||
|
|||
Loading…
Reference in new issue