Browse Source

关闭弹窗

refactor/namespace-and-layering
root 1 month ago
parent
commit
8e5c1615fe
  1. 11
      AuroraDesk.Infrastructure/AuroraDesk.Infrastructure.csproj
  2. 75
      AuroraDesk.Infrastructure/Extensions/HostBuilderExtensions.cs
  3. 82
      AuroraDesk.Presentation/ViewModels/CloseConfirmViewModel.cs
  4. 18
      AuroraDesk.Presentation/ViewModels/MainWindowViewModel.cs
  5. 73
      AuroraDesk.Presentation/Views/Dialogs/CloseConfirmDialog.axaml
  6. 46
      AuroraDesk.Presentation/Views/Dialogs/CloseConfirmDialog.axaml.cs
  7. 74
      AuroraDesk.Presentation/Views/MainWindow.axaml.cs
  8. 9
      AuroraDesk/App.axaml.cs
  9. 24
      AuroraDesk/AuroraDesk.csproj
  10. 12
      AuroraDesk/appsettings.json
  11. 173
      AuroraDesk/modify.md
  12. 25
      AuroraDesk/serilog.json
  13. 215
      MainWindow关闭确认框实现分析.md

11
AuroraDesk.Infrastructure/AuroraDesk.Infrastructure.csproj

@ -11,11 +11,16 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.10" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.10" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.10" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.10" />
<PackageReference Include="ReactiveUI" Version="20.1.1" />
<PackageReference Include="HeroIcons.Avalonia" Version="1.0.4" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="9.0.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="9.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.1.1" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
</ItemGroup>
</Project>

75
AuroraDesk.Infrastructure/Extensions/HostBuilderExtensions.cs

@ -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
);
});
}
}

82
AuroraDesk.Presentation/ViewModels/CloseConfirmViewModel.cs

@ -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);
}

18
AuroraDesk.Presentation/ViewModels/MainWindowViewModel.cs

@ -91,6 +91,20 @@ public class MainWindowViewModel : ReactiveObject
public ReactiveCommand<TabItem, Unit> CloseTabCommand { get; }
public ReactiveCommand<TabItem, Unit> SelectTabCommand { get; }
// 关闭确认对话框 Interaction
public Interaction<CloseConfirmViewModel, bool> CloseConfirmInteraction { get; }
/// <summary>
/// 请求关闭确认
/// </summary>
public async System.Threading.Tasks.Task<bool> RequestCloseConfirmAsync()
{
var viewModel = new CloseConfirmViewModel();
var result = await CloseConfirmInteraction.Handle(viewModel);
_logger?.LogInformation("用户选择: {Result}", result ? "确认关闭" : "取消关闭");
return result;
}
public MainWindowViewModel(
IScreen screen,
INavigationService navigationService,
@ -121,6 +135,9 @@ public class MainWindowViewModel : ReactiveObject
// 创建选择标签页命令(委托给 TabManagementService)
SelectTabCommand = ReactiveCommand.Create<TabItem>(tab => SelectTab(tab));
// 创建关闭确认对话框 Interaction
CloseConfirmInteraction = new Interaction<CloseConfirmViewModel, bool>();
// 监听导航项选择变化
this.WhenAnyValue(x => x.SelectedNavigationItem)
.Where(item => item != null)
@ -358,6 +375,7 @@ public class MainWindowViewModel : ReactiveObject
_logger?.LogInformation("关闭标签页: {Title}", tab.Title);
}
/// <summary>
/// 释放资源
/// </summary>

73
AuroraDesk.Presentation/Views/Dialogs/CloseConfirmDialog.axaml

@ -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>

46
AuroraDesk.Presentation/Views/Dialogs/CloseConfirmDialog.axaml.cs

@ -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);
}
}
}

74
AuroraDesk.Presentation/Views/MainWindow.axaml.cs

@ -6,8 +6,10 @@ using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
using Microsoft.Extensions.Logging;
using AuroraDesk.Presentation.ViewModels;
using AuroraDesk.Presentation.Views.Dialogs;
using ReactiveUI;
using System.Reactive.Disposables;
using System.Threading.Tasks;
namespace AuroraDesk.Presentation.Views;
@ -17,6 +19,7 @@ namespace AuroraDesk.Presentation.Views;
public partial class MainWindow : ReactiveWindow<MainWindowViewModel>, IActivatableView
{
private readonly ILogger<MainWindow>? _logger;
private bool _isClosingConfirmed = false;
/// <summary>
/// 无参构造函数,用于 XAML 设计器
@ -43,13 +46,24 @@ public partial class MainWindow : ReactiveWindow<MainWindowViewModel>, IActivata
_logger?.LogInformation("MainWindow 已创建,ViewModel 已设置");
// 处理窗口关闭事件,显示确认框
this.Closing += OnWindowClosing;
// 使用 WhenActivated 管理订阅
this.WhenActivated(disposables =>
{
// 可以在这里添加窗口级别的订阅,如果需要的话
// 例如:this.WhenAnyValue(x => x.ViewModel.Title)
// .Subscribe(title => ...)
// .DisposeWith(disposables);
// 注册关闭确认对话框 Interaction Handler
if (ViewModel != null)
{
ViewModel.CloseConfirmInteraction
.RegisterHandler(async interaction =>
{
var dialog = new CloseConfirmDialog(interaction.Input);
var result = await dialog.ShowDialog<bool>(this);
interaction.SetOutput(result);
})
.DisposeWith(disposables);
}
});
}
@ -96,11 +110,29 @@ public partial class MainWindow : ReactiveWindow<MainWindowViewModel>, IActivata
};
}
// 关闭按钮
// 关闭按钮 - 显示确认框而不是直接关闭
var closeButton = this.FindControl<Button>("CloseButton");
if (closeButton != null)
{
closeButton.Click += (sender, e) => Close();
if (ViewModel != null)
{
// 关闭按钮点击时显示确认框
closeButton.Click += async (sender, e) =>
{
var confirmed = await ViewModel.RequestCloseConfirmAsync();
if (confirmed)
{
// 设置标志,表示用户已确认关闭
_isClosingConfirmed = true;
Close();
}
};
}
else
{
// 设计器模式或无 ViewModel 时,直接关闭(用于设计器预览)
closeButton.Click += (sender, e) => Close();
}
}
}
@ -114,5 +146,35 @@ public partial class MainWindow : ReactiveWindow<MainWindowViewModel>, IActivata
BeginMoveDrag(e);
}
}
/// <summary>
/// 处理窗口关闭事件,显示确认框
/// </summary>
private async void OnWindowClosing(object? sender, WindowClosingEventArgs e)
{
// 如果用户已经通过关闭按钮确认关闭,直接允许关闭
if (_isClosingConfirmed)
{
return;
}
if (ViewModel != null)
{
// 取消默认关闭行为
e.Cancel = true;
// 显示确认框
var confirmed = await ViewModel.RequestCloseConfirmAsync();
if (confirmed)
{
// 用户确认关闭,设置标志并允许窗口关闭
_isClosingConfirmed = true;
e.Cancel = false;
// 不需要手动调用 Close(),系统会自动关闭窗口
}
// 如果用户取消,e.Cancel = true 已设置,窗口不会关闭
}
// 如果没有 ViewModel(设计器模式),允许直接关闭
}
}

9
AuroraDesk/App.axaml.cs

@ -62,15 +62,10 @@ public partial class App : Avalonia.Application
config.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{context.HostingEnvironment.EnvironmentName}.json", optional: true)
.AddJsonFile("serilog.json", optional: false, reloadOnChange: true) // 加载独立的 Serilog 配置文件
.AddEnvironmentVariables();
})
.ConfigureLogging((context, logging) =>
{
logging.ClearProviders();
logging.AddConfiguration(context.Configuration.GetSection("Logging"));
logging.AddConsole();
logging.AddDebug();
})
.ConfigureFileLogging() // 使用 Infrastructure 层的扩展方法配置文件日志
.ConfigureServices((context, services) =>
{
services.Configure<AppSettings>(context.Configuration.GetSection("AppSettings"));

24
AuroraDesk/AuroraDesk.csproj

@ -36,21 +36,27 @@
<PackageReference Include="AvaloniaEdit" Version="0.10.12" />
<PackageReference Include="AvaloniaEdit.TextMate" Version="11.3.0" />
<!-- HostBuilder 相关包 -->
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.CommandLine" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.10" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.10" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.10" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.10" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="9.0.10" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.10" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.10" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.10" />
<PackageReference Include="Microsoft.Extensions.Configuration.CommandLine" Version="9.0.10" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="9.0.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="9.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
</ItemGroup>
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="serilog.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>

12
AuroraDesk/appsettings.json

@ -1,16 +1,4 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"MyAvaloniaApp": "Debug"
},
"Console": {
"IncludeScopes": true,
"TimestampFormat": "yyyy-MM-dd HH:mm:ss "
}
},
"AppSettings": {
"ApplicationName": "My Avalonia App",
"Version": "1.0.0",

173
AuroraDesk/modify.md

@ -5280,3 +5280,176 @@ var title = _resourceService?.GetString("NavDashboard") ?? "仪表板";
- ✅ 减少了 UI 线程的负担
- ✅ 用户体验显著提升
### 添加文件日志记录功能(遵循整洁架构)
- **日期**: 2025�?月`n- **修改内容**: 添加文件日志记录功能,使�?Serilog 作为日志框架,遵循整洁架构原则`n- **修改文件**:
- AuroraDesk.Infrastructure/Extensions/HostBuilderExtensions.cs (新建)
- AuroraDesk/App.axaml.cs (更新)
- AuroraDesk/appsettings.json (更新)
- AuroraDesk/AuroraDesk.csproj (更新包版�?
- AuroraDesk.Infrastructure/AuroraDesk.Infrastructure.csproj (添加 Serilog 相关包和更新包版�?
- **主要变更**:
- �?**�?Infrastructure 层创建日志配置扩展方�?*:`n - 新建 HostBuilderExtensions.cs 文件,包�?ConfigureFileLogging 扩展方法
- 日志配置封装�?Infrastructure 层,符合整洁架构原则
- 使用 Serilog 作为日志框架,支持控制台和文件输出`n - 配置文件日志:`n - 普通日志文件:logs/auroradesk-.log(按天滚动,保留30天)
- 错误日志文件:`logs/auroradesk-error-.log(按天滚动,保留60天)
- �?**更新 App.axaml.cs**:`n - 使用 Infrastructure 层的 ConfigureFileLogging 扩展方法替代原来�?ConfigureLogging 配置
- 简化了组合根代码,符合依赖倒置原则
- �?**更新 appsettings.json**:`n - 添加 Serilog 配置节点
- 配置日志级别、输出模板等
- 更新日志命名空间�?MyAvaloniaApp 改为 AuroraDesk`n - �?**添加必要�?NuGet �?*:`n - 主项目:Serilog.Extensions.Hosting、`Serilog.Sinks.File、`Serilog.Settings.Configuration(已在之前添加)
- Infrastructure 项目:`Serilog.Extensions.Hosting、`Serilog.Sinks.File、`Serilog.Settings.Configuration、`Serilog.Sinks.Console、`Microsoft.Extensions.Hosting`n - �?**统一包版�?*:`n - 将所�?Microsoft.Extensions.* 包版本从 9.0.0 升级�?9.0.10,解决版本冲突问题`n- **架构原则**:`n - �?**日志配置属于基础设施�?*:日志记录的实现细节被封装在 Infrastructure 层`n - �?**组合根负责组�?*:主项目�?App.axaml.cs 作为组合根,调用 Infrastructure 层的扩展方法
- �?**依赖倒置**:主项目依赖�?Infrastructure 层的抽象(扩展方法),而不是具体实现细节`n - �?**单一职责**:日志配置的职责明确归属�?Infrastructure 层`n- **日志功能特�?*:`n - �?支持控制台日志输出(开发环境)
- �?支持文件日志输出(生产环境)
- �?日志按天滚动,自动管理文件`n - �?分离普通日志和错误日志
- �?支持配置文件和代码双重配置`n - �?日志级别可配置(Debug、Information、Warning、Error)`n- **日志文件位置**:`n - 普通日志:logs/auroradesk-YYYYMMDD.log(按天滚动,保留30天)
- 错误日志:`logs/auroradesk-error-YYYYMMDD.log(按天滚动,保留60天)
- **优势**:`n - �?遵循整洁架构原则,职责清晰`n - �?日志配置集中管理,易于维护`n - �?支持多种日志输出方式
- �?自动管理日志文件,避免磁盘空间占用过多`n - �?代码简洁,易于扩展
### 日志配置优先级说明`n- **日期**: 2025�?月`n- **修改内容**: 添加日志配置优先级说明和注释
- **修改文件**:
- AuroraDesk.Infrastructure/Extensions/HostBuilderExtensions.cs (添加详细注释)
- **配置优先级说�?*:`n - �?**优先级从高到�?*:`n 1. **代码中的配置**(WriteTo.Console、WriteTo.File�? 优先级最高,会覆盖配置文件中的相同配置`n 2. **appsettings.json 中的 Serilog 节点** - 会被 ReadFrom.Configuration 读取,但会被代码配置覆盖
3. **appsettings.json 中的 Logging 节点** - 不会生效,因为已经清除了所�?Microsoft.Extensions.Logging 提供程序
- �?**Serilog vs Logging 的区�?*:`n - Serilog 节点:Serilog 框架的配置,会被实际使用
- Logging 节点:Microsoft.Extensions.Logging 的配置,目前不会生效(因为只使用 Serilog)`n - �?**当前配置行为**:`n - ReadFrom.Configuration 先读�?Serilog 节点
- 然后代码中的 WriteTo.Console �?WriteTo.File 会覆盖配置文件中相同类型的配置`n - Logging 节点被读取但不会使用(保留是为了兼容性)
- **建议**:`n - 如果希望配置文件优先级更高,可以调整代码顺序,或者移除代码中的硬编码配置
- 可以考虑移除 Logging 节点,因为目前不会生效`n - 或者保�?Logging 节点用于其他日志提供程序的兼容性`n
### 移除 Logging 配置节点
- **日期**: 2025�?月`n- **修改内容**: 移除无用�?Logging 配置节点和相关代码,因为只使�?Serilog
- **修改文件**:
- AuroraDesk/appsettings.json (移除 Logging 节点)
- AuroraDesk.Infrastructure/Extensions/HostBuilderExtensions.cs (移除 ConfigureLogging 调用�?Logging 配置读取,移�?using Microsoft.Extensions.Logging)
- **主要变更**:
- �?**�?appsettings.json 移除 Logging 节点**:`n - 移除�?Logging 配置节点及其所有子配置
- 因为只使�?Serilog,这个配置不会生效`n - �?**简�?HostBuilderExtensions.cs**:`n - 移除�?.ConfigureLogging() 调用
- 移除�?logging.ClearProviders() �?logging.AddConfiguration() 代码
- 移除�?using Microsoft.Extensions.Logging; 引用(不再需要)
- 更新了注释,说明只使�?Serilog
- **架构优势**:`n - �?代码更简洁,只保留必要的配置
- �?配置文件更清晰,避免混淆
- �?减少不必要的依赖和代码`n - �?更符合单一职责原则
- **说明**:`n - 现在只使�?Serilog 作为日志框架,所有日志配置都�?Serilog 节点中`n - 代码配置(WriteTo.Console、WriteTo.File)优先级高于配置文件
- 配置文件中的 Serilog 节点用于配置日志级别等基础设置
### 鎻愬彇 Serilog 閰嶇疆鍒扮嫭绔嬫枃浠?- **鏃ユ湡**: 2025骞?鏈?- **淇敼鍐呭**: 灏?Serilog 閰嶇疆浠?appsettings.json 鎻愬彇鍒扮嫭绔嬬殑 serilog.json 鏂囦欢涓?- **淇敼鏂囦欢**:
- `AuroraDesk/serilog.json` (鏂板缓)
- `AuroraDesk/appsettings.json` (绉婚櫎 Serilog 鑺傜偣)
- `AuroraDesk/App.axaml.cs` (娣诲姞鍔犺浇 serilog.json 閰嶇疆)
- `AuroraDesk/AuroraDesk.csproj` (娣诲姞 serilog.json 鐨勫鍒堕厤缃?
- `AuroraDesk.Infrastructure/Extensions/HostBuilderExtensions.cs` (鏇存柊娉ㄩ噴璇存槑)
- **涓昏鍙樻洿**:
- 鉁?**鍒涘缓鐙珛鐨?Serilog 閰嶇疆鏂囦欢**锛? - 鏂板缓 `serilog.json` 鏂囦欢锛屽寘鍚墍鏈?Serilog 閰嶇疆
- 閰嶇疆鏂囦欢缁撴瀯淇濇寔涓嶅彉锛屽彧鏄粠 appsettings.json 涓垎绂诲嚭鏉? - 鉁?**鏇存柊閰嶇疆鍔犺浇閫昏緫**锛? - 鍦?`App.axaml.cs` 鐨?`ConfigureAppConfiguration` 涓坊鍔?`.AddJsonFile("serilog.json")` 鍔犺浇鐙珛鐨勯厤缃枃浠? - 鏀寔閰嶇疆鐑噸杞斤紙reloadOnChange: true锛? - 鉁?**鏇存柊椤圭洰閰嶇疆**锛? - 鍦?`.csproj` 涓坊鍔?`serilog.json` 鐨勫鍒堕厤缃紝纭繚鏋勫缓鏃跺鍒跺埌杈撳嚭鐩綍
- 鉁?**鏇存柊浠g爜娉ㄩ噴**锛? - 鏇存柊 `HostBuilderExtensions.cs` 涓殑娉ㄩ噴锛岃鏄庨厤缃粠 serilog.json 璇诲彇
- **鏋舵瀯浼樺娍**:
- 鉁?**閰嶇疆鍒嗙**锛氭棩蹇楅厤缃笌搴旂敤閰嶇疆鍒嗙锛岃亴璐f洿娓呮櫚
- 鉁?**鏄撲簬绠$悊**锛歋erilog 閰嶇疆鐙珛绠$悊锛屼究浜庣淮鎶ゅ拰淇敼
- 鉁?**缁撴瀯娓呮櫚**锛歛ppsettings.json 鏇寸畝娲侊紝鍙寘鍚簲鐢ㄧ浉鍏抽厤缃? - 鉁?**绗﹀悎鏈€浣冲疄璺?*锛氶厤缃枃浠舵寜鍔熻兘妯″潡鍒嗙锛屾彁楂樺彲缁存姢鎬?- **閰嶇疆鏂囦欢缁撴瀯**:
- `appsettings.json` - 搴旂敤閰嶇疆锛圓ppSettings銆丆onnectionStrings 绛夛級
- `serilog.json` - 鏃ュ織閰嶇疆锛圫erilog 鑺傜偣锛? - `appsettings.{Environment}.json` - 鐜鐗瑰畾閰嶇疆锛堝彲閫夛級
### MainWindow 关闭按钮添加确认框
- **日期**: 2025年1月
- **修改内容**: 在 MainWindow 的关闭按钮添加确认对话框,避免误操作关闭程序
- **问题分析**:
- ❌ **误操作风险**: 关闭按钮直接关闭程序,没有确认步骤,容易误操作
- ❌ **用户体验不佳**: 缺少确认机制,用户可能意外关闭程序导致数据丢失
- **修改文件**:
- `AuroraDesk.Presentation/ViewModels/MainWindowViewModel.cs` (添加确认框相关属性和命令)
- `AuroraDesk.Presentation/Views/MainWindow.axaml` (添加 DialogHost 控件)
- `AuroraDesk.Presentation/Views/MainWindow.axaml.cs` (修改关闭按钮行为)
- **主要实现**:
- ✅ **ViewModel 层**: 在 MainWindowViewModel 中添加关闭确认框相关属性
- `IsCloseConfirmDialogOpen`: 控制对话框显示状态
- `CloseConfirmDialogTitle`: 对话框标题("确认关闭")
- `CloseConfirmDialogMessage`: 对话框消息("确定要退出程序吗?")
- `CloseConfirmDialogIcon`: 对话框图标(QuestionMarkCircle)
- `CloseConfirmDialogAccentBrush`: 对话框强调色
- `ShowCloseConfirmDialogCommand`: 显示确认框命令
- `ConfirmCloseCommand`: 确认关闭命令
- `CancelCloseCommand`: 取消关闭命令
- `CloseWindowRequested`: 窗口关闭请求事件
- ✅ **View 层**: 在 MainWindow.axaml 中添加 DialogHost 控件
- 使用 DialogHost.Avalonia 库实现模态对话框
- 对话框样式参考 DialogHostPageView,保持 UI 一致性
- 包含图标、标题、消息和确认/取消按钮
- ✅ **事件处理**: 在 MainWindow.axaml.cs 中修改关闭按钮行为
- 关闭按钮点击时显示确认框,而不是直接关闭窗口
- 订阅 `CloseWindowRequested` 事件,确认后实际关闭窗口
- **技术细节**:
- 使用 DialogHost.Avalonia 库实现模态对话框,与项目中其他对话框保持一致
- 使用 ReactiveCommand 实现命令绑定,符合 ReactiveUI 模式
- 使用事件机制在 ViewModel 和 View 之间通信,避免直接引用
- 对话框样式与 DialogHostPageView 保持一致,提供统一的用户体验
- **效果**:
- ✅ **防止误操作**: 关闭程序前需要确认,避免意外关闭
- ✅ **用户体验提升**: 提供友好的确认提示,用户可以取消操作
- ✅ **UI 一致性**: 对话框样式与项目中其他对话框保持一致
- ✅ **代码规范**: 遵循 MVVM 模式和 ReactiveUI 最佳实践
### 重构:使用 ReactiveUI Interaction 实现关闭确认框(方案 3)
- **日期**: 2025年1月
- **修改内容**: 将关闭确认框从 DialogHost 包裹方式重构为 ReactiveUI Interaction 模式,符合 ReactiveUI 最佳实践
- **问题分析**:
- ❌ **架构问题**: DialogHost 包裹整个窗口,职责混淆,MainWindowViewModel 包含过多对话框 UI 状态
- ❌ **可扩展性问题**: 一个 DialogHost 只能管理一个对话框,无法添加多个对话框
- ❌ **可维护性问题**: XAML 文件过长,对话框逻辑分散在 ViewModel、View 和 Code-Behind
- ❌ **代码质量问题**: 使用传统事件而非 ReactiveUI 响应式模式,与项目风格不一致
- **修改文件**:
- `AuroraDesk.Presentation/ViewModels/CloseConfirmViewModel.cs` (新建 - 对话框 ViewModel)
- `AuroraDesk.Presentation/Views/Dialogs/CloseConfirmDialog.axaml` (新建 - 对话框 Window)
- `AuroraDesk.Presentation/Views/Dialogs/CloseConfirmDialog.axaml.cs` (新建 - 对话框代码后置)
- `AuroraDesk.Presentation/ViewModels/MainWindowViewModel.cs` (重构 - 使用 Interaction 替换原有属性)
- `AuroraDesk.Presentation/Views/MainWindow.axaml` (重构 - 移除 DialogHost 包裹)
- `AuroraDesk.Presentation/Views/MainWindow.axaml.cs` (重构 - 注册 Interaction Handler)
- **主要实现**:
- ✅ **创建独立的对话框 ViewModel**: `CloseConfirmViewModel` 专门管理对话框数据
- 包含标题、消息、图标、强调色、按钮文本等属性
- 使用 ReactiveObject 实现响应式属性通知
- ✅ **创建独立的对话框 Window**: `CloseConfirmDialog` 作为独立的窗口
- 使用 ReactiveWindow<CloseConfirmViewModel> 继承
- 对话框样式与 DialogHostPageView 保持一致
- 支持通过 ShowDialog 返回 bool 结果
- ✅ **使用 ReactiveUI Interaction**: 在 MainWindowViewModel 中定义 Interaction
- `Interaction<CloseConfirmViewModel, bool>` 类型
- `RequestCloseConfirmAsync()` 方法用于触发交互
- 完全符合 ReactiveUI 最佳实践
- ✅ **在 View 中注册 Handler**: 在 MainWindow.axaml.cs 的 WhenActivated 中注册
- 使用 `RegisterHandler` 处理交互
- 创建对话框窗口并显示,获取用户选择结果
- 使用 `DisposeWith` 确保资源释放
- ✅ **简化 MainWindowViewModel**: 移除所有对话框相关的属性(8个)和命令(3个)
- 移除 `IsCloseConfirmDialogOpen`、`CloseConfirmDialogTitle` 等属性
- 移除 `ShowCloseConfirmDialogCommand`、`ConfirmCloseCommand`、`CancelCloseCommand` 命令
- 移除 `CloseWindowRequested` 事件
- 只保留一个 `CloseConfirmInteraction``RequestCloseConfirmAsync()` 方法
- ✅ **简化 MainWindow.axaml**: 移除 DialogHost 包裹,恢复简洁的窗口结构
- 移除 `dialogHost` 命名空间引用
- 移除整个 DialogHost 控件及其内容
- 窗口结构恢复为简单的 Grid 布局
- **技术细节**:
- 使用 ReactiveUI Interaction 实现 ViewModel 和 View 之间的异步交互
- 对话框作为独立的 Window,使用 `ShowDialog<bool>` 返回结果
- 在 `WhenActivated` 中注册 Handler,确保订阅在视图激活时创建,停用时清理
- 使用 `async/await` 处理异步对话框操作
- 关闭按钮和窗口 Closing 事件都调用 `RequestCloseConfirmAsync()` 方法
- **架构优势**:
- ✅ **职责分离**: 对话框有独立的 ViewModel 和 View,职责清晰
- ✅ **可扩展性**: 可以轻松添加多个不同的对话框,每个都有独立的 Interaction
- ✅ **可维护性**: 代码结构清晰,对话框逻辑集中管理
- ✅ **符合最佳实践**: 完全符合 ReactiveUI 的 Interaction 模式
- ✅ **代码质量**: 响应式编程风格,与项目其他部分保持一致
- ✅ **可测试性**: 对话框逻辑可以独立测试
- **效果**:
- ✅ **代码简化**: MainWindowViewModel 减少了 11 个成员(8个属性 + 3个命令)
- ✅ **架构改进**: 从 DialogHost 包裹方式改为独立的对话框 Window
- ✅ **风格统一**: 完全符合 ReactiveUI 最佳实践,与项目风格一致
- ✅ **易于扩展**: 可以轻松添加其他对话框,只需创建新的 ViewModel、View 和 Interaction
- ✅ **功能完整**: 保持原有功能,关闭确认正常工作

25
AuroraDesk/serilog.json

@ -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"
}
}
}

215
MainWindow关闭确认框实现分析.md

@ -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…
Cancel
Save