MVVM 패턴에서 usercontrol 내 객체는 코드비하인드에 넣어야하나요?

안녕하세요, WPF MVVM 패턴을 열심히 익히고 있는 개발자입니다.
이제 대략 개념적인것들은 잡힌것 같은데 궁금한게 하나 있어서 질문드립니다.

AForge란 라이브러리를 사용해서 webcam preview 테스트를 진행하는 중에 MVVM 패턴으로 적용하는 방식을 검색해보니 UserControl을 만들어서 VideoSource를 플레이하는 Sample code가 있었습니다.

Sample code에서는 .cs파일 내에 Camera Device객체를 선언하고 DependencyProperty를 이용해서 주고받아야할 데이터를 처리하는 듯 합니다.

그러다보니 단순 image 뿐 아니라 여러가지 camera setting 값들을 MainViewModel에서 Binding 해야 하다보니 Property들이 계속해서 추가 할 수 밖에 없었는데요.
제 짧은 생각으로는 애초에 Camera Device객체를 MainViewModel에서 선언하고 UserControl은 Preview image만 전달받아 처리할 수 있는게 아닌가 하는 생각이 있어서 질문드립니다.
굳이 view단에서 처리하지 않아도 될 로직들이 .cs파일에 있어도 되는건가? 라는 생각이 들었구요. 또 검색해보니 UserControl은 따로 Viewmodel을 두지 않는다 라는 내용을 봐서 MainViewModel에다가 객체를 두어 활용해야하지 않나 라는 생각이 들기도 해서 질문드립니다.

어찌어찌 배워가면서 Property도 추가해서 연결하고 바인딩하고 해서 구동상엔 문제가 없습니다만 그래도 이런 개념들은 확실히 하고 가야 다음에 프로젝트할 때 헷갈리지 않을 것 같아서요!

아래는 코드비하인드에서 객체로 사용하는 Sample code입니다.

[VideoWindow.xaml]
<UserControl x:Class="MVVMVideoControl.Video.VideoWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
            xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
            xmlns:local="clr-namespace:MVVMVideoControl.Video"
            xmlns:controls="clr-namespace:AForge.Controls;assembly=AForge.Controls"
            mc:Ignorable="d" 
            d:DesignHeight="300" d:DesignWidth="300"
            Loaded="OnLoaded"
            Unloaded="OnUnloaded">
   <Grid>
       <Grid x:Name="NoVideoSourceGrid" Background="LightGray">
           <Border BorderBrush="DimGray"
                   BorderThickness="1">
               <TextBlock x:Name="NoVideoSourceMessage"
                          VerticalAlignment="Center"
                          HorizontalAlignment="Center"
                          TextWrapping="Wrap"
                          FontSize="20"
                          FontWeight="Bold" />
           </Border>
       </Grid>
       <WindowsFormsHost x:Name="VideoSourceWindowsFormsHost"
                         Background="Transparent">
           <controls:VideoSourcePlayer x:Name="VideoSourcePlayer" />
       </WindowsFormsHost>
   </Grid>
</UserControl>
[VideoWindow.xaml.cs]
public partial class VideoWindow : UserControl
{
public static readonly DependencyProperty VideoBitmapProperty = DependencyProperty.Register
           ("VideoBitmap", typeof(ImageSource), typeof(VideoWindow), new PropertyMetadata(VideoBitmapPropertyChangedCallback));


private VideoCaptureDevice videoCaptureDevice;

public ImageSource VideoBitmap
        {
            get
            {
                return (ImageSource)this.GetValue(VideoBitmapProperty);
            }

            set
            {
                this.SetValue(VideoBitmapProperty, value);
            }
        }

[Capture Device 초기화 부분]
if (!GetVideoDevices.Any(item => item.UsbId.Equals(videoDeviceSourceId)))
{
    return;
}

this.videoCaptureDevice = new VideoCaptureDevice(videoDeviceSourceId);
this.videoCapabilities = this.videoCaptureDevice.VideoCapabilities;
this.snapshotCapabilities = this.videoCaptureDevice.SnapshotCapabilities;
                    

//Snapshot 활성화
this.videoCaptureDevice.ProvideSnapshots = true;

//이벤트 초기화
this.videoCaptureDevice.SnapshotFrame += new NewFrameEventHandler(NewFrameSnapshot);
this.videoCaptureDevice.NewFrame += new NewFrameEventHandler(NewFrameVideo);

//VideoFilterRange 초기화
InitializedVideoProcAmpRange();

//VideoCameraControlRange 초기화
InitializedVideoControlRange();

//messenger 테스트
var videoMessage = new VideoMessage()
{
    mediaCapabilitiesList = GetVideoCapabilities,
    mediaStillPinCapabilitiesList = GetSnapshotCapabilities
};
Messenger.Default.Send(videoMessage);


private void NewFrameVideo(object sender, NewFrameEventArgs eventArgs)
{
    BitmapImage bi;
    using (var bitmap = (Bitmap)eventArgs.Frame.Clone())
    {
        bi = new BitmapImage();
        bi.BeginInit();
        MemoryStream ms = new MemoryStream();
        bitmap.Save(ms, ImageFormat.Bmp);
        bi.StreamSource = ms;
        bi.CacheOption = BitmapCacheOption.OnLoad;
        bi.EndInit();
    }
    bi.Freeze();
    Dispatcher.BeginInvoke(new ThreadStart(delegate { this.VideoBitmap = bi; }));

}

}

[MainWindow.xaml]
<video:VideoWindow
                    x:Name="CameraVideoDeviceControl"
                    VideoPreviewWidth="Auto"
                    VideoPreviewHeight="Auto"
                    SnapshotSize="{Binding SelectedStillPinSize, Converter={StaticResource MediaCapabilitiesConverter}}"
                    VideoSize="{Binding SelectedVideoSize, Converter={StaticResource MediaCapabilitiesConverter}}"
                    VideoSourceId="{Binding SelectedVideoDevice, Converter={StaticResource MediaInformationConverter}}"
                    VideoProcAmpRange="{Binding VideoProcAmpRange, Mode=OneWayToSource}"
                    VideoAmp="{Binding VideoAmp}"
                    VideoControlRange="{Binding VideoControlRange, Mode=OneWayToSource}"
                    VideoControl="{Binding VideoControl}"
                    SnapshotBitmap="{Binding SnapshotFrame, Mode=TwoWay}"
                    VideoBitmap="{Binding VideoFrame, Mode=TwoWay}">
</video:VideoWindow>
좋아요 3

컨트롤의 속성은 DependencyProperty로 노출하는게 일반적입니다. 어느정도로 노출하느냐의 문제인 것 같습니다.

말씀주신 Camera Device 객체가 화면으로 나타나는(View에 종속적인) 컨트롤 객체가 아니라면 ViewModel에 들어가도 됩니다. 개념적으로 문제가 되지 않고, Camera Device를 컨트롤에 바인딩 하면 됩니다.

그런데 Camera Device 객체가 화면으로 나타나는 무엇이라면 View에 배치하는게 맞고요, ViewModel로의 바인딩을 어렵게 생각하지 마시고 일반 속성 처럼 바라봐서, 바인딩 해야 할 속성 목록이 많다면 적절한 클래스를 정의해서 하나의 속성으로 바인딩 하실 수도 있습니다.

좋아요 3

코드 비하인드를 사용하는 것이 MVVM 패턴을 직접적으로 훼손하는 것인 아니에요.
MVVM 패턴은 데이터를 기준으로 정형화된 Model 과 View 를 분리시키고 개체 간의 관계를 서술하는 것이지요.
코드 비하인드 사용여부는 중요하지 않습니다. 다만 제로 코드 바하인드를 컨벤션으로 가져갈 수는 있어요.

결국 MVVM 패턴의 가장 큰 목표는 View 의 추상화를 통해 이 방법론을 완성하는 것이죠.

따라서 UserControl 의 코드 비하인드에서 Model(혹은 ViewModel) 의 의존성이 발생하는가
이것만 잘 고려되면 됩니다.

예제에서는 VideoCaptureDevice 등등의 타입이 의존성을 가지는 타입인지만 확인하면 될 것 같습니다.

VideoCaptureDevice 요 타입이 Model 이나 ViewModel 에 해당하는 타입이라면
해당 타입들을 삭제하거나 변경했을 때 View 에 영향이 없어야겠지요.(View 와 완전히 분리될 거니까)
Model 이나 ViewModel 로 사용할 타입이 아니거나 의존적인 타입이 아니라
컨트롤 내부에서만 사용하는 데이터 형식이라면 상관없습니다.
컨트롤 내부에서 사용하는 모든 타입을 Model 이나 ViewModel 로 볼 수는 없으니까요.
(그렇다면 당연히 밖으로 노출되어서도 안 되겠지요. ~ㅂ~)

뭐 사실 이것도 엄격하게 MVVM 패턴을 지키는 방향으로 작성되었을 때에 해당하는 얘기예요.
대부분의 라이브러리는 사용성을 위해 만들어지기 때문에 보통은 사용성을 포기하면서까지 패턴을 지키지는 않아요.
그래서 이런 라이브러리를 사용할 때에 패턴을 깨는 내용이 들어가 있으면 좀 곤란한 상황이 연출되기도 하지요.

이건 쓰는 사람이 요리조리 잘 해먹어야하는 문제에 가깝겠네요. =ㅂ=;

좋아요 4

아, 한 가지 더 첨언하자면

지금 상황은 UserControl 이 아니라 CustomControl 이 더 적절해보이는데

UserControl 과 CustomControl 간의 차이와 쓰임에 대해 좀 더 명확히 하시면
문제가 쉽게 풀릴 수도 있지 않을까 합니다.

…라고 생각은 하지만 별 상관없을 수도 있겠군요. +_+

좋아요 2

샘플에서 보신 방식이 일반적으로 많이 사용하는 형식이긴 합니다.

그런데 처음 접하게 되면 잘 감이 오질 않아 아래와 같이 느끼실 수도 있을텐데요,

이런 문제를 접하게 되면 생각의 방향을 잠시 바꿔보는 것도 좋습니다.

예를들면 위와 같은 상황에서는 "사용자에게 제일 먼저 노출되는 것이 무엇인가?"로 들어가 보면,
'이 페이지에서는 카메라를 이용해 사진을 찍고 그 데이터를 수정/편집/저장한다’로 볼 수 있습니다.

그러면 사용자에게 위 문장에서 사진을 찍는 행위까지를 UserControl에서 처리하고 데이터를 수정/편집/저장하는 것을 ViewModel에서 처리한다고 이해할 수 있습니다.

위와 같이 처리하면 카메라와 관련된 옵션은 굳이 DependencyProperty로 빼지않고 그 페이지 내(*.xaml 또는 *.xaml.cs)에서 다 처리한다고 보고 사진 데이터를 전달할 Image만 DependencyProperty로 선언하여 사용할 수 있습니다.

좋아요 5

@dimohy @Greg.Lee @level120

좋은 설명 감사합니다.
제가 생각하고 있는 개념을 정리하는데도 큰 도움 되었습니다.

몇 번을 다시 읽어보게 되네요. :smile:

@도깨비 좋은 질문 감사합니다!

좋아요 3

답변 해주신 분들 모두 감사합니다!
딱 이해가 되는건 아니지만 실제 반영해보면은 이해가 될 것 같습니다.

답변에서 궁금한게 또 있습니다 ㅎㅎ;
View와의 종속적, 의존성 이야기가 있는데요. View, ViewMdel의 분리는 알고 있으니까 어떤 의미로 말씀하신건지 알겠습니다. 다만 뭔가 와닿지 않아서요. 제가 이해한 예로 설명드리면 맞는건지 알려주시면 감사하겠습니다.

현재 위 코드상에서는 VideoSourcePlayer 라는 WindowsFormsHost 를 쓰기 때문에 CameraDevice 객체가 View에 의존성이 있다 라고 볼수 있을 거 같은데요.(VideoWindow.xaml)
만약 이 부분을 그냥 Image Source 로 대체하고 아래 순서로 따라가도 의존성이 있다고 볼수 있을까요?

  1. ViewModel에서 CameraDevice 객체 선언
  2. VideoWindow(UserControl)에 DependencyProperty 선언 (Image Source)
  3. ViewModel에서 VideoWindow의 Property와 바인딩
  4. CameraDevice 이벤트 연결 (NewFrame)
  5. 이벤트 함수에서 ViewModel<->VideWindow 바인딩 되어 있는 Image 객체에 Frame 적용

그러면 NewFrame 이벤트가 들어올때마다 ViewModel → View의 Image 로 계속해서 Frame이 들어가면서 View가 처리 될 것 같은데요.

제가 원래 했던 질문을 조금 풀어쓴거긴 한데 이게 의존성이 있다고 볼수 있는걸까요? 제 생각은 그냥 Bitamp Frame만 연결되어 있다는 느낌이긴 합니다.

의견 부탁드립니다~!

좋아요 1

생각하신 구상 괜찮습니다.
좀 더 이상적(또는 최종 목표)로는 ViewModel이 특정 View에 종속되지 않게 해야 하는 점인데요,

View가 ViewModel의 의존성을 가지는 건 디자인 상 문제가 없지만,
ViewModel이 View에 의존성을 가지는 건 굳이 View와 ViewModel를 구분한 의미를 훼손합니다.

Visual Studio에서 View 프로젝트와 ViewModel 프로젝트를 별도로 구성하고,
View → ViewModel을 프로젝트 참조하도록 하고,
ViewModel 프로젝트는 View를 참조하지 않도록 구성해서 코드 진행하면 코드 구성이 원하시는 구성으로 점진적으로 진행되리라 생각합니다.

※ 여기서 고민해야 할 문제는 ViewModel에서 전달하는 Image Source를 View에 종속적이 되지 않게 해야 하는데요, 간단하면서 명확한 솔루션은 Image Source를 인터페이스(또는 추상클래스)로 캡슐화 하여 View로 전달하는 방법이 있습니다.

좋아요 3

@도깨비 의존성에 대한 말씀을 하셔서 좀 더 사족을 달아보자면요. =ㅂ=

이래서 MVVM 패턴을 실제로 적용하는 것은 생각보다 쉽지 않다는 거죠.

MVVM 패턴의 개념을 엄격히 지키는 방향을 추구한다면
View 와 ViewModel / Model 은 양방향 모두 의존성이 없어야합니다.

ViewModel 에 View 에 대한 의존성을 가지지 않게 하는 것은 다들 인지하지만
View 에 ViewModel 의 의존성을 분리시키는 것은 느슨하게 하는 경향이 있지요. ~ㅂ~!!

말하자면 View 와 ViewModel(Model 을 포함한)의 의존성 분리 정도에 따라
MVVM 패턴의 완성도가 달라진다고 보면 됩니다.

여기서 말하는 의존성이란,
디자인 타임에 발생하는 타입 의존성을 의미합니다. 따라서 엄격한 MVVM 패턴 구현에서는
ViewModel 이 View 타입에 대해 알거나 접근하면 안 되는 것처럼,
View 역시 ViewModel 에 대한 타입을 알거나 접근해서는 안 됩니다.

그럼 다시 원론으로 돌아와서

의존성 차원에서
예시로 든 CameraDevice 가 Model 인지 여부부터 정리해야할 거 같아요.

대체로 MVVM 패턴은

  1. 무언가 표현하고픈 데이터 영역을 Model 로 정형화 하고
  2. 이것을 어떻게 화면에 보이게 만들지 View 를 결정합니다.
  3. 이후 이 View 들을 추상화하여 ViewModel 을 만들고
  4. 추상화 과정에서 ViewModel 이 적절한 Model 을 사용하는 방식이 되지요.

videoCaptureDevice 가 화면에 표현되어야할 정형화된 데이터라면 충분히 Model 로 사용할 수 있을 겁니다.
그래서 videoCaptureDevice 가 Model 이라면
ViewModel 입장에서 Property 로 사용할 수 있습니다. 당연히 View에서 Binding 으로 연결할 수 있겠지요.
그런데 만약 View 가 Binding 전달될 videoCaptureDevice 를 사용하기 위해
DP 로 videoCaptureDevice 타입을 선언해 사용한다면
View에서 Model 의 타입에 접근하는 상황이 발생합니다. 패턴는 해치는 결과가 되지요. +ㅁ+!

또한 이런 방식이라면 Model 타입의 내용이 변경될 때 View 의 로직이 영향을 받는 결과가 생길 겁니다.
(이건 정말 MVVM 패턴의 존재 의의를 부정하는 결과가 되지요. ㅇㅅㅇ;:wink:

그래서 View 를 패턴에 엄격하게 설계를 할 때에는
View 의 속성들이 타입에 의존하지 않고 값에 의존하도록 설계합니다. 그 값을 DP 형태로 사용하는 것이죠.

이건 좀 더 확장되면
ViewModel 에서 Model 을 직접 사용할 것인가,
아니면 Model 의 값을 가져와서 ViewModel 의 property로 사용할 것인가 의 문제로도 이어집니다.

킁;;;;

아, 이거 얘기가 길어지는데요. 결론적으로

주제넘게 제가 @도깨비 님의 소스코드를 좀 핥아보자면요.
VideDeviceSourceId 를 DP 로 만들어 Binding 으로 전달받구요.(이미 그렇게 하고 있죠.)
전달 받은 VideDeviceSourceId 를 이용해 videoCaptureDevice 를 내부적으로만 생성하여 처리할 것 같습니다.
(방법론에서 @level120 님과 같은 의견이라도 보면 될 거 같습니다.)

아니면 별도의 VideoRenderService 같은 걸 만들고 (요건 Ioc 로 획득해 쓰는 걸루)
VideoWindow 가 생성될 때 전달된 VideDeviceSourceId 를 이용해 VideoRenderService 에 VideoCaptureDevice 관련 설정을 몽창 넣구요.
VideoWindow 에서 렌더링될 BitmapImage 만 전달받는 형식도 가능할 것 같습니다.
(아마 패턴 구현 측면에서는 가장 높은 완성도가 되겠지만, 번거로움도 높아지겠죠.)

제가 보기엔 VideoWindow 라는 View에서는 VideoCaptureDevice 라는 타입이 중요해 보이지 않는 것같거든요.
게다게 VideoCaptureDevice 가 Model 로서 의미가 있어보이지도 않습니다. (잘은 모르지만…;;; )
화면을 그리기 위한 설정이나 인프라 정도로 생각이 돼요.

그러니까 결론적으로는
어떤 것을 Model 로 분류(혹은 정의)를 할 것이냐 먼저 결정하는 것이 중요하다…
라는 얘깁니다.

써놓고 보니 엄청 기네요… 죄송… -ㅁ-;;;

좋아요 3

도움이 되실까 해서 관련 글을 작성해 보았습니다.

좋아요 2