1
0
Fork 0

refactor(P5a): App DI 컴포지션 루트 + MainWindow 생성자 주입 (서비스로케이터 제거)

MVVM(P5b)의 선행조건 — 엔진/설정을 생성자 주입으로 가시화하고 서비스로케이터 결합을 제거.

- App.xaml.cs: new DpapiSettingsStore()+CreateDefault() 직접 생성 → ServiceCollection.AddEverything2Everything()
  (P2 DI 재사용) 컴포지션 루트. Settings/Engine은 컨테이너 단일 싱글턴에서 해소(공유 보존).
  Generic Host 대신 ServiceProvider 사용 — Environment.Exit 진입점과 라이프사이클 충돌 회피(적대적 리뷰 반영).
- MainWindow: ((App)Application.Current).Engine/.Settings 서비스로케이터 8곳 → 생성자 주입 _engine/_settings.
  App.ShowMainWindow/ShowConvertDialog가 주입. 헤드리스 테스트 가능성 토대.

70개 테스트 전부 그린, 빌드 0경고/0오류.
This commit is contained in:
Yun Chan 2026-06-02 09:37:35 +09:00
parent 6ce4c429fe
commit 12bda2dcbf
2 changed files with 27 additions and 16 deletions

View file

@ -2,19 +2,27 @@ using System.Windows;
using Everything2Everything.App.Cli;
using Everything2Everything.App.Views;
using Everything2Everything.Core;
using Microsoft.Extensions.DependencyInjection;
namespace Everything2Everything.App;
public partial class App : Application
{
/// <summary>App·LlmProvider가 공유하는 설정 저장소 (키 저장 즉시 변환에 반영).</summary>
public ISettingsStore Settings { get; } = new DpapiSettingsStore();
private readonly IServiceProvider _services;
/// <summary>App·LlmProvider가 공유하는 설정 저장소 (키 저장 즉시 변환에 반영). DI 컨테이너 단일 싱글턴.</summary>
public ISettingsStore Settings { get; }
public ConversionEngine Engine { get; }
public App()
{
Engine = Everything2EverythingBootstrap.CreateDefault(Settings);
// 컴포지션 루트 — Core의 DI 확장(Scrutor 자동등록)으로 Provider/Registry/Engine/Settings를 구성.
var services = new ServiceCollection();
services.AddEverything2Everything();
_services = services.BuildServiceProvider();
Settings = _services.GetRequiredService<ISettingsStore>();
Engine = _services.GetRequiredService<ConversionEngine>();
}
protected override async void OnStartup(StartupEventArgs e)
@ -68,14 +76,14 @@ public partial class App : Application
private void ShowMainWindow()
{
var window = new MainWindow();
var window = new MainWindow(Engine, Settings);
MainWindow = window;
window.Show();
}
private void ShowConvertDialog(IReadOnlyList<string> files)
{
var window = new Views.MainWindow(files);
var window = new Views.MainWindow(Engine, Settings, files);
MainWindow = window;
window.Show();
}

View file

@ -16,6 +16,8 @@ namespace Everything2Everything.App.Views;
public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
{
private readonly ConversionEngine _engine;
private readonly ISettingsStore _settings;
private readonly ObservableCollection<QueueItem> _activeQueue = new();
private readonly ObservableCollection<DateGroup> _pastResults = new();
private CancellationTokenSource? _cts;
@ -28,10 +30,11 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
public ICommand CloseCommand { get; }
public ICommand RefreshCommand { get; }
public MainWindow() : this(null) { }
public MainWindow(IReadOnlyList<string>? initialFiles)
public MainWindow(ConversionEngine engine, ISettingsStore settings, IReadOnlyList<string>? initialFiles = null)
{
_engine = engine;
_settings = settings;
AddFilesCommand = new RelayCommand(_ => PickAndAddFiles());
ProcessQueueCommand = new RelayCommand(_ => OnProcessQueueClick(this, new RoutedEventArgs()),
_ => _activeQueue.Count > 0 && _cts is null);
@ -66,7 +69,7 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
private async Task RefreshCapabilityStatusAsync()
{
var engine = ((App)Application.Current).Engine;
var engine = _engine;
var notReady = new List<string>();
foreach (var p in engine.Providers.All)
{
@ -90,7 +93,7 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
private void OnSettingsClick(object sender, RoutedEventArgs e)
{
var win = new SettingsWindow(((App)Application.Current).Settings) { Owner = this };
var win = new SettingsWindow(_settings) { Owner = this };
win.ShowDialog();
_ = RefreshCapabilityStatusAsync();
RefreshAvailableOutputFormats();
@ -304,7 +307,7 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
var targetLang = AiTargetLangBox?.Text?.Trim();
opts.Ai.TargetLanguage = string.IsNullOrEmpty(targetLang) ? null : targetLang;
opts.VideoPreferGpu = ((App)Application.Current).Settings.Get("video.gpu") != "false";
opts.VideoPreferGpu = _settings.Get("video.gpu") != "false";
return opts;
}
@ -563,7 +566,7 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
UpdateProcessQueueButton();
ShowProcessingProgress(snapshot.Count);
var engine = ((App)Application.Current).Engine;
var engine = _engine;
var options = BuildOptions();
var reporter = new Progress<ConvertProgress>(p =>
@ -780,7 +783,7 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
{
try
{
ContextMenuRegistrar.Register(((App)Application.Current).Engine);
ContextMenuRegistrar.Register(_engine);
MessageBox.Show(this,
"컨텍스트 메뉴를 등록했습니다.\n파일 우클릭 → \"추가 옵션 표시\" 또는 \"JPEG로 빠른 변환/변환…\".",
"Everything2Everything", MessageBoxButton.OK, MessageBoxImage.Information);
@ -794,7 +797,7 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
private void OnDiagnoseClick(object sender, RoutedEventArgs e)
{
var window = new DiagnoseWindow(((App)Application.Current).Engine) { Owner = this };
var window = new DiagnoseWindow(_engine) { Owner = this };
window.ShowDialog();
}
@ -934,7 +937,7 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
private LossClass? WorstLossForQueue(string outputExt)
{
if (_activeQueue.Count == 0) return null;
var graph = ((App)Application.Current).Engine.Providers.Graph;
var graph = _engine.Providers.Graph;
LossClass? worst = null;
foreach (var item in _activeQueue)
{
@ -971,7 +974,7 @@ public partial class MainWindow : Wpf.Ui.Controls.FluentWindow
{
if (OutputFormatCombo is null) return;
var engine = ((App)Application.Current).Engine;
var engine = _engine;
IReadOnlyCollection<string> available;
if (_activeQueue.Count == 0)