Skip to content

Commit 95fa75d

Browse files
committed
story 9
1 parent ba42b66 commit 95fa75d

31 files changed

+944
-530
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,3 +400,4 @@ FodyWeavers.xsd
400400
# JetBrains Rider
401401
*.sln.iml
402402
docs/Story/.keep
403+
/.idea/.idea.Modulus/.idea

src/Modulus.App/App.axaml.cs

Lines changed: 84 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,24 @@
1-
using Avalonia;
1+
using Avalonia;
22
using Avalonia.Controls.ApplicationLifetimes;
33
using Avalonia.Data.Core.Plugins;
44
using Avalonia.Markup.Xaml;
55
using Modulus.App.Services;
66
using Modulus.App.ViewModels;
77
using Modulus.App.Views;
8+
using Modulus.App.Options;
89
using Microsoft.Extensions.DependencyInjection;
10+
using Microsoft.Extensions.Configuration;
11+
using Microsoft.Extensions.Options;
912
using System;
13+
using System.IO;
1014

1115
namespace Modulus.App;
1216

1317
public partial class App : Application
1418
{
1519
private ServiceProvider? _serviceProvider;
20+
private IConfiguration? _configuration;
21+
private ConfigurationChangeListener? _configChangeListener;
1622

1723
public override void Initialize()
1824
{
@@ -21,53 +27,102 @@ public override void Initialize()
2127

2228
public override void OnFrameworkInitializationCompleted()
2329
{
24-
// Line below is needed to remove Avalonia data validation.
25-
// Without this line you will get duplicate validations from both Avalonia and CT
26-
BindingPlugins.DataValidators.RemoveAt(0);
30+
try
31+
{
32+
// Line below is needed to remove Avalonia data validation.
33+
// Without this line you will get duplicate validations from both Avalonia and CT
34+
BindingPlugins.DataValidators.RemoveAt(0);
2735

28-
// 配置服务
29-
ConfigureServices();
36+
// 配置服务
37+
ConfigureServices();
3038

31-
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
32-
{
33-
var mainViewModel = _serviceProvider!.GetRequiredService<MainViewModel>();
34-
desktop.MainWindow = new MainWindow
39+
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
40+
{
41+
var mainViewModel = _serviceProvider!.GetRequiredService<MainWindowViewModel>();
42+
desktop.MainWindow = new MainWindow(mainViewModel);
43+
44+
// 启动配置更改监听器
45+
_configChangeListener = _serviceProvider.GetRequiredService<ConfigurationChangeListener>();
46+
47+
desktop.Exit += (s, e) =>
48+
{
49+
_configChangeListener?.Dispose();
50+
(_serviceProvider as IDisposable)?.Dispose();
51+
};
52+
}
53+
else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform)
3554
{
36-
DataContext = mainViewModel
37-
};
55+
var mainViewModel = _serviceProvider!.GetRequiredService<MainWindowViewModel>();
56+
singleViewPlatform.MainView = new MainWindow(mainViewModel);
57+
}
58+
59+
base.OnFrameworkInitializationCompleted();
3860
}
39-
else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform)
61+
catch (OptionsValidationException ex)
4062
{
41-
// 在单视图平台中,也应该使用依赖注入获取MainViewModel
42-
var mainViewModel = _serviceProvider!.GetRequiredService<MainViewModel>();
43-
singleViewPlatform.MainView = new MainView
63+
// 处理配置验证错误
64+
System.Diagnostics.Debug.WriteLine($"Configuration validation failed: {string.Join(", ", ex.Failures)}");
65+
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
4466
{
45-
DataContext = mainViewModel
46-
};
67+
desktop.Shutdown(1);
68+
}
69+
throw;
4770
}
48-
49-
base.OnFrameworkInitializationCompleted();
5071
}
5172

5273
private void ConfigureServices()
5374
{
75+
// 构建配置
76+
var builder = new ConfigurationBuilder()
77+
.SetBasePath(AppContext.BaseDirectory)
78+
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
79+
.AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"}.json", optional: true, reloadOnChange: true)
80+
.AddEnvironmentVariables("MODULUS_")
81+
.AddUserSecrets<App>(optional: true);
82+
83+
_configuration = builder.Build();
84+
5485
var services = new ServiceCollection();
55-
86+
87+
// 注册配置服务
88+
services.AddSingleton<IConfiguration>(_configuration);
89+
90+
// 注册选项(支持热重载和验证)
91+
services.AddOptions<AppOptions>()
92+
.Bind(_configuration.GetSection(AppOptions.SectionName))
93+
.ValidateDataAnnotations()
94+
.ValidateOnStart();
95+
96+
// 注册插件选项(支持热重载、验证和目录创建)
97+
services.AddOptions<PluginOptions>()
98+
.Bind(_configuration.GetSection(PluginOptions.SectionName))
99+
.ValidateOnStart();
100+
101+
// 注册自定义选项验证器
102+
services.AddSingleton<IValidateOptions<PluginOptions>, PluginOptionsValidation>();
103+
104+
// 注册配置更改监听器
105+
services.AddSingleton<ConfigurationChangeListener>();
106+
56107
// 注册导航服务(单例)
57108
services.AddSingleton<INavigationService, NavigationService>();
58-
109+
59110
// 注册导航插件服务(单例)
60111
services.AddSingleton<NavigationPluginService>();
61-
112+
62113
// 注册插件管理器(单例)
63114
services.AddSingleton<IPluginManager, PluginManager>();
64-
65-
// 注册视图模型(单例)
66-
services.AddSingleton<MainViewModel>();
67-
68-
// 注册页面的ViewModel(可选择注册为单例或Transient
115+
116+
// 注册主视图模型(单例)
117+
services.AddSingleton<MainWindowViewModel>();
118+
119+
// 注册所有页面的ViewModel(Transient
69120
services.AddTransient<DashboardViewModel>();
70-
121+
services.AddTransient<PluginManagerViewModel>();
122+
services.AddTransient<SettingsViewModel>();
123+
services.AddTransient<MainWindowViewModel>();
124+
services.AddTransient<MainWindowViewModel>();
125+
71126
// 创建服务提供器
72127
_serviceProvider = services.BuildServiceProvider();
73128
}

src/Modulus.App/Controls/ViewModels/NavigationViewModel.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,12 @@ public partial class NavigationViewModel : ObservableObject
115115
[ObservableProperty]
116116
private IRelayCommand<NavigationItemModel>? navigateToViewCommand;
117117

118+
/// <summary>
119+
/// 状态消息(用于显示插件加载等状态)
120+
/// </summary>
121+
[ObservableProperty]
122+
private string statusMessage = string.Empty;
123+
118124
#endregion
119125

120126
#region Commands
@@ -262,4 +268,9 @@ public class NavigationPageInfo
262268
/// Navigation route/identifier for this page
263269
/// </summary>
264270
public string Route { get; set; } = string.Empty;
265-
}
271+
272+
/// <summary>
273+
/// Icon character to display (typically from an icon font like Segoe MDL2 Assets)
274+
/// </summary>
275+
public string Icon { get; set; } = string.Empty;
276+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using System;
2+
using Microsoft.Extensions.Configuration;
3+
using Microsoft.Extensions.Primitives;
4+
5+
namespace Microsoft.Extensions.Configuration
6+
{
7+
public static class ConfigurationExtensionsCompat
8+
{
9+
public static T GetValue<T>(this IConfiguration configuration, string key, T defaultValue = default!)
10+
{
11+
var value = configuration[key];
12+
if (value == null)
13+
return defaultValue;
14+
return (T)Convert.ChangeType(value, typeof(T));
15+
}
16+
}
17+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
using System;
2+
using System.IO;
3+
using Modulus.App.Options;
4+
5+
namespace Modulus.App.Extensions
6+
{
7+
public static class OptionsExtensions
8+
{
9+
/// <summary>
10+
/// 获取已处理环境变量的插件安装路径
11+
/// </summary>
12+
public static string GetResolvedInstallPath(this PluginOptions options)
13+
{
14+
var path = Environment.ExpandEnvironmentVariables(options.InstallPath);
15+
return Path.IsPathRooted(path)
16+
? path
17+
: Path.Combine(AppContext.BaseDirectory, path);
18+
}
19+
20+
/// <summary>
21+
/// 获取已处理环境变量的用户插件路径
22+
/// </summary>
23+
public static string GetResolvedUserPath(this PluginOptions options)
24+
{
25+
return Environment.ExpandEnvironmentVariables(options.UserPath);
26+
}
27+
28+
/// <summary>
29+
/// 确保插件目录存在
30+
/// </summary>
31+
public static void EnsurePluginDirectoriesExist(this PluginOptions options)
32+
{
33+
var installPath = options.GetResolvedInstallPath();
34+
var userPath = options.GetResolvedUserPath();
35+
36+
if (!Directory.Exists(installPath))
37+
Directory.CreateDirectory(installPath);
38+
39+
if (!Directory.Exists(userPath))
40+
Directory.CreateDirectory(userPath);
41+
}
42+
}
43+
}

src/Modulus.App/Modulus.App.csproj

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,19 @@
1414
<PackageReference Include="Avalonia.Themes.Fluent" Version="$(AvaloniaVersion)" />
1515
<PackageReference Include="Avalonia.Fonts.Inter" Version="$(AvaloniaVersion)" />
1616
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.0" />
17-
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
18-
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
19-
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
17+
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.5" />
18+
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.5" />
19+
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.5" />
20+
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.5" />
21+
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.5" />
22+
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="9.0.5" />
23+
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.5" />
24+
<PackageReference Include="Microsoft.Extensions.Options" Version="9.0.5" />
25+
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.5" />
2026

2127
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
2228
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="$(AvaloniaVersion)" />
29+
<PackageReference Include="Microsoft.Extensions.Options.DataAnnotations" Version="9.0.5" />
2330
</ItemGroup>
2431
<ItemGroup>
2532
<ProjectReference Include="..\Modulus.Plugin.Abstractions\Modulus.Plugin.Abstractions.csproj" />
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using System;
2+
using System.ComponentModel.DataAnnotations;
3+
4+
namespace Modulus.App.Options
5+
{
6+
/// <summary>
7+
/// 应用程序配置选项
8+
/// </summary>
9+
public sealed class AppOptions
10+
{
11+
public const string SectionName = "App";
12+
13+
/// <summary>
14+
/// 应用程序名称
15+
/// </summary>
16+
[Required]
17+
[MinLength(1)]
18+
public string Name { get; init; } = "Modulus";
19+
20+
/// <summary>
21+
/// 应用程序版本
22+
/// </summary>
23+
[Required]
24+
[RegularExpression(@"^\d+\.\d+\.\d+(?:\-.+)?$", ErrorMessage = "Version must be in format X.Y.Z or X.Y.Z-suffix")]
25+
public string Version { get; init; } = "1.0.0";
26+
}
27+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
using System;
2+
using System.ComponentModel.DataAnnotations;
3+
4+
namespace Modulus.App.Options
5+
{
6+
/// <summary>
7+
/// 插件系统配置选项
8+
/// </summary>
9+
public sealed class PluginOptions
10+
{
11+
public const string SectionName = "Plugins";
12+
13+
/// <summary>
14+
/// 安装目录下的插件路径
15+
/// </summary>
16+
[Required]
17+
[MinLength(1)]
18+
public string InstallPath { get; init; } = "Plugins";
19+
20+
/// <summary>
21+
/// 用户数据目录下的插件路径
22+
/// </summary>
23+
[Required]
24+
[MinLength(1)]
25+
public string UserPath { get; init; } = "%APPDATA%/Modulus/Plugins";
26+
27+
/// <summary>
28+
/// 验证插件路径是否存在
29+
/// </summary>
30+
public bool ValidateDirectoryExists { get; init; } = true;
31+
}
32+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
using Microsoft.Extensions.Options;
2+
using System;
3+
using System.ComponentModel.DataAnnotations;
4+
using System.IO;
5+
6+
namespace Modulus.App.Options
7+
{
8+
public class PluginOptionsValidation : IValidateOptions<PluginOptions>
9+
{
10+
public ValidateOptionsResult Validate(string? name, PluginOptions options)
11+
{
12+
try
13+
{
14+
// First validate required fields using data annotations
15+
var validationContext = new ValidationContext(options);
16+
Validator.ValidateObject(options, validationContext, validateAllProperties: true);
17+
18+
// Expand environment variables in paths
19+
var installPath = Environment.ExpandEnvironmentVariables(options.InstallPath);
20+
var userPath = Environment.ExpandEnvironmentVariables(options.UserPath);
21+
22+
// Create directories if they don't exist
23+
if (!Directory.Exists(installPath))
24+
{
25+
Directory.CreateDirectory(installPath);
26+
}
27+
28+
if (!Directory.Exists(userPath))
29+
{
30+
Directory.CreateDirectory(userPath);
31+
}
32+
33+
// Validate that paths are now accessible
34+
if (!Directory.Exists(installPath))
35+
{
36+
return ValidateOptionsResult.Fail($"Unable to create or access plugin install directory '{installPath}'");
37+
}
38+
39+
if (!Directory.Exists(userPath))
40+
{
41+
return ValidateOptionsResult.Fail($"Unable to create or access plugin user directory '{userPath}'");
42+
}
43+
44+
return ValidateOptionsResult.Success;
45+
}
46+
catch (ValidationException ex)
47+
{
48+
return ValidateOptionsResult.Fail(ex.Message);
49+
}
50+
catch (Exception ex)
51+
{
52+
return ValidateOptionsResult.Fail($"Plugin directory validation failed: {ex.Message}");
53+
}
54+
}
55+
}
56+
}

0 commit comments

Comments
 (0)