WPF 다른 위치에서 생성된 개체 참조 질문

참고 강좌: https://youtu.be/PzP8mw7JUzI
소스코드: https://github.com/plane90/_Study_WPF_ModernDesign

강좌를 보고 소스코드를 따라 쳤습니다.
그리고 응용을 하려던 중 막혀서 질문드려봅니다.


MainViewModel은 MainWidow.xaml의 DataContext로 사용됩니다.

image
그리고 SideBar의 RadioButton은 RelayCommand를 통해
ContentControl의 'Content’를 설정합니다.

참고로 DisplayView.xaml와 HomeView.xmal은 UserControl이고
image
DispalyViewModel과 HomeViewModel은 ContentControl의 Content가 되는 개체입니다.
DataTemplate을 통해 각 ViewModel이 DisplayView 또는 HomeView를 연결해준 것이죠.

제가 하려던 것은 HomeViewModel.cs에 몇가지 데이터를 정의하고
그 데이터를 HomeView의 컨트롤 Source(ListBox 같은…)로 사용하는 것입니다.
그리고 해당 Source를 편집할 수 있고 다른 View를 다녀와도 그대로 보존이 되어있어야됩니다.

여기서 문제가 발생합니다.


HomeView에서 HomeViewModel의 데이터를 참조하기 위해
Xaml에서 해당 클래스를 DataContext로 정의했더니 매번 새로운 HomeViewModel 개체가 생성됐습니다.
그래서 다른 View를 다녀오면 편집한 데이터가 모두 날아가버리는것이죠.

HomeViewModel에서 Trace.WriteLine(this.GetHashCode()); 구문을 넣고 출력값을 봤더니 매번 다른 값이 출력됩니다. 즉, Xaml의 DataContext 속성에 의해 계속 생성자가 호출해버리는 겁니다.

여기서 궁금한 것은

  1. MainViewModel의 생성자에서 생성된 HomeVieModel 개체를 DataContext로 참조할 수 있는 방법이 있나요?

  2. 위 사항의 가능 여부에 상관없이 제가 MVVM을 잘못사용하고 있는 것인가요? 그리고 더 나은 Practice가 있을까요?

질문, 답변 모두 환영합니다.
감사합니다.

좋아요 2

IoC를 사용해서
뷰모델을 싱글턴 형식으로 관리하시면 해당 뷰가 생성될때
매번 뷰모델이 새로 생성되지는 않습니다.

mvvm관련 라이브러리를 사용하시면 이를 쉽게 제공하고 있습니다.

가령 mvvmlight에서는 위와 같은 처리를 SimpleIoC를 이용해 뷰모델 로케이터를 제공합니다.

+ 더 나아가 IoC를 활용한 DI(의존성 주입) 관련해서 키워드를 찾아 보시면 됩니다.

좋아요 5

무언가 착각하신 것 같습니다.

일단, GetHashCode 함수는 내부적으로 뜯어보면 아시겠지만 인스턴스의 고유 ID가 아닙니다.
그렇기 때문에 GetHashCode 값이 다르다고 이전과 다른 인스턴스라고 볼 수 없습니다.

ViewModel을 싱글톤으로 만들거나 다른 특수한 방법을 사용하지 않는 한, 일반적으로 DataContext에 있는 ViewModel은 xaml의 View와 생명주기를 같이합니다.

하나의 ViewModel 안에 여러 개의 ViewModel을 정의해서 같이 사용할 수도 있습니다.
스크린 샷에 있는 UI로 보았을 때, 하나의 MainViewModel에서 Home, Discovery, Feature 세 개의 ViewModel을 동시에 관리하는 것이 더 좋을 것 같아요.

그리고 가능한 Binding에는 UI와 관련없는 순수 데이터로만 취급하는 것이 좋습니다.
이 질문이 나온게 Binding으로 CurrentView를 사용하고 있기 때문이 아닌가 싶기도 해서요.

시간 상 코드를 본 것이 아니기 때문에 정확하지 않을 수도 있지만 참고가 될 수 있을 것 같습니다.

좋아요 4

키워드 제공해주셔서 너무 감사합니다
구글에 뭐라고 쳐야할지 막막했거든요 …ㅠ

좋아요 1

음 그렇군요 너무 MVVM이라는 틀에 갇혀서
특별한 방법이 있지 않을까 라고 생각했네요.

그런데 순수 데이터라는 것이 정확히 어떤 것인가요?
예를들어 MainViewModel은 단순히 초기화해주는 처리 뿐이지만
그래도 이런 경우는 순수데이터라고 볼 수 없나요?

좋아요 1

용어를 이상하게 쓰긴 했는데… 적합한 단어가 생각나질 않아서 :cry:

여기서는 UI와 관련없는 데이터를 말합니다.
Border, TextBox, Label, ContentView, VisualElement 같은 것들요.

이런 UI와 관련된 코드는 ViewModel 보다는 xaml.cs에서 작성하는 것이 더 관리하기 편합니다.

그런데 처음에는 햇갈리고 잘 와닿지 않을수도 있는데 Binding으로 UI 요소를 넣어도 동작은 하지만, 그러면 MVVM 구조를 사용함에 있어 약간 멀미나는 코드가 될 확률이 높거든요.

저의 경우 이런 상황에서는 모두 다 만들어놓고 숨김처리를 할 것 같아요.

<!-- SomeWindow.xaml -->
<Window ...>
    <Window.Resources>
        <BooleanToVisibilityConverter x:key="BooleanToVisibilityConverter"/>
    </Window.Resources>

    <HomeView DataContext={Binding HomeViewModel} Visibility="{Binding IsHomeViewVisible, Converter={StaticRecource BooleanToVisibilityConverter}}"/>
    <DiscoveryView DataContext={Binding DiscoveryViewModel} Visibility="{Binding IsDiscoveryViewVisible, Converter={StaticRecource BooleanToVisibilityConverter}}"/>
    <FeatureView DataContext={Binding FeatureViewModel} Visibility="{Binding FeatureViewVisible, Converter={StaticRecource BooleanToVisibilityConverter}}"/>
</Window>
// SomeWindowViewModel
public SomeWindowViewModel : INotifyPropertyChanged
{
    ...
    private bool _isHomeViewVisible;
    public bool IsHomeViewVisible
    {
        get => _isHomeViewVisible;
        set
        {
            _isHomeViewVisible = value;
            NotifyPropertyChanged();
        }
    }

    private bool _isDiscoveryViewVisible;
    public bool IsDiscoveryViewVisible
    {
        get => _isDiscoveryViewVisible;
        set
        {
            _isDiscoveryViewVisible = value;
            NotifyPropertyChanged();
        }
    }

    private bool _isFeatureViewVisible;
    public bool IsFeatureViewVisible
    {
        get => _isFeatureViewVisible;
        set
        {
            _isFeatureViewVisible = value;
            NotifyPropertyChanged();
        }
    }

    public HomeViewModel HomeViewModel { get; } = new();
    public DiscoveryViewModel DiscoveryViewModel { get; } = new();
    public FeatureViewModel FeatureViewModel { get; } = new();
    ...
}
좋아요 4

@level120 >ㅁ<b

개념이나 코드에 대한 설명에 더불어 간단한 사족 하나 더 붙여보겠슴다.

@Guroom 님이 첫번째로 부딪힌 문제는

<Control.DataContext>
   <ViewModel:HomeViewModel/>
</Control.DataContext>

요기서 발생합니다.
위와 같이 컨트롤의 엘리먼트에 값을 할당하는 것에 대한 이해가 없었기 때문에 생긴 문제죠.
위 코드가 어떤 동작을 하는 것인지 알 필요가 있었습니다.

두번째는 DataTemplate 을 이용한 View loading 문제 입니다.

앞서 제시한 코드가 어떤 동작을 하는 지 이해했다면
DataTemplate 을 이용한 코드 역시 같은 문제가 있다는 것을 인지할 수 있었겠죠?

다른 설명은 @level120 님이 잘 해주셔서 진짜 사족만 달아봤습니당 ~ㅁ~

좋아요 2


알려주신 코드를 적용해봤는데요, 한번 봐주실 수 있나요?
image
구조는 이렇게 되어있고 MainViewModel.cs 이랑 MainView.xaml만 작업했습니다.

<!--MainWindow.xaml-->
<Window x:Class="_Study_WPF_MVVM.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:_Study_WPF_MVVM"
        xmlns:view="clr-namespace:_Study_WPF_MVVM.MVVM.View"
        xmlns:viewModel="clr-namespace:_Study_WPF_MVVM.MVVM.ViewModel"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.Resources>
        <BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
    </Window.Resources>
    <Window.DataContext>
        <viewModel:MainWindowViewModel/>
    </Window.DataContext>

    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="150"/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="40"/>
            <RowDefinition/>
        </Grid.RowDefinitions>

        <Border BorderBrush="Gray" BorderThickness="1">
            <TextBlock Text="Test App" TextAlignment="Center" VerticalAlignment="Center" FontSize="20"/>
        </Border>
        <Border Grid.Column="1" BorderBrush="Gray" BorderThickness="1">
            <TextBlock Text="Content" VerticalAlignment="Center" HorizontalAlignment="Left" FontSize="20" Margin="4,0,0,0"/>
        </Border>

        <StackPanel Grid.Row="1">
            <RadioButton Content="HomeView" Command="{Binding HomeViewCommand}"/>
            <RadioButton Content="DiscoveryView" Command="{Binding DiscoveryViewCommand}"/>
        </StackPanel>

        <view:HomeView Grid.Column="1" Grid.Row="1" 
                       DataContext="{Binding HomeViewModel}"
                       Visibility="{Binding IsHomeViewModel, Converter={StaticResource BooleanToVisibilityConverter}}"/>
        <view:DiscoveryView Grid.Column="1" Grid.Row="1" 
                       DataContext="{Binding DiscoveryViewModel}"
                       Visibility="{Binding IsDiscoveryViewVisible, Converter={StaticResource BooleanToVisibilityConverter}}"/>
    </Grid>
</Window>

유저컨트롤의 DataContext가 각 ViewModel로 지정되면
Visiblity의 바인딩 소스가 지정된 DataContext를 따르기 때문에
MainWindowViewModel의 bool 프로퍼티를 못찾는 것 같아요

using RTC_LoggerServer.Core;
using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace _Study_WPF_MVVM.MVVM.ViewModel
{
    class MainWindowViewModel : INotifyPropertyChanged
    {
        private bool _isHomeViewVisible = false;
        public bool IsHomeViewVisible
        {
            get => _isHomeViewVisible;
            set
            {
                _isHomeViewVisible = value;
                OnPropertyChanged();
            }
        }

        private bool _isDiscoveryViewVisible = false;
        public bool IsDiscoveryViewVisible
        {
            get => _isDiscoveryViewVisible;
            set
            {
                _isDiscoveryViewVisible = value;
                OnPropertyChanged();
            }
        }

        public HomeViewModel HomeViewModel { get; } = new();
        public DiscoveryViewModel DiscoveryViewModel { get; } = new();

        public RelayCommand HomeViewCommand { get; set; }
        public RelayCommand DiscoveryViewCommand { get; set; }

        public MainWindowViewModel()
        {
            HomeViewCommand = new RelayCommand((o) => IsHomeViewVisible = true);
            DiscoveryViewCommand = new RelayCommand((o) => IsDiscoveryViewVisible = true);
            IsDiscoveryViewVisible = false;
            IsHomeViewVisible = false;
        }

        public event PropertyChangedEventHandler PropertyChanged;
        protected void OnPropertyChanged([CallerMemberName] string name = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
        }
    }
}

그래서 생성 시 Visibility에 바인딩 된 bool값에 false를 주어도 숨김처리가 되지 않습니다.
어떻게 해야될까요? (관련 지식이 없어서 어렵네요…)

bool형을 Visibility으로 변환하는 컨버터를 구현해서 사용하면 됩니다. 아래의 글을 참고하세요.

좋아요 2
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
    Trace.WriteLine($"Convert Called {value}");
    var visibility = (bool)value ? Visibility.Visible : Visibility.Hidden;
    return visibility;
}

직접 구현해서 써봤는데 Convert 호출이 없네용.

image
이 에러 때문일까 싶은데… 흠…
DataContext에서 사용할 바인딩소스랑
Visibility에서 사용할 바인딩소스를 같게 해줄 수 있나요?
DiscovertyViewModel 개체랑 IsDiscoveryViewVisible 프로퍼티는 MainWindowViewModel에 있어서…

.
.
.

해결했습니다.
Visibility에 바인딩될 Bool 프로퍼티들은
각 ViewModel로 옮겨주었습니다.
근데 이 방식이 맞는지는 모르겠네요.
다시한번 제가 너무 방황한다는 걸 느끼네요 ㅜㅠ

잘 동작합니다.
감사합니다 !

namespace _Study_WPF_MVVM.MVVM.ViewModel
{
    class MainWindowViewModel
    {
        public HomeViewModel HomeViewModel { get; } = new();
        public DiscoveryViewModel DiscoveryViewModel { get; } = new();

        public RelayCommand HomeViewCommand { get; set; }
        public RelayCommand DiscoveryViewCommand { get; set; }

        public MainWindowViewModel()
        {
            HomeViewCommand = new RelayCommand((o) => UpdateVisibility(HomeViewModel));
            DiscoveryViewCommand = new RelayCommand((o) => UpdateVisibility(DiscoveryViewModel));
        }

        private void UpdateVisibility(object selected)
        {
            HomeViewModel.IsHomeViewVisible = HomeViewModel.Equals(selected);
            DiscoveryViewModel.IsDiscoveryViewVisible = DiscoveryViewModel.Equals(selected);
        }
    }
}

.

namespace _Study_WPF_MVVM.MVVM.ViewModel
{
    class HomeViewModel : ObservableObject
    {
        private bool _isHomeViewVisible;
        public bool IsHomeViewVisible
        {
            get => _isHomeViewVisible;
            set
            {
                _isHomeViewVisible = value;
                Trace.WriteLine($"IsHomeViewVisible Changed :{IsHomeViewVisible}");
                OnPropertyChanged();
            }
        }
    }
}

.

namespace _Study_WPF_MVVM.MVVM.ViewModel
{
    class DiscoveryViewModel : ObservableObject
    {
        private bool _isDiscoveryViewVisible;
        public bool IsDiscoveryViewVisible
        {
            get => _isDiscoveryViewVisible;
            set
            {
                _isDiscoveryViewVisible = value;
                Trace.WriteLine($"IsDiscoveryViewVisible Changed :{IsDiscoveryViewVisible}");
                OnPropertyChanged();
            }
        }
    }
}

ps. DataContext 속성을 한번 지정하면 다른 속성들은 기존의 바인딩소스를 쓸 수 없는건가요?

네. 변경되면 쓸 수 없습니다. 그런데 그럴일이 없을 것 같은데 동적으로 변경을 어떤 목적으로 해줘야 하나요?

좋아요 1

목적이 있다기 보다 궁금했어요 ㅎㅎ
감사합니다

권장하는 방법은 아닌데 Parent 속성으로 상위 ViewModel에 접근할 수 있는 속성을 하나 만들 수도 있습니다.

public SomeChildViewModel
{
    public SomeViewModel Parent { get; }

    public SomeChildViewModel(SomeViewModel parent)
    {
        Parent = parent;
    }
}
좋아요 1

그러네요 이 방법은 Dependancy Injection 이라 해도 될 것 같은데요
감사합니다 덕분에 많이 성장합니다 ㅎㅎ

프로그래밍 언어에 대한 질문이 아닌 WPF 질문이어서 데스크톱 Q&A로 카테고리를 변경해드렸습니다. 감사합니다.