DreamineVMS 1.0.0.0
Windows PC의 RTSP·USB 카메라 영상을 HLS로 변환해 원격 CCTV Viewer에 전달하는 데스크톱 에이전트입니다.
로딩중...
검색중...
일치하는것 없음
MainWindowViewModel.cs
이 파일의 문서화 페이지로 가기
1using Dreamine.Hybrid.Interfaces;
8using System.Collections.ObjectModel;
9using System.ComponentModel;
10using System.Runtime.CompilerServices;
11using System.Windows.Input;
12
14
31public sealed class MainWindowViewModel : INotifyPropertyChanged, IDisposable
32{
77 private readonly IDisposable _dashboardSubscription;
104 private string _statusMessage = "VMS application started.";
114
180 IVmsCameraRepository repository,
181 ICameraRuntimeStateService runtimeState,
182 ICameraStreamService streamService,
183 IVmsDashboardStateService dashboardStateService,
184 IHybridMessageBus messageBus,
185 AgentSettingsViewModel agentSettings)
186 {
187 _repository = repository ?? throw new ArgumentNullException(nameof(repository));
188 _runtimeState = runtimeState ?? throw new ArgumentNullException(nameof(runtimeState));
189 _streamService = streamService ?? throw new ArgumentNullException(nameof(streamService));
190 _dashboardStateService = dashboardStateService ?? throw new ArgumentNullException(nameof(dashboardStateService));
191 ArgumentNullException.ThrowIfNull(messageBus);
192 _agentSettings = agentSettings ?? throw new ArgumentNullException(nameof(agentSettings));
193
194 foreach (CameraDevice camera in _repository.GetAll())
195 {
196 CameraRuntimeState state = _runtimeState.GetState(camera.Id);
197 Cameras.Add(new CameraRowViewModel(camera, state));
198 }
199
200 SelectedCamera = Cameras.FirstOrDefault();
201 Logs.Add(new VmsLogItem { Message = "VMS application started." });
202
208
209 _runtimeState.StateChanged += OnCameraRuntimeStateChanged;
211 }
212
221 public event PropertyChangedEventHandler? PropertyChanged;
222
231 public event EventHandler? OpenLiveTabRequested;
232
241 public ObservableCollection<CameraRowViewModel> Cameras { get; } = new();
242
251 public ObservableCollection<VmsLogItem> Logs { get; } = new();
252
262 {
263 get => _selectedCamera;
264 set
265 {
266 if (ReferenceEquals(_selectedCamera, value))
267 {
268 return;
269 }
270
271 _selectedCamera = value;
273 (ConnectSelectedCameraCommand as AsyncRelayCommand)?.RaiseCanExecuteChanged();
274 (DisconnectSelectedCameraCommand as AsyncRelayCommand)?.RaiseCanExecuteChanged();
275 }
276 }
277
286 public string StatusMessage
287 {
288 get => _statusMessage;
289 private set
290 {
291 if (_statusMessage == value)
292 {
293 return;
294 }
295
296 _statusMessage = value;
298 }
299 }
300
309 public ICommand ConnectSelectedCameraCommand { get; }
310
319 public ICommand DisconnectSelectedCameraCommand { get; }
320
329 public ICommand StartAllCommand { get; }
330
339 public ICommand StopAllCommand { get; }
340
349 public ICommand ClearLogsCommand { get; }
350
359 public void Dispose()
360 {
361 _dashboardSubscription.Dispose();
362 _runtimeState.StateChanged -= OnCameraRuntimeStateChanged;
363
364 foreach (CameraRowViewModel row in Cameras)
365 {
366 row.Dispose();
367 }
368 Cameras.Clear();
369 }
370
387 private async Task ConnectSelectedCameraAsync()
388 {
390 if (selected is null)
391 {
392 return;
393 }
394
395 await _streamService.StartAsync(selected.Id);
396 AddLog($"Connect requested: {selected.Name}");
397 }
398
415 private async Task DisconnectSelectedCameraAsync()
416 {
418 if (selected is null)
419 {
420 return;
421 }
422
423 await _streamService.StopAsync(selected.Id);
424 AddLog($"Disconnect requested: {selected.Name}");
425 }
426
451 private async Task ConnectCameraAsync(string? cameraId)
452 {
453 if (string.IsNullOrWhiteSpace(cameraId))
454 {
455 AddLog("Connect request ignored: camera id is empty.");
456 return;
457 }
458
459 CameraRowViewModel? camera = Cameras.FirstOrDefault(row => string.Equals(row.Id, cameraId, StringComparison.OrdinalIgnoreCase));
460 if (camera is null)
461 {
462 AddLog($"Connect request ignored: camera not found. id={cameraId}");
463 return;
464 }
465
466 await _streamService.StartAsync(camera.Id).ConfigureAwait(false);
467 AddLog($"Dashboard connect requested: {camera.Name}");
468 }
469
494 private async Task DisconnectCameraAsync(string? cameraId)
495 {
496 if (string.IsNullOrWhiteSpace(cameraId))
497 {
498 AddLog("Disconnect request ignored: camera id is empty.");
499 return;
500 }
501
502 CameraRowViewModel? camera = Cameras.FirstOrDefault(row => string.Equals(row.Id, cameraId, StringComparison.OrdinalIgnoreCase));
503 if (camera is null)
504 {
505 AddLog($"Disconnect request ignored: camera not found. id={cameraId}");
506 return;
507 }
508
509 await _streamService.StopAsync(camera.Id).ConfigureAwait(false);
510 AddLog($"Dashboard disconnect requested: {camera.Name}");
511 }
512
529 private async Task StartAllAsync()
530 {
531 await _streamService.StartAllAsync();
532 AddLog("Start all cameras requested.");
533 }
534
551 private async Task StopAllAsync()
552 {
553 await _streamService.StopAllAsync();
554 AddLog("Stop all cameras requested.");
555 }
556
589 private async Task OnDashboardActionRequestedAsync(VmsDashboardActionRequestedMessage message, CancellationToken cancellationToken)
590 {
591 switch (message.Action)
592 {
594 _dashboardStateService.Refresh("Embedded dashboard refresh requested.");
595 AddLog("Embedded dashboard refresh requested.");
596 break;
597
599 // Server Dashboard is a browser/WebView route.
600 // Do not switch the WPF tab from here, otherwise the Server Dashboard tab and WPF Live tab appear to be mixed.
601 AddLog("Server dashboard requested Open Live. Use /live route in the server WebView.");
602 break;
603
605 await ConnectCameraAsync(message.CameraId).ConfigureAwait(false);
606 break;
607
609 await DisconnectCameraAsync(message.CameraId).ConfigureAwait(false);
610 break;
611
613 await StartAllAsync().ConfigureAwait(false);
614 break;
615
617 await StopAllAsync().ConfigureAwait(false);
618 break;
619
621 ClearLogs();
622 break;
623
624 default:
625 AddLog($"Dashboard action requested: {message.Action}");
626 break;
627 }
628 }
629
654 private void OnCameraRuntimeStateChanged(object? sender, CameraRuntimeState state)
655 {
656 // 상태 스토어 갱신은 IVmsDashboardStateService가 담당.
657 // 여기서는 WPF UI 로그/상태 메시지만 처리.
658 DispatchToUi(() => AddLogCore(state.LastMessage));
659 }
660
677 private void AddLog(string message)
678 {
679 DispatchToUi(() => AddLogCore(message));
680 }
681
698 private void AddLogCore(string message)
699 {
700 StatusMessage = message;
701 Logs.Insert(0, new VmsLogItem { Message = message });
702 while (Logs.Count > 300)
703 {
704 Logs.RemoveAt(Logs.Count - 1);
705 }
706 }
707
716 private void ClearLogs()
717 {
718 Logs.Clear();
719 AddLog("Logs cleared.");
720 }
721
731 {
732 DispatchToUi(() => OpenLiveTabRequested?.Invoke(this, EventArgs.Empty));
733 }
734
751 private static void DispatchToUi(Action action)
752 {
753 System.Windows.Application? app = System.Windows.Application.Current;
754 if (app is null)
755 {
756 return;
757 }
758
759 var dispatcher = app.Dispatcher;
760
761 // 종료 시퀀스 중에는 Invoke가 OperationCanceledException을 던지므로 무시합니다.
762 if (dispatcher.HasShutdownStarted || dispatcher.HasShutdownFinished)
763 {
764 return;
765 }
766
767 if (dispatcher.CheckAccess())
768 {
769 action();
770 }
771 else
772 {
773 try
774 {
775 dispatcher.Invoke(action);
776 }
777 catch (OperationCanceledException) { }
778 }
779 }
780
797 private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
798 {
799 PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
800 }
801}
802
811public sealed class RelayCommand : ICommand
812{
821 private readonly Action _execute;
830 private readonly Func<bool>? _canExecute;
831
864 public RelayCommand(Action execute, Func<bool>? canExecute = null)
865 {
866 _execute = execute ?? throw new ArgumentNullException(nameof(execute));
867 _canExecute = canExecute;
868 }
869
878 public event EventHandler? CanExecuteChanged;
879
904 public bool CanExecute(object? parameter) => _canExecute?.Invoke() ?? true;
905
922 public void Execute(object? parameter) => _execute();
923
933 {
934 System.Windows.Threading.Dispatcher? dispatcher =
935 System.Windows.Application.Current?.Dispatcher;
936
937 if (dispatcher is null || dispatcher.CheckAccess())
938 {
939 CanExecuteChanged?.Invoke(this, EventArgs.Empty);
940 return;
941 }
942
943 dispatcher.InvokeAsync(() => CanExecuteChanged?.Invoke(this, EventArgs.Empty));
944 }
945}
946
955public sealed class AsyncRelayCommand : ICommand
956{
965 private readonly Func<Task> _execute;
974 private readonly Func<bool>? _canExecute;
983 private bool _isRunning;
984
1017 public AsyncRelayCommand(Func<Task> execute, Func<bool>? canExecute = null)
1018 {
1019 _execute = execute ?? throw new ArgumentNullException(nameof(execute));
1020 _canExecute = canExecute;
1021 }
1022
1031 public event EventHandler? CanExecuteChanged;
1032
1057 public bool CanExecute(object? parameter) => !_isRunning && (_canExecute?.Invoke() ?? true);
1058
1075 public async void Execute(object? parameter)
1076 {
1077 if (!CanExecute(parameter))
1078 {
1079 return;
1080 }
1081
1082 try
1083 {
1084 _isRunning = true;
1086
1087 // WPF UI 컨텍스트를 유지하기 위해 ConfigureAwait(false) 사용 안 함.
1088 await _execute();
1089 }
1090 catch (Exception ex)
1091 {
1092 // 비동기 명령의 미처리 예외가 UI 스레드로 던져져 앱이 죽는 것을 막습니다.
1093 System.Diagnostics.Debug.WriteLine($"[AsyncRelayCommand] {ex}");
1094 }
1095 finally
1096 {
1097 _isRunning = false;
1099 }
1100 }
1101
1111 {
1112 System.Windows.Threading.Dispatcher? dispatcher =
1113 System.Windows.Application.Current?.Dispatcher;
1114
1115 if (dispatcher is null || dispatcher.CheckAccess())
1116 {
1117 CanExecuteChanged?.Invoke(this, EventArgs.Empty);
1118 return;
1119 }
1120
1121 dispatcher.InvokeAsync(() => CanExecuteChanged?.Invoke(this, EventArgs.Empty));
1122 }
1123}
void OnPropertyChanged([CallerMemberName] string? propertyName=null)
readonly IVmsDashboardStateService _dashboardStateService
readonly ICameraRuntimeStateService _runtimeState
ObservableCollection< CameraRowViewModel > Cameras
readonly ICameraStreamService _streamService
MainWindowViewModel(IVmsCameraRepository repository, ICameraRuntimeStateService runtimeState, ICameraStreamService streamService, IVmsDashboardStateService dashboardStateService, IHybridMessageBus messageBus, AgentSettingsViewModel agentSettings)
readonly AgentSettingsViewModel _agentSettings
void OnCameraRuntimeStateChanged(object? sender, CameraRuntimeState state)
async Task OnDashboardActionRequestedAsync(VmsDashboardActionRequestedMessage message, CancellationToken cancellationToken)
async Task ConnectCameraAsync(string? cameraId)
async Task DisconnectCameraAsync(string? cameraId)
ObservableCollection< VmsLogItem > Logs
RelayCommand(Action execute, Func< bool >? canExecute=null)
AsyncRelayCommand(Func< Task > execute, Func< bool >? canExecute=null)