(3) MVVM 패턴 적용하여 코드 개선_1

00·2025년 1월 19일

폴더 구조 변경

기존

OpenCvSharpProjects (프로젝트 루트)
├── App.Xaml
│ └── App.Xaml.cs
├── MainWindow.xaml
│ └── MainWindow.xaml.cs
├── top_left.png
└──bottom_right.png

변경

OpenCvSharpProjects (프로젝트 루트)
├── Models
│   └── GameInfo.cs
├── ViewModels
│   └── CameraViewModel.cs
├── Views
│   └── MainWindow.xaml
│   	└── MainWindow.xaml.cs
├── Converters
│   └── RectToVisibilityConverter.cs
├── App.xaml
│   └── App.xaml.cs
├── Resources
│	└──top_left.png
│	└── bottom_right.png
  • Models 폴더: MVVM 패턴의 Model에 해당한다.
    Model은 애플리케이션에서 사용되는 데이터와 비즈니스 로직을 담당한다. 예를 들어, 게임 정보, 이미지 처리 결과, 설정 등을 저장하는 클래스들이 Model에 속한다.
    따라서 GameInfo.cs와 같이 게임 정보를 저장하는 Model 클래스는 이 폴더에 저장한다.

  • ViewModels 폴더: MVVM 패턴의 ViewModel에 해당한다.
    ViewModel은 이름 그대로 View와 Model 사이의 중개자 역할을 수행하며, View에 필요한 데이터를 제공하고 View에서 발생하는 이벤트를 처리한다. 웹캠 이미지 캡처, 이미지 처리, 객체 인식, 게임 제어 등의 로직을 담당하는 클래스들이 ViewModel에 속한다.
    따라서CameraViewModel.cs와 같이 ViewModel 클래스는 이 폴더에 저장한다.

  • Views 폴더: MVVM 패턴의 View에 해당한다.
    View는 사용자 인터페이스(UI)를 담당하며, ViewModel에서 제공하는 데이터를 표시하고 사용자 입력을 받는ㄷ다. WPF 창 (.xaml 파일)들이 View에 속한다.
    따라서 MainWindow.xaml과 같이 View (WPF 창) 파일은 이 폴더에 저장한다.

  • Converters 폴더: Value Converter 클래스들을 저장하는 폴더다. Value Converter는 View에서 데이터를 표시하거나 사용자 입력을 처리할 때 데이터 형식을 변환하는 데 사용된다. 예를 들어, RectToVisibilityConverterRect 객체를 Visibility 값으로 변환한다.
    따라서RectToVisibilityConverter.cs와 같이 Value Converter 클래스는 이 폴더에 저장한다.

  • Resources 폴더: 엄밀히 말하면 MVVM 패턴의 구성 요소는 아니지만, View에서 사용하는 리소스 (이미지, 템플릿 등)를 저장하는 폴더로 쓰려고 만들었다.


MVVM 패턴을 적용하여 코드를 개선

1단계: Model 클래스 (GameInfo.cs) 작성

GameInfo 클래스는 게임 관련 정보를 저장하는 Model이다. 현재는 게임 화면 영역과 유저 위치 정보를 저장하도록 구현되어 있다. 필요에 따라 다른 게임 정보 (예: 객체 정보, 맵 정보)를 추가할 수 있다.
RectPoint 형식은 OpenCvSharp 네임스페이스에 정의되어 있으니까GameInfo.cs 파일의 맨 위에 using OpenCvSharp; 지시문을 추가해야 한다.

// Models/GameInfo.cs
using OpenCvSharp; // 추가

namespace OpenCvSharpProjects
{
    public class GameInfo
    {
        public Rect GameWindowRect { get; set; } // 게임 화면 영역
        public Point UserLocation { get; set; } // 유저 위치
    }
}

2단계: ViewModel 클래스 (CameraViewModel.cs) 작성

CameraViewModel 클래스는 웹캠 캡처, 이미지 처리, 객체 인식, 게임 제어 등의 로직을 담당한다. MVVM 패턴의 ViewModel에 해당하는 부분이다.

// ViewModels/CameraViewModel.cs
using OpenCvSharp;
using OpenCvSharp.WpfExtensions;
using System.Collections.Generic;
using System.IO;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;

namespace OpenCvSharpProjects
{
    public class CameraViewModel : ViewModelBase
    {
        private VideoCapture capture;
        private Mat frame;
        private GameInfo gameInfo;

        public CameraViewModel()
        {
            capture = new VideoCapture(0);
            frame = new Mat();
            gameInfo = new GameInfo();

            StartCaptureCommand = new RelayCommand(StartCapture);
            StopCaptureCommand = new RelayCommand(StopCapture);
        }

        // View에 바인딩할 속성
        public WriteableBitmap CameraImage
        {
            get { return frame.ToWriteableBitmap(); }
            set
            {
                frame = value.ToMat();
                OnPropertyChanged();
            }
        }

        public Rect GameWindowRect
        {
            get { return gameInfo.GameWindowRect; }
            set
            {
                gameInfo.GameWindowRect = value;
                OnPropertyChanged();
            }
        }

        // View에서 실행할 Command
        public ICommand StartCaptureCommand { get; private set; }
        public ICommand StopCaptureCommand { get; private set; }

        // 웹캠 캡처 시작
        private async void StartCapture()
        {
            CompositionTarget.Rendering += UpdateFrame;
        }

        // 웹캠 캡처 중지
        private void StopCapture()
        {
            CompositionTarget.Rendering -= UpdateFrame;
        }

        // 프레임 업데이트
        private async void UpdateFrame(object? sender, EventArgs e)
        {
            capture.Read(frame);

            if (!frame.Empty())
            {
                await Task.Run(() => ProcessImage(frame.Clone()));

                OnPropertyChanged("CameraImage");
            }
        }

        // 이미지 처리
        private void ProcessImage(Mat image)
        {
            // TODO: 게임 화면 영역 검출, 특정 객체 인식, 행동 결정 등의 로직 구현
            // 이 부분은 다음 단계에서 구현합니다.
        }
    }
}

코드 설명

멤버 변수

  • capture: VideoCapture 객체를 저장하는 변수이다. 웹캠에서 이미지를 캡처하는 데 사용된다.
  • frame: Mat 객체를 저장하는 변수이다. OpenCV에서 이미지를 표현하는 데 사용된다.
  • gameInfo: GameInfo 객체를 저장하는 변수이다. 게임 관련 정보를 저장하는 데 사용된다.

생성자 (CameraViewModel())

  • capture 변수를 초기화하여 웹캠을 엽니다.
  • frame 변수를 초기화하여 이미지를 저장할 Mat 객체를 생성합니다.
  • gameInfo 변수를 초기화하여 게임 정보를 저장할 GameInfo 객체를 생성합니다.
  • StartCaptureCommandStopCaptureCommandRelayCommand 객체로 초기화합니다. 각각 StartCapture 메서드와 StopCapture 메서드를 실행하는 Command이다.

속성

  • CameraImage: 웹캠에서 캡처한 이미지를 WriteableBitmap 형식으로 제공하는 속성이다. get 접근자는 frameWriteableBitmap으로 변환하여 반환하고, set 접근자는 WriteableBitmapMat으로 변환하여 frame에 저장하고 OnPropertyChanged() 메서드를 호출하여 속성 변경을 알립니다.
  • GameWindowRect: 게임 화면 영역을 나타내는 Rect 객체를 제공하는 속성이다. get 접근자는 gameInfo에서 GameWindowRect 값을 가져와 반환하고, set 접근자는 gameInfoGameWindowRect 값을 설정하고 OnPropertyChanged() 메서드를 호출하여 속성 변경을 알립니다.

Command

  • StartCaptureCommand: 웹캠 캡처를 시작하는 Command이다.
  • StopCaptureCommand: 웹캠 캡처를 중지하는 Command이다.

메서드

  • StartCapture(): CompositionTarget.Rendering 이벤트에 UpdateFrame 메서드를 등록하여 웹캠 캡처를 시작합니다.
  • StopCapture(): CompositionTarget.Rendering 이벤트에서 UpdateFrame 메서드를 제거하여 웹캠 캡처를 중지합니다.
  • UpdateFrame(): 웹캠에서 프레임을 읽어와 frame에 저장하고, ProcessImage 메서드를 비동기적으로 실행하여 이미지 처리를 수행합니다. 그리고 OnPropertyChanged("CameraImage")를 호출하여 CameraImage 속성 변경을 알립니다.
  • ProcessImage(): 이미지 처리를 수행하는 메서드이다. 현재는 주석으로 처리되어 있으며, 실제 구현은 다음 단계에서 진행될 예정입니다.

정리

이 ViewModel 클래스는 웹캠 캡처, 이미지 처리, 게임 정보 관리 등의 기능을 제공하며,
View와의 상호 작용을 위해 CameraImage, GameWindowRect 속성과 StartCaptureCommand, StopCaptureCommand Command를 제공한다.

CameraViewModel 클래스는 VideoCapture 객체를 사용하여 웹캠에서 프레임을 캡처하고, ProcessImage 메서드를 통해 이미지 처리를 수행한다. CameraImage 속성은 캡처된 이미지를 View에 표시하기 위한 WriteableBitmap 객체를 제공한다. GameWindowRect 속성은 게임 화면 영역을 나타내는 Rect 객체를 제공한다.
StartCaptureCommandStopCaptureCommand는 웹캠 캡처를 시작하고 중지하는 Command다.

오류 발생

'ViewModelBase' 형식 또는 네임스페이스 이름을 찾을 수 없습니다. 
 using 지시문 또는 어셈블리 참조가 있는지 확인하세요.

'RelayCommand' 형식 또는 네임스페이스 이름을 찾을 수 없습니다. 
 using 지시문 또는 어셈블리 참조가 있는지 확인하세요.

발생한 오류는 ViewModelBaseRelayCommand 클래스를 찾을 수 없다는 오류다.

1. ViewModelBase 및 RelayCommand 오류 해결

ViewModelBaseRelayCommand 클래스는 MVVM 패턴 구현을 돕는 클래스다. 이 오류는 해당 클래스들이 정의되지 않았거나, using 문을 통해 네임스페이스를 포함하지 않았기 때문에 발생한다.

  • (1) 직접 구현: ViewModelBase 클래스를 직접 구현하거나, .NET Community ToolkitObservableObject 클래스를 사용할 수 있다.

    • ViewModelBase.cs 파일 생성: ViewModels 폴더에 ViewModelBase.cs 파일을 생성하고 아래 코드를 추가했다.

      using System.ComponentModel;
      using System.Runtime.CompilerServices;
      
      namespace OpenCvSharpProjects
      {
          public class ViewModelBase : INotifyPropertyChanged
          {
              public event PropertyChangedEventHandler PropertyChanged;
      
              protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
              {
                  PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
              }
          }
      }
    • RelayCommand.cs 파일 생성: ViewModels 폴더에 RelayCommand.cs 파일을 생성하고 아래 코드를 추가했다.

      using System;
      using System.Windows.Input;
      
      namespace OpenCvSharpProjects
      {
          public class RelayCommand : ICommand
          {
              private readonly Action _execute;
              private readonly Func<bool> _canExecute;
      
              public RelayCommand(Action execute, Func<bool> canExecute = null)
              {
                  _execute = execute;
                  _canExecute = canExecute;
              }
      
              public event EventHandler CanExecuteChanged;
      
              public bool CanExecute(object parameter)
              {
                  return _canExecute == null || _canExecute();
              }
      
              public void Execute(object parameter)
              {
                  _execute();
              }
          }
      }
  • (2) .NET Community Toolkit 사용: NuGet 패키지 관리자에서 CommunityToolkit.Mvvm 패키지를 설치하고 CameraViewModel 클래스 정의를 다음과 같이 변경한다.

    using CommunityToolkit.Mvvm.ComponentModel;
    using CommunityToolkit.Mvvm.Input;
    
    public class CameraViewModel : ObservableObject // ViewModelBase 대신 ObservableObject 사용
    {
        // ... (나머지 코드) ...
    }

오류 발생

이 비동기 메서드에는 'await' 연산자가 없으며 메서드가 동시에 실행됩니다. 
'await' 연산자를 사용하여 비블로킹 API 호출을 대기하거나, 
'await Task.Run(...)'을 사용하여 백그라운드 스레드에서 CPU 바인딩된 작업을 수행하세요.

비동기 메서드에서 await 연산자를 사용하지 않았다는 경고다.

2. 비동기 메서드 경고 해결

StartCapture() 메서드는 비동기 메서드이지만 await 연산자를 사용하지 않아 경고가 발생했다. CompositionTarget.Rendering 이벤트는 UI 스레드에서 발생하기 때문에, StartCapture() 메서드 내에서 무거운 작업을 수행하면 UI가 응답하지 않게 된다. 이를 방지하기 위해 Task.Run()을 사용하여 무거운 작업을 백그라운드 스레드에서 실행해야 한다.

private async void StartCapture()
{
    await Task.Run(() => // 백그라운드 스레드에서 실행
    {
        // 무거운 작업 (예: 초기화 작업)
    });

    // UI 스레드에서 실행해야 하는 작업 (예: 이벤트 등록)
    Dispatcher.Invoke(() => 
    {
        CompositionTarget.Rendering += UpdateFrame;
    });
}

3. ViewModelBase 클래스 구현 위치

ViewModelBase 클래스를 직접 구현하거나 .NET Community ToolkitObservableObject 클래스를 사용하는 경우, ViewModels 폴더에 ViewModelBase.cs 파일을 생성하고 해당 파일에 클래스를 정의해야 한다.

4. RelayCommand 클래스 구현 위치

RelayCommand 클래스를 직접 구현하는 경우, ViewModels 폴더에 RelayCommand.cs 파일을 생성하고 해당 파일에 클래스를 정의해야 한다. .NET Community Toolkit을 사용하는 경우에는 RelayCommand 클래스가 라이브러리에 포함되어 있으므로 별도로 구현할 필요가 없다.

오류 발생

일관성 없는 액세스 가능성: 'ViewModelBase' 기본 클래스가 'CameraViewModel' 클래스보다 액세스하기 어렵습니다.

CameraViewModel 클래스가 ViewModelBase 클래스보다 접근 제한자가 더 넓기 때문에 발생. 즉, ViewModelBase 클래스의 접근 제한자가 CameraViewModel 클래스의 접근 제한자보다 더 제한적이라는 의미.

해결 방법

  • 접근 제한자 일치시키기: ViewModelBase 클래스와 CameraViewModel 클래스의 접근 제한자를 동일하게 설정한다. CameraViewModel 클래스는 public으로 선언되어 있으므로, ViewModelBase 클래스도 public으로 선언해야 한다.
// ViewModelBase.cs
public class ViewModelBase : INotifyPropertyChanged 
{
    // ...
}
  • .NET Community Toolkit의 ObservableObject 클래스 사용: .NET Community ToolkitObservableObject 클래스를 사용하는 경우, CameraViewModel 클래스에서 ViewModelBase 클래스 대신 ObservableObject 클래스를 상속받도록 변경합니다.
// ViewModels/CameraViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;
// ...

public class CameraViewModel : ObservableObject // ViewModelBase 대신 ObservableObject 사용
{
    // ...
}

이 경우 ViewModelBase.cs 파일은 필요하지 않습니다.

어떤 방법을 선택해야 할까?

  • ViewModelBase 클래스 직접 구현: 간단한 속성 변경 알림 기능만 필요한 경우 적합합니다.
  • .NET Community ToolkitObservableObject 클래스 사용: .NET Community Toolkit이 제공하는 다양한 MVVM 기능을 활용하고 싶은 경우 적합합니다.

주의 사항:

  • .NET Community Toolkit을 사용하려면 NuGet 패키지 관리자에서 CommunityToolkit.Mvvm 패키지를 설치해야 합니다.

0개의 댓글