using System.Runtime.InteropServices; using MBN_STOCK_WEBVIEW.Infrastructure; using MBN_STOCK_WEBVIEW.LegacyApplication; using MBN_STOCK_WEBVIEW.LegacyBridge; using MBN_STOCK_WEBVIEW.Playout.Safety; using Microsoft.UI.Windowing; using Microsoft.Web.WebView2.Core; using MMoneyCoderSharp.Data; using Windows.Graphics; using WinRT.Interop; namespace MBN_STOCK_WEBVIEW.LegacyParityApp; public sealed partial class MainWindow : Window { private const int SmCxDrag = 68; private const int SmCyDrag = 69; private const uint WmSettingChange = 0x001A; private const uint WmDisplayChange = 0x007E; private const uint WmDpiChanged = 0x02E0; private const nuint InteractionMetricsSubclassId = 0x4D424E49; private readonly CancellationTokenSource _lifetimeCancellation = new(); private readonly SemaphoreSlim _intentGate = new(1, 1); private readonly SemaphoreSlim _orderedPlayoutIntentGate = new(1, 1); private readonly PlayoutLaunchAuthorization _playoutLaunchAuthorization; private readonly WindowSubclassProcedure _windowSubclassProcedure; private readonly LegacyOperatorController _controller; private readonly LegacyIndustrySelectionWorkflow _industryWorkflow; private DatabaseRuntime? _databaseRuntime; private bool _closing; private bool _interactionMetricsRefreshQueued; private bool _intentBusy; private bool _windowSubclassAttached; private bool _webViewReady; private nint _windowHandle; private LegacyInteractionMetrics _interactionMetrics = LegacyInteractionMetrics.Default; public MainWindow() { // Capture and remove Gate A bearer material before XAML can create WebView2. // Only the native process keeps the capability digest for this launch. _playoutLaunchAuthorization = PlayoutLaunchAuthorization.CaptureFromEnvironment(); _windowSubclassProcedure = OnWindowSubclassMessage; InitializeComponent(); EnsureCloseConfirmationAttached(); var localApplicationData = Environment.GetFolderPath( Environment.SpecialFolder.LocalApplicationData); var runtimeCutMenuOverridePath = string.IsNullOrWhiteSpace(localApplicationData) ? null : Path.Combine( localApplicationData, "MBN_STOCK_WEBVIEW", "Res", "종목.ini"); var cutMenuComposition = LegacyRuntimeStockCutMenuLoader.ResolveFromCandidates( runtimeCutMenuOverridePath, AppContext.BaseDirectory); var runtimeUiCatalogs = LegacyRuntimeUiCatalogLoader.ResolveFromBaseDirectory( AppContext.BaseDirectory); ILegacyStockLookup stockLookup; IIndustrySelectionService industrySelectionService; IThemeSelectionService themeSelectionService; IExpertSelectionService expertSelectionService; ITradingHaltSelectionService tradingHaltSelectionService; IOverseasIndustryIndexSearchService overseasIndustrySearchService; IOverseasStockSearchService overseasStockSearchService; IWorldStockSearchService worldStockSearchService; IManualFinancialScreenService manualFinancialService; IStockSearchService manualFinancialStockSearchService; INamedPlaylistPersistenceService namedPlaylistPersistenceService; IThemeCatalogPersistenceService themeCatalogPersistenceService; IExpertCatalogPersistenceService expertCatalogPersistenceService; IStockMasterIdentityValidationService stockMasterIdentityValidationService; IOperatorCatalogSchemaValidationService operatorCatalogSchemaValidationService; string? initializationError = null; try { _databaseRuntime = DatabaseRuntime.CreateLegacyExecutableDefault( AppContext.BaseDirectory); DataQueryExecutor.Configure(_databaseRuntime.Executor); var databaseMutationAuthorization = new LaunchDatabaseMutationAuthorization(_playoutLaunchAuthorization); stockLookup = new LegacyParityStockSearchService(_databaseRuntime.Executor); industrySelectionService = new LegacyIndustrySelectionService( _databaseRuntime.Executor); themeSelectionService = new LegacyThemeSelectionService( _databaseRuntime.Executor); expertSelectionService = new LegacyExpertSelectionService( _databaseRuntime.Executor); tradingHaltSelectionService = new LegacyTradingHaltSelectionService( _databaseRuntime.Executor); overseasIndustrySearchService = new LegacyOverseasIndustryIndexSearchService( _databaseRuntime.Executor); overseasStockSearchService = new LegacyOverseasStockSearchService( _databaseRuntime.Executor); worldStockSearchService = new LegacyWorldStockSearchService( _databaseRuntime.Executor); manualFinancialService = new LegacyManualFinancialScreenService( _databaseRuntime.Executor, new OracleManualFinancialMutationExecutor( _databaseRuntime.ConnectionFactory, _databaseRuntime.Options.Resilience, errorDetector: null, mutationAuthorization: databaseMutationAuthorization)); manualFinancialStockSearchService = new LegacyStockSearchService( _databaseRuntime.Executor); namedPlaylistPersistenceService = new LegacyNamedPlaylistPersistenceService( _databaseRuntime.Executor, new OracleNamedPlaylistMutationExecutor( _databaseRuntime.ConnectionFactory, _databaseRuntime.Options.Resilience, errorDetector: null, mutationAuthorization: databaseMutationAuthorization)); var operatorCatalogMutationExecutor = new OperatorCatalogMutationExecutor( _databaseRuntime.ConnectionFactory, _databaseRuntime.Options.Resilience, errorDetector: null, mutationAuthorization: databaseMutationAuthorization); themeCatalogPersistenceService = new LegacyThemeCatalogPersistenceService( _databaseRuntime.Executor, operatorCatalogMutationExecutor); expertCatalogPersistenceService = new LegacyExpertCatalogPersistenceService( _databaseRuntime.Executor, operatorCatalogMutationExecutor); stockMasterIdentityValidationService = new LegacyStockMasterIdentityValidationService(_databaseRuntime.Executor); operatorCatalogSchemaValidationService = new LegacyOperatorCatalogSchemaValidationService(_databaseRuntime.Executor); } catch { initializationError = "데이터베이스가 설정되지 않았습니다. 로컬 설정을 확인하세요."; stockLookup = new UnavailableStockLookup(initializationError); industrySelectionService = new UnavailableIndustrySelectionService( initializationError); themeSelectionService = new UnavailableThemeSelectionService( initializationError); expertSelectionService = new UnavailableExpertSelectionService( initializationError); tradingHaltSelectionService = new UnavailableTradingHaltSelectionService( initializationError); overseasIndustrySearchService = new UnavailableOverseasIndustryIndexSearchService( initializationError); overseasStockSearchService = new UnavailableOverseasStockSearchService( initializationError); worldStockSearchService = new UnavailableWorldStockSearchService( initializationError); manualFinancialService = new UnavailableManualFinancialScreenService( initializationError); manualFinancialStockSearchService = new UnavailableStockSearchService( initializationError); namedPlaylistPersistenceService = new UnavailableNamedPlaylistPersistenceService( initializationError); themeCatalogPersistenceService = new UnavailableThemeCatalogPersistenceService( initializationError); expertCatalogPersistenceService = new UnavailableExpertCatalogPersistenceService( initializationError); stockMasterIdentityValidationService = new UnavailableStockMasterIdentityValidationService(initializationError); operatorCatalogSchemaValidationService = new UnavailableOperatorCatalogSchemaValidationService(initializationError); DataQueryExecutor.Reset(); } _industryWorkflow = new LegacyIndustrySelectionWorkflow( industrySelectionService, actionLayout: runtimeUiCatalogs.IndustryLayout); var overseasWorkflow = new LegacyOverseasSelectionWorkflow( overseasIndustrySearchService, overseasStockSearchService); var comparisonStore = new LegacyComparisonPairFileStore( Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "MBN_STOCK_WEBVIEW", "Data", "종목비교.dat"), Path.Combine(AppContext.BaseDirectory, "Data", "종목비교.dat")); var manualListsWorkflow = CreateManualListsWorkflow(stockLookup); _controller = new LegacyOperatorController( stockLookup, fixedCatalog: runtimeUiCatalogs.FixedCatalog, industryWorkflow: _industryWorkflow, themeWorkflow: new LegacyThemeWorkflow(themeSelectionService), expertWorkflow: new LegacyExpertWorkflowController(expertSelectionService), tradingHaltWorkflow: new LegacyTradingHaltWorkflowController( tradingHaltSelectionService), comparisonWorkflow: new LegacyComparisonWorkflowController( new LegacyComparisonDataService(stockLookup, worldStockSearchService), comparisonStore, runtimeUiCatalogs.ComparisonLayout), manualFinancialWorkflow: new LegacyManualFinancialWorkflow( manualFinancialService, manualFinancialStockSearchService), namedPlaylistWorkflow: new LegacyNamedPlaylistWorkflowController( namedPlaylistPersistenceService), overseasWorkflow: overseasWorkflow, manualListsWorkflow: manualListsWorkflow, operatorCatalogWorkflow: new LegacyOperatorCatalogWorkflowController( themeSelectionService, expertSelectionService, themeCatalogPersistenceService, expertCatalogPersistenceService, manualFinancialStockSearchService, stockMasterIdentityValidationService, operatorCatalogSchemaValidationService), cutRows: cutMenuComposition.Rows); if (initializationError is not null) { _controller.ReportError(initializationError); } else if (new[] { cutMenuComposition.WarningMessage, runtimeUiCatalogs.WarningMessage } .Where(message => message is not null) .ToArray() is { Length: > 0 } catalogWarnings) { _controller.ReportWarning(string.Join(" ", catalogWarnings)); } InitializePlayoutRuntime(); Root.Loaded += OnRootLoaded; Closed += OnClosed; } private async void OnRootLoaded(object sender, RoutedEventArgs e) { Root.Loaded -= OnRootLoaded; if (_closing) { return; } ConfigureWindow(); await InitializeWebViewAsync(); if (!_closing) { StartDatabaseHealthMonitor(); await ConnectPlayoutAsync(_lifetimeCancellation.Token); } } private void ConfigureWindow() { EnsureCloseConfirmationAttached(); var windowHandle = WindowNative.GetWindowHandle(this); _windowHandle = windowHandle; AttachInteractionMetricsRefresh(); RefreshInteractionMetrics(); var windowId = Microsoft.UI.Win32Interop.GetWindowIdFromWindow(windowHandle); var appWindow = AppWindow.GetFromWindowId(windowId); appWindow.Title = "V-Stock 증권정보송출시스템 for 매일경제TV (26.03.26)"; if (appWindow.Presenter is OverlappedPresenter presenter) { presenter.IsMaximizable = false; presenter.IsResizable = false; } // WinForms stored 1905 x 1015 as ClientSize. AppWindow.Resize would include // the caption and borders and silently make the WebView viewport smaller. appWindow.ResizeClient(new SizeInt32(1905, 1015)); var displayArea = DisplayArea.GetFromWindowId(windowId, DisplayAreaFallback.Primary); appWindow.Move(new PointInt32(displayArea.WorkArea.X, displayArea.WorkArea.Y)); } private async Task InitializeWebViewAsync() { try { var webRoot = Path.Combine(AppContext.BaseDirectory, "Web"); var startPage = Path.Combine(webRoot, "index.html"); if (!File.Exists(startPage)) { throw new FileNotFoundException("The parity Web UI was not found.", startPage); } await Browser.EnsureCoreWebView2Async(); if (_closing) { return; } var coreWebView = Browser.CoreWebView2 ?? throw new InvalidOperationException("WebView2 was not initialized."); coreWebView.SetVirtualHostNameToFolderMapping( LegacyBridgeProtocol.TrustedHost, webRoot, CoreWebView2HostResourceAccessKind.DenyCors); coreWebView.Settings.AreDevToolsEnabled = System.Diagnostics.Debugger.IsAttached; coreWebView.Settings.AreBrowserAcceleratorKeysEnabled = false; coreWebView.Settings.AreDefaultContextMenusEnabled = false; coreWebView.Settings.IsStatusBarEnabled = false; coreWebView.Settings.IsZoomControlEnabled = false; coreWebView.Settings.IsPinchZoomEnabled = false; coreWebView.NavigationStarting += OnNavigationStarting; coreWebView.NavigationCompleted += OnNavigationCompleted; coreWebView.NewWindowRequested += OnNewWindowRequested; coreWebView.WebMessageReceived += OnWebMessageReceived; coreWebView.ProcessFailed += OnProcessFailed; _webViewReady = true; Browser.Source = new Uri( $"https://{LegacyBridgeProtocol.TrustedHost}/index.html"); } catch (Exception exception) { if (!_closing) { ShowError("WebView2 초기화에 실패했습니다.", exception.Message); } } } private void OnNavigationStarting(object? sender, CoreWebView2NavigationStartingEventArgs args) { if (!LegacyBridgeProtocol.IsTrustedSource(args.Uri) && !string.Equals(args.Uri, "about:blank", StringComparison.OrdinalIgnoreCase)) { args.Cancel = true; } } private void OnNavigationCompleted(object? sender, CoreWebView2NavigationCompletedEventArgs args) { if (_closing) { return; } LoadingOverlay.Visibility = Visibility.Collapsed; if (!args.IsSuccess) { ShowError("원본 호환 화면을 불러오지 못했습니다.", args.WebErrorStatus.ToString()); } } private static void OnNewWindowRequested( object? sender, CoreWebView2NewWindowRequestedEventArgs args) { args.Handled = true; } private void OnWebMessageReceived( object? sender, CoreWebView2WebMessageReceivedEventArgs args) { if (_closing) { return; } LegacyUiIntent? intent = null; string? parseError = null; if (!LegacyBridgeProtocol.IsTrustedSource(args.Source) || !LegacyBridgeProtocol.TryParseIntent( args.WebMessageAsJson, out intent, out parseError) || intent is null) { if (!string.IsNullOrEmpty(parseError)) { PostState(_controller.ReportError(parseError)); } return; } _ = ProcessIntentAsync(intent); } private async Task ProcessIntentAsync(LegacyUiIntent intent) { var gateEntered = false; var orderedPlayoutIntentEntered = false; var databaseGateEntered = false; var dispatchStarted = false; try { // MainForm performed database search synchronously. The busy snapshot // freezes the Web controls. Playout commands must retain their order behind // a preceding local row-selection intent, but only one may wait or execute // at a time so a double post can never become a queued vendor command. if (IsOrderedPlayoutIntent(intent)) { orderedPlayoutIntentEntered = await _orderedPlayoutIntentGate.WaitAsync( 0, _lifetimeCancellation.Token); if (orderedPlayoutIntentEntered) { await _intentGate.WaitAsync(_lifetimeCancellation.Token); gateEntered = true; } } else { // Keep the zero-timeout admission used by database and edit intents; // those requests must not build an unbounded queue behind slow I/O. gateEntered = await _intentGate.WaitAsync( 0, _lifetimeCancellation.Token); } if (!gateEntered) { PostState(IsNamedPlaylistModalIntent(intent) ? ReportIntentFailure( intent, "다른 요청을 처리 중이어서 DB 재생목록 요청을 시작하지 않았습니다.") : _controller.Current); return; } if (_closing) { return; } if (_playoutLaunchAuthorization.IsGateA && IsPersistentWriteIntent(intent)) { PostState(ReportIntentFailure( intent, "Gate A 검증 회차에서는 DB 및 로컬 영구 저장 변경이 허용되지 않습니다.")); return; } if (_controller.Current.Dialog?.IsConfirmation == true && intent is not LegacyReadyIntent and not LegacyConfirmNativeDialogIntent and not LegacyCancelNativeDialogIntent) { // A native MessageBox is modal in the original application. Ignore // every forged or already-posted background intent until the // operator answers the currently visible Yes/No question. PostState(_controller.Current); return; } if (IsOperatorMutationIntent(intent) && IsOperatorMutationQuarantined()) { PostState(ReportIntentFailure( intent, "이전 저장 결과가 불명확하여 이번 실행의 추가 변경이 차단되었습니다. DB 또는 로컬 파일을 읽기 전용으로 대조한 뒤 앱을 다시 시작하세요.")); return; } if (_controller.Current.FixedSectionBatch is not null && IsOperatorMutationIntent(intent) && !IsFixedSectionBatchMutationIntent(intent)) { PostState(ReportIntentFailure( intent, "고정 컷 섹션의 수동 입력 중에는 현재 입력 단계만 변경할 수 있습니다. 완료하거나 취소한 뒤 다른 편성 작업을 실행하세요.")); return; } if (IsOperatorMutationIntent(intent) && !CanAcceptOperatorMutation()) { PostState(ReportIntentFailure( intent, "PREPARE 또는 송출 중에는 플레이리스트와 운영 데이터를 변경할 수 없습니다. TAKE OUT 후 다시 시도하세요.")); return; } if ((intent is LegacySearchStocksIntent searchIntent && !(searchIntent.Trigger == LegacySearchTrigger.Button && searchIntent.Text.Length == 0)) || intent is LegacySelectTabIntent or LegacySearchThemesIntent or LegacySelectThemeIntent or LegacyActivateThemeResultIntent or LegacySearchExpertsIntent or LegacySelectExpertIntent or LegacySearchTradingHaltsIntent or LegacySearchComparisonDomesticIntent or LegacySearchComparisonWorldIntent or LegacySearchOverseasIndustriesIntent or LegacySearchOverseasStocksIntent or LegacyOpenOperatorCatalogIntent or LegacyBeginUc4ThemeCatalogIntent or LegacyEditUc4SelectedThemeIntent or LegacyBeginUc6ExpertCatalogIntent or LegacyEditUc6SelectedExpertIntent or LegacySearchOperatorCatalogIntent or LegacySelectOperatorCatalogResultIntent or LegacyBeginCreateThemeCatalogIntent or LegacyBeginCreateExpertCatalogIntent or LegacySearchOperatorCatalogStocksIntent or LegacyActivateOperatorCatalogStockIntent or LegacySetOperatorCatalogDraftBuyAmountIntent or LegacyCheckOperatorCatalogThemeNameIntent or LegacySaveOperatorCatalogIntent or LegacyConfirmNativeDialogIntent or LegacyAddComparisonPairIntent or LegacyMoveComparisonPairIntent or LegacyDeleteSelectedComparisonPairIntent or LegacyDeleteAllComparisonPairsIntent or LegacyOpenManualFinancialIntent or LegacySearchManualFinancialIntent or LegacyFindManualFinancialIntent or LegacyLoadManualFinancialIntent or LegacyActivateManualFinancialResultIntent or LegacySearchManualFinancialStocksIntent or LegacySaveManualFinancialIntent or LegacyDeleteManualFinancialIntent or LegacyDeleteAllManualFinancialIntent or LegacyOpenManualListIntent or LegacyRefreshManualListIntent or LegacySaveManualNetSellIntent or LegacySearchManualViIntent or LegacySaveManualViIntent or LegacyImportManualListsIntent or LegacyRefreshNamedPlaylistsIntent or LegacyLoadSelectedNamedPlaylistIntent or LegacyLoadNamedPlaylistByIdIntent or LegacyCreateNamedPlaylistIntent or LegacySaveCurrentNamedPlaylistIntent or LegacySaveCurrentNamedPlaylistToIntent or LegacyDeleteSelectedNamedPlaylistIntent or LegacyExecutePlayoutIntent or LegacyGateAPrepareIntent or LegacySetFadeDurationIntent or LegacyToggleBackgroundIntent or LegacyChooseBackgroundIntent) { _intentBusy = true; PostState(_controller.Current); } await AcquirePriorityDatabaseActivityAsync(_lifetimeCancellation.Token); databaseGateEntered = true; if (_closing) { return; } dispatchStarted = true; var state = await HandleIntentAsync(intent, _lifetimeCancellation.Token); ObserveOperatorMutationQuarantine(state); if (!_closing) { _intentBusy = false; PostState(state); } } catch (OperationCanceledException) when (_lifetimeCancellation.IsCancellationRequested) { } catch when (_closing) { } catch { _intentBusy = false; var failureState = ReportIntentFailure( intent, "요청을 처리하지 못했습니다. 데이터베이스 연결 상태를 확인하세요.", writeOutcomeMayBeUnknown: dispatchStarted); ObserveOperatorMutationQuarantine(failureState); PostState(failureState); } finally { if (_closing) { _intentBusy = false; } if (databaseGateEntered) { _databaseActivityGate.Release(); } if (gateEntered) { _intentGate.Release(); } if (orderedPlayoutIntentEntered) { _orderedPlayoutIntentGate.Release(); } } } private static bool IsOrderedPlayoutIntent(LegacyUiIntent intent) => intent is LegacyExecutePlayoutIntent or LegacyGateAPrepareIntent; private static bool IsNamedPlaylistModalIntent(LegacyUiIntent intent) => intent is LegacyRefreshNamedPlaylistsIntent or LegacySelectNamedPlaylistIntent or LegacyLoadSelectedNamedPlaylistIntent or LegacyLoadNamedPlaylistByIdIntent or LegacyCreateNamedPlaylistIntent or LegacySaveCurrentNamedPlaylistIntent or LegacySaveCurrentNamedPlaylistToIntent or LegacyDeleteSelectedNamedPlaylistIntent; private LegacyOperatorSnapshot ReportIntentFailure( LegacyUiIntent intent, string message, bool writeOutcomeMayBeUnknown = false) => intent switch { LegacyRefreshNamedPlaylistsIntent refresh when !string.IsNullOrWhiteSpace(refresh.RequestId) => _controller.ReportNamedPlaylistCommandFailure( "refresh-named-playlists", refresh.RequestId, message), LegacySelectNamedPlaylistIntent selection when !string.IsNullOrWhiteSpace(selection.RequestId) => _controller.ReportNamedPlaylistCommandFailure( "select-named-playlist", selection.RequestId, message), LegacyCreateNamedPlaylistIntent create when writeOutcomeMayBeUnknown && !string.IsNullOrWhiteSpace(create.RequestId) => _controller.ReportNamedPlaylistWriteOutcomeUnknown( "create-named-playlist", create.RequestId, message), LegacyCreateNamedPlaylistIntent create when !string.IsNullOrWhiteSpace(create.RequestId) => _controller.ReportNamedPlaylistCommandFailure( "create-named-playlist", create.RequestId, message), LegacyDeleteSelectedNamedPlaylistIntent delete when writeOutcomeMayBeUnknown && !string.IsNullOrWhiteSpace(delete.RequestId) => _controller.ReportNamedPlaylistWriteOutcomeUnknown( "delete-selected-named-playlist", delete.RequestId, message), LegacyDeleteSelectedNamedPlaylistIntent delete when !string.IsNullOrWhiteSpace(delete.RequestId) => _controller.ReportNamedPlaylistCommandFailure( "delete-selected-named-playlist", delete.RequestId, message), LegacyLoadNamedPlaylistByIdIntent load when !string.IsNullOrWhiteSpace(load.DefinitionId) => _controller.ReportNamedPlaylistCommandFailure( "load-named-playlist-by-id", load.DefinitionId, message), LegacySaveCurrentNamedPlaylistToIntent save when writeOutcomeMayBeUnknown && !string.IsNullOrWhiteSpace(save.DefinitionId) => _controller.ReportNamedPlaylistWriteOutcomeUnknown( "save-current-named-playlist-to", save.DefinitionId, message), LegacySaveCurrentNamedPlaylistToIntent save when !string.IsNullOrWhiteSpace(save.DefinitionId) => _controller.ReportNamedPlaylistCommandFailure( "save-current-named-playlist-to", save.DefinitionId, message), _ => _controller.ReportError(message) }; private async Task HandleIntentAsync( LegacyUiIntent intent, CancellationToken cancellationToken) { return intent switch { LegacyReadyIntent => _controller.Current, LegacySearchStocksIntent search => await _controller.SearchStocksAsync( search.Text, search.Trigger, cancellationToken), LegacySelectStockIntent selection => _controller.SelectStock(selection.ResultIndex), LegacyCutPointerDownIntent pointer => _controller.CutPointerDown( pointer.PhysicalIndex, pointer.Control, pointer.Shift, pointer.X, pointer.Y, pointer.TimestampMilliseconds, pointer.AllowAppend), LegacyCutKeyDownIntent key => _controller.CutKeyDown( key.Key, key.Control, key.Shift), LegacyClearCutSelectionIntent => _controller.ClearCutSelection(), LegacyDropSelectedCutsIntent => _controller.DropSelectedCutsOnPlaylist(), LegacySetPlaylistEnabledIntent enabled => _controller.SetPlaylistRowEnabled(enabled.RowId, enabled.Enabled), LegacySetAllPlaylistEnabledIntent enabled => _controller.SetAllPlaylistRowsEnabled(enabled.Enabled), LegacySelectPlaylistRowIntent selection => _controller.SelectPlaylistRow( selection.RowId, selection.Control, selection.Shift), LegacyActivatePlaylistRowIntent activation => _controller.ActivatePlaylistRow(activation.RowId), LegacyMoveSelectedPlaylistRowsIntent move => _controller.MoveSelectedPlaylistRows(move.Direction), LegacyReorderPlaylistRowsIntent reorder => _controller.ReorderPlaylistRows( reorder.SourceRowId, reorder.TargetRowId, reorder.Position), LegacyDeleteSelectedPlaylistRowsIntent => _controller.DeleteSelectedPlaylistRows(), LegacyDeleteAllPlaylistRowsIntent => _controller.DeleteAllPlaylistRows(), LegacyToggleActivePlaylistEnabledIntent => _controller.ToggleActivePlaylistRowEnabled(), LegacySelectPlaylistBoundaryIntent boundary => SelectPlaylistBoundaryAndStopRefresh(boundary.Last), LegacySelectTabIntent tab => await _controller.SelectTabAsync(tab.TabId, cancellationToken), LegacySwapTabsIntent tabs => await _controller.SwapTabsAsync( tabs.Source, tabs.Target, cancellationToken), LegacyHoverSwapTabsIntent tabs => await _controller.SwapTabsAtExpectedPositionsAsync( tabs.Source, tabs.Target, tabs.ExpectedSourceIndex, tabs.ExpectedTargetIndex, cancellationToken), LegacySetMovingAveragesIntent movingAverages => _controller.SetMovingAverages(movingAverages.Ma5, movingAverages.Ma20), LegacyActivateFixedActionIntent action => await _controller.ActivateFixedActionAsync( action.ActionId, cancellationToken), LegacyActivateFixedSectionIntent section => await _controller.ActivateFixedSection( section.SectionIndex, cancellationToken), LegacySelectIndustryIntent industry => _controller.SelectIndustry(industry.Index, industry.DoubleClick), LegacySetIndustryComparisonTextIntent edit => _controller.SetIndustryComparisonText(edit.Slot, edit.Value), LegacySwapIndustriesIntent => _controller.SwapIndustries(), LegacyActivateIndustryActionIntent action => _controller.ActivateIndustryAction(action.ActionId), LegacyActivateIndustrySectionIntent section => _controller.ActivateIndustrySection(section.SectionIndex), LegacySelectOverseasFixedIndexIntent selection => _controller.SelectOverseasFixedIndex(selection.TargetId), LegacySearchOverseasIndustriesIntent search => await _controller.SearchOverseasIndustriesAsync( search.Query, cancellationToken), LegacySearchOverseasStocksIntent search => await _controller.SearchOverseasStocksAsync( search.Query, cancellationToken), LegacySelectOverseasIndustryIntent selection => _controller.SelectOverseasIndustry(selection.SelectionId), LegacySelectOverseasStockIntent selection => _controller.SelectOverseasStock(selection.SelectionId), LegacyActivateOverseasActionIntent action => _controller.ActivateOverseasAction(action.ActionId), LegacySearchComparisonDomesticIntent search => await _controller.SearchComparisonDomesticAsync( search.Query, cancellationToken), LegacySearchComparisonWorldIntent search => await _controller.SearchComparisonWorldAsync( search.Query, cancellationToken), LegacySelectComparisonDomesticIntent selection => _controller.SelectComparisonDomesticResult(selection.SelectionId), LegacySelectComparisonWorldIntent selection => _controller.SelectComparisonWorldResult(selection.SelectionId), LegacySelectComparisonFixedIntent selection => _controller.SelectComparisonFixedTarget(selection.TargetId), LegacyChooseComparisonDomesticIntent selection => _controller.ChooseComparisonDomesticResult(selection.SelectionId), LegacyChooseComparisonWorldIntent selection => _controller.ChooseComparisonWorldResult(selection.SelectionId), LegacyChooseComparisonFixedIntent selection => _controller.ChooseComparisonFixedTarget(selection.TargetId), LegacySwapComparisonTargetsIntent => _controller.SwapComparisonTargets(), LegacyClearComparisonTargetsIntent => _controller.ClearComparisonTargets(), LegacyAddComparisonPairIntent => await _controller.AddComparisonPairAsync(cancellationToken), LegacySelectComparisonPairIntent selection => _controller.SelectComparisonPair(selection.RowId), LegacyMoveComparisonPairIntent move => await _controller.MoveComparisonPairAsync( move.Direction, cancellationToken), LegacyDeleteSelectedComparisonPairIntent => await _controller.DeleteComparisonPairAsync( all: false, cancellationToken), LegacyDeleteAllComparisonPairsIntent => await _controller.DeleteComparisonPairAsync( all: true, cancellationToken), LegacyActivateComparisonActionIntent action => _controller.ActivateComparisonAction(action.ActionId), LegacyActivateComparisonSectionIntent section => _controller.ActivateComparisonSection(section.SectionIndex), LegacySearchThemesIntent search => await _controller.SearchThemesAsync( search.Query, cancellationToken), LegacySelectThemeIntent selection => await _controller.SelectThemeAsync( selection.ResultId, cancellationToken), LegacyActivateThemeResultIntent activation => await _controller.ActivateThemeResultAsync( activation.ResultId, cancellationToken), LegacySetThemeSortIntent sort => _controller.SetThemeSort(sort.SortId), LegacyActivateThemeActionIntent action => _controller.ActivateThemeAction(action.ActionId), LegacySearchExpertsIntent search => await _controller.SearchExpertsAsync(search.Query, cancellationToken), LegacySelectExpertIntent selection => await _controller.SelectExpertAsync( selection.SelectionId, cancellationToken), LegacyActivateExpertActionIntent action => _controller.ActivateExpertAction(action.ActionId), LegacySearchTradingHaltsIntent search => await _controller.SearchTradingHaltsAsync( search.Query, cancellationToken), LegacySelectTradingHaltIntent selection => _controller.SelectTradingHalt(selection.SelectionId), LegacyToggleTradingHaltSortIntent => _controller.ToggleTradingHaltSort(), LegacyActivateTradingHaltActionIntent action => _controller.ActivateTradingHaltAction(action.ActionId), LegacyOpenOperatorCatalogIntent catalog => await _controller.OpenOperatorCatalogAsync( catalog.Entity, cancellationToken), LegacyBeginUc4ThemeCatalogIntent => await _controller.BeginThemeCatalogFromUc4Async(cancellationToken), LegacyEditUc4SelectedThemeIntent => await _controller.EditSelectedThemeCatalogAsync(cancellationToken), LegacyDeleteUc4SelectedThemeIntent => await _controller.DeleteSelectedThemeCatalogAsync(cancellationToken), LegacyBeginUc6ExpertCatalogIntent => await _controller.BeginExpertCatalogFromUc6Async(cancellationToken), LegacyEditUc6SelectedExpertIntent => await _controller.EditSelectedExpertCatalogAsync(cancellationToken), LegacyDeleteUc6SelectedExpertIntent => await _controller.DeleteSelectedExpertCatalogAsync(cancellationToken), LegacyCloseOperatorCatalogIntent => await _controller.CloseOperatorCatalogAsync(cancellationToken), LegacySearchOperatorCatalogIntent search => await _controller.SearchOperatorCatalogAsync( search.Query, search.NxtSession, cancellationToken), LegacySelectOperatorCatalogResultIntent selection => await _controller.SelectOperatorCatalogResultAsync( selection.ResultId, cancellationToken), LegacyBeginCreateThemeCatalogIntent create => await _controller.BeginCreateThemeCatalogAsync( create.Market, cancellationToken), LegacyBeginCreateExpertCatalogIntent => await _controller.BeginCreateExpertCatalogAsync(cancellationToken), LegacySearchOperatorCatalogStocksIntent search => await _controller.SearchOperatorCatalogStocksAsync( search.Query, cancellationToken), LegacySelectOperatorCatalogStockIntent selection => _controller.SelectOperatorCatalogStock(selection.ResultId), LegacyAddOperatorCatalogStockIntent add => _controller.AddOperatorCatalogStock(add.BuyAmount), LegacyActivateOperatorCatalogStockIntent activation => _controller.ActivateOperatorCatalogStock( activation.ResultId, activation.BuyAmount), LegacyMoveOperatorCatalogDraftRowIntent move => _controller.MoveOperatorCatalogDraftRow(move.RowId, move.Direction), LegacyReorderOperatorCatalogDraftRowIntent reorder => _controller.ReorderOperatorCatalogDraftRow( reorder.SourceRowId, reorder.TargetRowId, reorder.Position), LegacyRemoveOperatorCatalogDraftRowIntent remove => _controller.RemoveOperatorCatalogDraftRow(remove.RowId), LegacySelectOperatorCatalogDraftRowIntent selection => _controller.SelectOperatorCatalogDraftRow(selection.RowId), LegacyRemoveActiveOperatorCatalogDraftRowIntent => _controller.RemoveActiveOperatorCatalogDraftRow(), LegacyClearOperatorCatalogDraftRowsIntent => _controller.ClearOperatorCatalogDraftRows(), LegacySetOperatorCatalogDraftBuyAmountIntent amount => _controller.SetOperatorCatalogDraftBuyAmount( amount.RowId, amount.BuyAmount), LegacyCheckOperatorCatalogThemeNameIntent check => await _controller.CheckOperatorCatalogThemeNameAsync( check.Name, cancellationToken), LegacySaveOperatorCatalogIntent save => await _controller.SaveOperatorCatalogAsync( save.Name, cancellationToken), LegacyConfirmNativeDialogIntent => await _controller.ConfirmDialogAsync(cancellationToken), LegacyCancelNativeDialogIntent => _controller.CancelDialog(), LegacyRefreshNamedPlaylistsIntent refresh => await RefreshNamedPlaylistsForDispatchAsync( refresh.RequestId, cancellationToken), LegacySelectNamedPlaylistIntent selection => SelectNamedPlaylistForDispatch( selection.DefinitionId, selection.RequestId), LegacyLoadSelectedNamedPlaylistIntent => await _controller.LoadSelectedNamedPlaylistAsync(cancellationToken), LegacyLoadNamedPlaylistByIdIntent load => await _controller.LoadNamedPlaylistByIdAsync( load.DefinitionId, cancellationToken), LegacyCreateNamedPlaylistIntent create => await CreateNamedPlaylistForDispatchAsync( create.Title, create.RequestId, cancellationToken), LegacySaveCurrentNamedPlaylistIntent => await _controller.SaveCurrentNamedPlaylistAsync(cancellationToken), LegacySaveCurrentNamedPlaylistToIntent save => await _controller.SaveCurrentNamedPlaylistToAsync( save.DefinitionId, cancellationToken), LegacyDeleteSelectedNamedPlaylistIntent delete => await DeleteNamedPlaylistForDispatchAsync( delete.RequestId, cancellationToken), LegacyOpenManualFinancialIntent manual => await _controller.OpenManualFinancialAsync( manual.Screen, cancellationToken), LegacyCloseManualFinancialIntent => _controller.CloseManualFinancial(), LegacySearchManualFinancialIntent search => await _controller.SearchManualFinancialAsync( search.Query, cancellationToken), LegacyFindManualFinancialIntent find => await _controller.FindManualFinancialAsync( find.Query, find.Direction, find.Generation, cancellationToken), LegacyToggleManualFinancialNameSortIntent sort => _controller.ToggleManualFinancialNameSort(sort.Generation), LegacyLoadManualFinancialIntent load => await _controller.LoadManualFinancialAsync( load.ResultId, cancellationToken), LegacyActivateManualFinancialResultIntent activation => await _controller.ActivateManualFinancialResultAsync( activation.ResultId, cancellationToken), LegacyBeginManualFinancialCreateIntent => _controller.BeginManualFinancialCreate(), LegacySearchManualFinancialStocksIntent search => await _controller.SearchManualFinancialStocksAsync( search.StockName, cancellationToken), LegacySelectManualFinancialStockIntent selection => _controller.SelectManualFinancialStock(selection.ResultId), LegacySaveManualFinancialIntent save => await _controller.SaveManualFinancialAsync( save.Record, cancellationToken), LegacyDeleteManualFinancialIntent => await _controller.DeleteManualFinancialAsync( all: false, cancellationToken), LegacyDeleteAllManualFinancialIntent => await _controller.DeleteManualFinancialAsync( all: true, cancellationToken), LegacyActivateManualFinancialActionIntent action => _controller.ActivateManualFinancialAction(action.ActionId), LegacyOpenManualListIntent manual => manual.Screen == LegacyManualListScreen.NetSell ? await _controller.OpenManualNetSellAsync( manual.Audience, cancellationToken) : await _controller.OpenManualViAsync(cancellationToken), LegacyCloseManualListIntent => _controller.CloseManualLists(), LegacyRefreshManualListIntent => await _controller.RefreshManualListsAsync(cancellationToken), LegacySetManualNetSellCellIntent cell => _controller.SetManualNetSellCell(cell.RowId, cell.Field, cell.Value), LegacySaveManualNetSellIntent => await _controller.SaveManualNetSellAsync(cancellationToken), LegacyAddManualNetSellCutIntent => _controller.AddManualNetSellPlaylistRow(), LegacySearchManualViIntent search => await _controller.SearchManualViAsync(search.Query, cancellationToken), LegacySelectManualViResultIntent result => _controller.SelectManualViSearchResult(result.ResultId), LegacyAddManualViResultIntent result => _controller.AddManualViSearchResult(result.ResultId), LegacyMoveManualViItemIntent move => _controller.MoveManualViItem(move.ItemId, move.Direction), LegacyDeleteManualViItemIntent item => _controller.DeleteManualViItem(item.ItemId), LegacyDeleteAllManualViItemsIntent => _controller.DeleteAllManualViItems(), LegacySaveManualViIntent => await _controller.SaveManualViAsync(cancellationToken), LegacyAddManualViCutIntent => _controller.AddManualViPlaylistRow(), LegacyConfirmManualListIntent => await _controller.ConfirmManualListsAsync(cancellationToken), LegacyImportManualListsIntent => await _controller.ImportLegacyManualListsAsync(cancellationToken), LegacyGateAPrepareIntent prepare => await ExecuteGateAPrepareAsync( prepare.Capability, cancellationToken), LegacyExecutePlayoutIntent playout => await ExecuteOperatorPlayoutAsync( playout.Command, cancellationToken), LegacySetFadeDurationIntent fade => await SetFadeDurationAsync(fade.Duration, cancellationToken), LegacyToggleBackgroundIntent => await ToggleBackgroundAsync(cancellationToken), LegacyChooseBackgroundIntent => await ChooseBackgroundAsync(cancellationToken), LegacyDismissDialogIntent => _controller.DismissDialog(), _ => _controller.ReportError("지원하지 않는 화면 요청입니다.") }; } private async Task RefreshNamedPlaylistsForDispatchAsync( string requestId, CancellationToken cancellationToken) { var state = await _controller.RefreshNamedPlaylistsAsync(cancellationToken); var succeeded = state.NamedPlaylist?.ReadStatus == LegacyNamedPlaylistReadStatus.Ready; return _controller.CompleteNamedPlaylistDispatch( "refresh-named-playlists", requestId, succeeded); } private LegacyOperatorSnapshot SelectNamedPlaylistForDispatch( string definitionId, string requestId) { var state = _controller.SelectNamedPlaylist(definitionId); var succeeded = string.Equals( state.NamedPlaylist?.SelectedDefinitionId, definitionId, StringComparison.Ordinal); return _controller.CompleteNamedPlaylistDispatch( "select-named-playlist", requestId, succeeded); } private async Task CreateNamedPlaylistForDispatchAsync( string title, string requestId, CancellationToken cancellationToken) { var previousRevision = _controller.Current.NamedPlaylist?.Revision ?? -1; var state = await _controller.CreateNamedPlaylistAsync(title, cancellationToken); var succeeded = (state.NamedPlaylist?.Revision ?? -1) > previousRevision && IsCommittedNamedPlaylistMutation(state.NamedPlaylist?.LastMutationOutcome); return _controller.CompleteNamedPlaylistDispatch( "create-named-playlist", requestId, succeeded); } private async Task DeleteNamedPlaylistForDispatchAsync( string requestId, CancellationToken cancellationToken) { var previousRevision = _controller.Current.NamedPlaylist?.Revision ?? -1; var state = await _controller.DeleteSelectedNamedPlaylistAsync(cancellationToken); var succeeded = (state.NamedPlaylist?.Revision ?? -1) > previousRevision && IsCommittedNamedPlaylistMutation(state.NamedPlaylist?.LastMutationOutcome); return _controller.CompleteNamedPlaylistDispatch( "delete-selected-named-playlist", requestId, succeeded); } private static bool IsCommittedNamedPlaylistMutation( LegacyNamedPlaylistMutationOutcome? outcome) => outcome is LegacyNamedPlaylistMutationOutcome.CommittedFresh or LegacyNamedPlaylistMutationOutcome.CommittedOptimistic; private sealed class LaunchDatabaseMutationAuthorization : IDatabaseMutationAuthorization { private readonly PlayoutLaunchAuthorization _authorization; public LaunchDatabaseMutationAuthorization( PlayoutLaunchAuthorization authorization) { _authorization = authorization ?? throw new ArgumentNullException(nameof(authorization)); } public bool IsAuthorized(DatabaseMutationAuthorizationRequest request) { ArgumentNullException.ThrowIfNull(request); return _authorization.AllowsDatabaseWrites; } } private void PostState(LegacyOperatorSnapshot snapshot) { if (!_closing && _webViewReady && Browser.CoreWebView2 is not null) { // SystemInformation.DragSize was evaluated on every original // ListView.MouseDown. Refreshing at every native-to-web state boundary, // in addition to the settings/DPI message hook, prevents a startup-only // value from surviving a runtime Windows preference change. RefreshInteractionMetrics(); Browser.CoreWebView2.PostWebMessageAsJson( LegacyBridgeProtocol.SerializeState( snapshot, _intentBusy, CreatePlayoutSnapshot(), interactionMetrics: _interactionMetrics)); } } private void AttachInteractionMetricsRefresh() { if (_windowSubclassAttached || _windowHandle == 0) { return; } try { _windowSubclassAttached = SetWindowSubclass( _windowHandle, _windowSubclassProcedure, InteractionMetricsSubclassId, 0); } catch (DllNotFoundException) { } catch (EntryPointNotFoundException) { } catch (BadImageFormatException) { } catch (PlatformNotSupportedException) { } } private void DetachInteractionMetricsRefresh() { if (!_windowSubclassAttached || _windowHandle == 0) { return; } try { RemoveWindowSubclass( _windowHandle, _windowSubclassProcedure, InteractionMetricsSubclassId); } catch (DllNotFoundException) { } catch (EntryPointNotFoundException) { } catch (BadImageFormatException) { } catch (PlatformNotSupportedException) { } finally { _windowSubclassAttached = false; } } private nint OnWindowSubclassMessage( nint windowHandle, uint message, nuint wParam, nint lParam, nuint subclassId, nuint referenceData) { if (message is WmSettingChange or WmDisplayChange or WmDpiChanged) { QueueInteractionMetricsRefresh(); } return DefSubclassProc(windowHandle, message, wParam, lParam); } private void QueueInteractionMetricsRefresh() { if (_closing || _interactionMetricsRefreshQueued) { return; } _interactionMetricsRefreshQueued = true; if (!DispatcherQueue.TryEnqueue( Microsoft.UI.Dispatching.DispatcherQueuePriority.High, () => { _interactionMetricsRefreshQueued = false; if (_closing) { return; } RefreshInteractionMetrics(); if (_webViewReady) { PostState(_controller.Current); } })) { _interactionMetricsRefreshQueued = false; } } private void RefreshInteractionMetrics() { _interactionMetrics = ReadSystemInteractionMetrics(); } private LegacyInteractionMetrics ReadSystemInteractionMetrics() { try { // GetSystemMetrics returns physical pixels and is not DPI-aware. // Asking the DPI-aware API for its 96-DPI values produces the same // logical DIP coordinate space used by XAML. The bridge also carries // XamlRoot.RasterizationScale so JavaScript can account for a zoom // value retained by an older WebView profile without private API use. return LegacyInteractionMetrics.FromPixelsAtDpi( GetSystemMetricsForDpi( SmCxDrag, LegacyInteractionMetrics.CssPixelsPerInch), GetSystemMetricsForDpi( SmCyDrag, LegacyInteractionMetrics.CssPixelsPerInch), LegacyInteractionMetrics.CssPixelsPerInch, ReadHostRasterizationScale()); } catch (DllNotFoundException) { return CreateFallbackInteractionMetrics(); } catch (EntryPointNotFoundException) { return CreateFallbackInteractionMetrics(); } catch (BadImageFormatException) { return CreateFallbackInteractionMetrics(); } catch (PlatformNotSupportedException) { return CreateFallbackInteractionMetrics(); } } private LegacyInteractionMetrics CreateFallbackInteractionMetrics() => LegacyInteractionMetrics.Default with { HostRasterizationScale = ReadHostRasterizationScale() }; private double ReadHostRasterizationScale() { var scale = Root.XamlRoot?.RasterizationScale ?? 1d; return double.IsFinite(scale) && scale > 0d ? scale : 1d; } [DllImport("user32.dll", ExactSpelling = true)] private static extern int GetSystemMetricsForDpi(int nIndex, uint dpi); [DllImport("comctl32.dll", ExactSpelling = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool SetWindowSubclass( nint windowHandle, WindowSubclassProcedure subclassProcedure, nuint subclassId, nuint referenceData); [DllImport("comctl32.dll", ExactSpelling = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool RemoveWindowSubclass( nint windowHandle, WindowSubclassProcedure subclassProcedure, nuint subclassId); [DllImport("comctl32.dll", ExactSpelling = true)] private static extern nint DefSubclassProc( nint windowHandle, uint message, nuint wParam, nint lParam); [UnmanagedFunctionPointer(CallingConvention.Winapi)] private delegate nint WindowSubclassProcedure( nint windowHandle, uint message, nuint wParam, nint lParam, nuint subclassId, nuint referenceData); private void OnProcessFailed(object? sender, CoreWebView2ProcessFailedEventArgs args) { ObserveOperatorMutationQuarantine( _controller.InvalidateOperatorCatalogBrowserCorrelation()); if (!_closing) { ShowError("WebView2 프로세스가 중단되었습니다.", args.ProcessFailedKind.ToString()); } } private void ShowError(string title, string message) { if (_closing) { return; } ErrorBar.Title = title; ErrorBar.Message = message; ErrorBar.IsOpen = true; } private void OnClosed(object sender, WindowEventArgs args) { if (_closing) { return; } _closing = true; ObserveOperatorMutationQuarantine( _controller.InvalidateOperatorCatalogBrowserCorrelation()); Root.Loaded -= OnRootLoaded; Closed -= OnClosed; DetachCloseConfirmation(); DetachInteractionMetricsRefresh(); _lifetimeCancellation.Cancel(); CancelActiveDatabaseHealthCheck(); ShutdownPlayoutRuntime(); ShutdownManualListsRuntime(); DataQueryExecutor.Reset(); if (Browser.CoreWebView2 is not null) { Browser.CoreWebView2.NavigationStarting -= OnNavigationStarting; Browser.CoreWebView2.NavigationCompleted -= OnNavigationCompleted; Browser.CoreWebView2.NewWindowRequested -= OnNewWindowRequested; Browser.CoreWebView2.WebMessageReceived -= OnWebMessageReceived; Browser.CoreWebView2.ProcessFailed -= OnProcessFailed; } _webViewReady = false; // These process-lifetime guards deliberately remain undisposed. An in-flight // provider call may resume after Closed and must still be able to observe // cancellation and release the gate without racing disposed objects. } private sealed class UnavailableThemeCatalogPersistenceService : IThemeCatalogPersistenceService { private readonly string _message; public UnavailableThemeCatalogPersistenceService(string message) { _message = message; } public bool CanMutate => false; public Task ThemeNameExistsAsync( ThemeMarket market, string themeTitle, CancellationToken cancellationToken = default) => Task.FromException(new InvalidOperationException(_message)); public Task SuggestNextCodeAsync( ThemeMarket market, CancellationToken cancellationToken = default) => Task.FromException(new InvalidOperationException(_message)); public Task CreateAsync( ThemeCatalogDefinition definition, CancellationToken cancellationToken = default) => Task.FromException( new InvalidOperationException(_message)); public Task ReplaceAsync( ThemeSelectionIdentity expectedIdentity, string newTitle, IReadOnlyList items, CancellationToken cancellationToken = default) => Task.FromException( new InvalidOperationException(_message)); public Task DeleteAsync( ThemeSelectionIdentity expectedIdentity, CancellationToken cancellationToken = default) => Task.FromException( new InvalidOperationException(_message)); } private sealed class UnavailableExpertCatalogPersistenceService : IExpertCatalogPersistenceService { private readonly string _message; public UnavailableExpertCatalogPersistenceService(string message) { _message = message; } public bool CanMutate => false; public Task SuggestNextCodeAsync( CancellationToken cancellationToken = default) => Task.FromException(new InvalidOperationException(_message)); public Task CreateAsync( ExpertCatalogDefinition definition, CancellationToken cancellationToken = default) => Task.FromException( new InvalidOperationException(_message)); public Task ReplaceAsync( ExpertSelectionIdentity expectedIdentity, string newName, IReadOnlyList recommendations, CancellationToken cancellationToken = default) => Task.FromException( new InvalidOperationException(_message)); public Task DeleteAsync( ExpertSelectionIdentity expectedIdentity, CancellationToken cancellationToken = default) => Task.FromException( new InvalidOperationException(_message)); } private sealed class UnavailableStockMasterIdentityValidationService : IStockMasterIdentityValidationService { private readonly string _message; public UnavailableStockMasterIdentityValidationService(string message) { _message = message; } public Task> ValidateThemeItemsAsync( IReadOnlyList items, CancellationToken cancellationToken = default) => Task.FromException>( new InvalidOperationException(_message)); public Task> ValidateExpertRecommendationsAsync( IReadOnlyList recommendations, CancellationToken cancellationToken = default) => Task.FromException>( new InvalidOperationException(_message)); } private sealed class UnavailableOperatorCatalogSchemaValidationService : IOperatorCatalogSchemaValidationService { private readonly string _message; public UnavailableOperatorCatalogSchemaValidationService(string message) { _message = message; } public Task ValidateAsync( CancellationToken cancellationToken = default) => Task.FromException( new InvalidOperationException(_message)); } private sealed class UnavailableNamedPlaylistPersistenceService : INamedPlaylistPersistenceService { private readonly string _message; public UnavailableNamedPlaylistPersistenceService(string message) { _message = message; } public bool CanMutate => false; public Task ListAsync( int maximumResults = LegacyNamedPlaylistPersistenceService.DefaultMaximumDefinitions, CancellationToken cancellationToken = default) => Task.FromException( new InvalidOperationException(_message)); public Task SuggestNextProgramCodeAsync( CancellationToken cancellationToken = default) => Task.FromException(new InvalidOperationException(_message)); public Task LoadAsync( string programCode, CancellationToken cancellationToken = default) => Task.FromException( new InvalidOperationException(_message)); public Task CreateDefinitionAsync( string programCode, string title, CancellationToken cancellationToken = default) => Task.FromException(new InvalidOperationException(_message)); public Task ReplaceItemsAsync( string programCode, IReadOnlyList items, CancellationToken cancellationToken = default) => Task.FromException(new InvalidOperationException(_message)); public Task DeleteAsync( string programCode, CancellationToken cancellationToken = default) => Task.FromException(new InvalidOperationException(_message)); } private sealed class UnavailableStockLookup : ILegacyStockLookup { private readonly string _message; public UnavailableStockLookup(string message) { _message = message; } public Task SearchAsync( string query, CancellationToken cancellationToken = default) => Task.FromException( new InvalidOperationException(_message)); } private sealed class UnavailableIndustrySelectionService : IIndustrySelectionService { private readonly string _message; public UnavailableIndustrySelectionService(string message) { _message = message; } public Task> GetAsync( IndustryMarket market, CancellationToken cancellationToken = default) => Task.FromException>( new InvalidOperationException(_message)); } private sealed class UnavailableThemeSelectionService : IThemeSelectionService { private readonly string _message; public UnavailableThemeSelectionService(string message) { _message = message; } public Task SearchAsync( string query, ThemeSession nxtSession, int maximumResults = LegacyThemeSelectionService.DefaultMaximumResults, CancellationToken cancellationToken = default) => Task.FromException(new InvalidOperationException(_message)); public Task PreviewAsync( ThemeSelectionIdentity identity, int maximumItems = LegacyThemeSelectionService.DefaultMaximumPreviewItems, CancellationToken cancellationToken = default) => Task.FromException(new InvalidOperationException(_message)); } private sealed class UnavailableExpertSelectionService : IExpertSelectionService { private readonly string _message; public UnavailableExpertSelectionService(string message) { _message = message; } public Task SearchAsync( string query = "", int maximumResults = LegacyExpertSelectionService.DefaultMaximumResults, CancellationToken cancellationToken = default) => Task.FromException(new InvalidOperationException(_message)); public Task PreviewAsync( ExpertSelectionIdentity identity, int maximumItems = LegacyExpertSelectionService.DefaultMaximumPreviewItems, CancellationToken cancellationToken = default) => Task.FromException(new InvalidOperationException(_message)); } private sealed class UnavailableTradingHaltSelectionService : ITradingHaltSelectionService { private readonly string _message; public UnavailableTradingHaltSelectionService(string message) { _message = message; } public Task SearchAsync( string query = "", int maximumResults = LegacyTradingHaltSelectionService.DefaultMaximumResults, CancellationToken cancellationToken = default) => Task.FromException( new InvalidOperationException(_message)); } private sealed class UnavailableManualFinancialScreenService : IManualFinancialScreenService { private readonly string _message; public UnavailableManualFinancialScreenService(string message) { _message = message; } public bool CanMutate => false; public Task SearchAsync( ManualFinancialScreenKind screen, string query = "", int maximumResults = LegacyManualFinancialScreenService.DefaultMaximumResults, CancellationToken cancellationToken = default) => Task.FromException( new InvalidOperationException(_message)); public Task GetAsync( ManualFinancialIdentity identity, CancellationToken cancellationToken = default) => Task.FromException( new InvalidOperationException(_message)); public Task CreateAsync( ManualFinancialRecord record, CancellationToken cancellationToken = default) => Task.FromException( new InvalidOperationException(_message)); public Task UpdateAsync( ManualFinancialSnapshot expected, ManualFinancialRecord replacement, CancellationToken cancellationToken = default) => Task.FromException( new InvalidOperationException(_message)); public Task DeleteAsync( ManualFinancialSnapshot expected, CancellationToken cancellationToken = default) => Task.FromException( new InvalidOperationException(_message)); public Task DeleteAllAsync( ManualFinancialScreenKind screen, CancellationToken cancellationToken = default) => Task.FromException( new InvalidOperationException(_message)); } private sealed class UnavailableStockSearchService : IStockSearchService { private readonly string _message; public UnavailableStockSearchService(string message) { _message = message; } public Task SearchAsync( string query, int maximumResults = LegacyStockSearchService.DefaultMaximumResults, CancellationToken cancellationToken = default) => Task.FromException(new InvalidOperationException(_message)); } private sealed class UnavailableOverseasStockSearchService : IOverseasStockSearchService { private readonly string _message; public UnavailableOverseasStockSearchService(string message) { _message = message; } public Task SearchAsync( string query, int maximumResults = LegacyOverseasStockSearchService.DefaultMaximumResults, CancellationToken cancellationToken = default) => Task.FromException( new InvalidOperationException(_message)); } private sealed class UnavailableWorldStockSearchService : IWorldStockSearchService { private readonly string _message; public UnavailableWorldStockSearchService(string message) { _message = message; } public Task SearchAsync( string query, int maximumResults = LegacyWorldStockSearchService.DefaultMaximumResults, CancellationToken cancellationToken = default) => Task.FromException( new InvalidOperationException(_message)); } private sealed class UnavailableOverseasIndustryIndexSearchService : IOverseasIndustryIndexSearchService { private readonly string _message; public UnavailableOverseasIndustryIndexSearchService(string message) { _message = message; } public Task SearchAsync( string query, int maximumResults = LegacyOverseasIndustryIndexSearchService.DefaultMaximumResults, CancellationToken cancellationToken = default) => Task.FromException( new InvalidOperationException(_message)); } }