📝[WPF] - DataContext란 무엇인가?

 

📚MVVM 패턴에서의 DataContext 활용

WPF에서 가장 일반적인 사용법은 MVVM 패턴에서 ViewModel을 DataContext로 설정하는 것이다.

우선 아래와 같이 PersonViewModel 이라는 ViewModel 클래스가 있다고 가정해보자.

public class PersonViewModel : INotifyPropertyChanged
{
    private string _name;
    public string Name 
    { 
        get => _name; 
        set 
        { 
            _name = value; 
            OnPropertyChanged(); 
        } 
    }
    
    public event PropertyChangedEventHandler PropertyChanged;
    
    protected void OnPropertyChanged([CallerMemberName] string name = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
    }
}

 

그렇다면 아래와 같이 PersonViewModel을 DataContext로 설정할 수 있다.

public MainWindow() // MainWinodw 클래스의 생성자
{
    InitializeComponent();
    this.DataContext = new PersonViewModel(); // this = 현재 MainWindow 인스턴스
}

 

위와 같이 ViewModel을 DataContext로 설정하는 것은 MVVM 패턴의 핵심인데

이렇게하면 아래와 같은 MVVM 디자인 패턴이 나오게 된 이유를 알 수 있다.

 

 

📌1. 관심사의 분리

View: UI 표현만 담당

ViewModel: 비즈니스 로직과 UI 상태 관리

Model: 데이터와 비즈니스 규칙설정

 

// ❌ 잘못된 예: View에 비즈니스 로직이 같이 존재하는 Case
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }
    
    private void SaveButton_Click(object sender, RoutedEventArgs e)
    {
        // View에 비즈니스 로직이 들어감 - 잘못된 접근
        var name = nameTextBox.Text;
        if (string.IsNullOrEmpty(name))
        {
            MessageBox.Show("이름을 입력하세요");
            return;
        }
        // 데이터베이스 저장 로직...
    }
}

 

// ✅ 올바른 예: ViewModel에 로직 분리
public class PersonViewModel : INotifyPropertyChanged
{
    private string _name;
    public string Name 
    { 
        get => _name; 
        set { _name = value; OnPropertyChanged(); ValidateInput(); }
    }
    
    private string _errorMessage;
    public string ErrorMessage 
    { 
        get => _errorMessage; 
        set { _errorMessage = value; OnPropertyChanged(); }
    }
    
    public ICommand SaveCommand { get; }
    
    public PersonViewModel()
    {
        SaveCommand = new RelayCommand(Save, CanSave);
    }
    
    private bool CanSave() => !string.IsNullOrEmpty(Name);
    
    private void Save()
    {
        // 비즈니스 로직이 ViewModel에 있음
        // 데이터 검증, 저장 로직 등
    }
    
    private void ValidateInput()
    {
        ErrorMessage = string.IsNullOrEmpty(Name) ? "이름을 입력하세요" : null;
    }
}

 

 

 

📌2. 데이터 바인딩의 완전한 활용

ViewModel을 DataContext로 설정하면 양방향 바인딩Command 바인딩을 완전히 활용할 수 있다.

기술적으로는 ViewModel을 DataContext로 설정하지 않아도 양방향 바인딩과 Command 바인딩이 가능하다.

하지만 활용도와 편의성에서 큰 차이가 난다.

 - DataContext 사용 vs 미사용 비교

1. DataContext 사용 (권장)

<Window DataContext="{Binding Source={StaticResource PersonViewModel}}">
    <StackPanel>
        <!-- 간단한 바인딩 경로 -->
        <TextBox Text="{Binding Name}"/>
        <TextBox Text="{Binding Age}"/>
        <Button Content="저장" Command="{Binding SaveCommand}"/>
    </StackPanel>
</Window>

 

2. DataContext 미사용 (가능하지만 복잡)

<Window>
    <Window.Resources>
        <local:PersonViewModel x:Key="PersonVM"/>
    </Window.Resources>
    
    <StackPanel>
        <!-- 매번 전체 경로 지정 필요 -->
        <TextBox Text="{Binding Source={StaticResource PersonVM}, Path=Name}"/>
        <TextBox Text="{Binding Source={StaticResource PersonVM}, Path=Age}"/>
        <Button Content="저장" Command="{Binding Source={StaticResource PersonVM}, Path=SaveCommand}"/>
    </StackPanel>
</Window>

📚 (알면좋고) ViewModel을 DataContext로 설정하지 않으면 활용도가 떨어지는 이유들

1. 코드 중복과 복잡성

<!-- ❌ DataContext 없이 - 매번 Source 지정 -->
<TextBox Text="{Binding Source={StaticResource PersonVM}, Path=Name, UpdateSourceTrigger=PropertyChanged}"/>
<TextBlock Text="{Binding Source={StaticResource PersonVM}, Path=ErrorMessage}"/>
<ProgressBar Visibility="{Binding Source={StaticResource PersonVM}, Path=IsLoading, Converter={StaticResource BoolToVisibilityConverter}}"/>

<!-- ✅ DataContext 사용 - 간단명료 -->
<TextBox Text="{Binding Name, UpdateSourceTrigger=PropertyChanged}"/>
<TextBlock Text="{Binding ErrorMessage}"/>
<ProgressBar Visibility="{Binding IsLoading, Converter={StaticResource BoolToVisibilityConverter}}"/>

 

 

2. 유지보수성 문제

<!-- ViewModel 이름이 변경되면? -->
<!-- ❌ 모든 바인딩을 일일이 수정해야 함 -->
<TextBox Text="{Binding Source={StaticResource PersonVM}, Path=Name}"/>
<TextBox Text="{Binding Source={StaticResource PersonVM}, Path=Age}"/>
<Button Command="{Binding Source={StaticResource PersonVM}, Path=SaveCommand}"/>

<!-- ✅ DataContext 한 곳에서만 변경 -->
<Window DataContext="{Binding Source={StaticResource NewPersonViewModel}}">
    <!-- 모든 바인딩이 자동으로 적용됨 -->
</Window>

 

 

3. 동적 DataContext 변경의 어려움

// ✅ DataContext 사용시 - 간단한 전환
public void SwitchToEditMode()
{
    this.DataContext = new EditPersonViewModel(currentPerson);
    // 모든 UI 요소가 자동으로 새 ViewModel에 바인딩됨
}

// ❌ DataContext 미사용시 - 각각 수동 변경 필요
public void SwitchToEditMode()
{
    var newVM = new EditPersonViewModel(currentPerson);
    nameTextBox.SetBinding(TextBox.TextProperty, new Binding("Name") { Source = newVM });
    ageTextBox.SetBinding(TextBox.TextProperty, new Binding("Age") { Source = newVM });
    saveButton.SetBinding(Button.CommandProperty, new Binding("SaveCommand") { Source = newVM });
    // ... 모든 컨트롤을 일일이 설정
}

 

 

4. 중첩된 컨트롤에서의 복잡성

<!-- ❌ DataContext 없이 - 중첩될수록 복잡해짐 -->
<Grid>
    <StackPanel>
        <TextBox Text="{Binding Source={StaticResource PersonVM}, Path=Name}"/>
        <StackPanel>
            <TextBox Text="{Binding Source={StaticResource PersonVM}, Path=Address.Street}"/>
            <TextBox Text="{Binding Source={StaticResource PersonVM}, Path=Address.City}"/>
        </StackPanel>
    </StackPanel>
</Grid>

<!-- ✅ DataContext 사용 - 중첩되어도 간단 -->
<Grid DataContext="{StaticResource PersonVM}">
    <StackPanel>
        <TextBox Text="{Binding Name}"/>
        <StackPanel>
            <TextBox Text="{Binding Address.Street}"/>
            <TextBox Text="{Binding Address.City}"/>
        </StackPanel>
    </StackPanel>
</Grid>

기술적으로는 ViewModel을 DataContext로 설정하지 않아도 양방향 바인딩과 Command 바인딩이

작동할 수 있게 개발이  가능하다 

 

하지만 아래와 같은 개발 불쾌한 개발경험을 인내해야 한다.

  • 코드량이 2배 이상 증가
  • 유지보수가 어려움
  • 실수할 가능성이 높아짐

따라서 "활용도가 떨어진다"는 것은 기능적 제약이 아니라 개발 효율성과 코드 품질의 문제이다.

MVVM 패턴에서 ViewModel을 DataContext로 설정하는 것표준이자 Best Practice인 이유가 바로 이러한 이유이다.

 

 

 

📌3. UI 상태 관리의 중앙화

ViewModel에서 모든 UI 상태를 관리하므로 일관성 있는 사용자 경험을 제공할 수 있다.

public class OrderViewModel : INotifyPropertyChanged
{
    private bool _isLoading;
    private bool _isEditMode;
    private string _statusMessage;
    
    public bool IsLoading 
    { 
        get => _isLoading; 
        set 
        { 
            _isLoading = value; 
            OnPropertyChanged();
            // UI 상태 변경시 관련 Command들도 자동으로 업데이트
            SaveCommand.RaiseCanExecuteChanged();
            CancelCommand.RaiseCanExecuteChanged();
        }
    }
    
    public bool IsEditMode 
    { 
        get => _isEditMode; 
        set 
        { 
            _isEditMode = value; 
            OnPropertyChanged();
            // 편집 모드에 따라 UI 요소들의 활성화 상태 제어
        }
    }
    
    public string StatusMessage 
    { 
        get => _statusMessage; 
        set { _statusMessage = value; OnPropertyChanged(); }
    }
}

 

 

 

 

📌4. 테스트 용이성

ViewModel을 DataContext로 사용하면 UI 없이도 비즈니스 로직을 테스트할 수 있다.

[Test]
public void Save_WithValidData_ShouldCallRepository()
{
    // Arrange
    var mockRepository = new Mock<IPersonRepository>();
    var viewModel = new PersonViewModel(mockRepository.Object);
    viewModel.Name = "테스트 사용자";
    
    // Act
    viewModel.SaveCommand.Execute(null);
    
    // Assert
    mockRepository.Verify(r => r.Save(It.IsAny<Person>()), Times.Once);
    Assert.IsFalse(viewModel.HasErrors);
}

 

 

 

 

📌5. View와의 느슨한 결합

ViewModel은 View에 대한 참조를 갖지 않으므로 다양한 View에서 재사용할 수 있다.

// 같은 ViewModel을 다른 View에서 사용 가능
// MainWindow.xaml
<Window DataContext="{Binding PersonViewModel}">
    <Grid><!-- 상세한 편집 화면 --></Grid>
</Window>

// PersonSummaryControl.xaml  
<UserControl DataContext="{Binding PersonViewModel}">
    <StackPanel><!-- 간단한 요약 화면 --></StackPanel>
</UserControl>

 

 

 

 

📌6. 데이터 변경 알림의 자동화

INotifyPropertyChanged를 구현한 ViewModel을 DataContext로 설정하면, 데이터 변경이 자동으로 UI에 반영된다.

public class ShoppingCartViewModel : INotifyPropertyChanged
{
    private ObservableCollection<CartItem> _items;
    public ObservableCollection<CartItem> Items 
    { 
        get => _items; 
        set { _items = value; OnPropertyChanged(); }
    }
    
    public decimal TotalAmount => Items?.Sum(i => i.Price * i.Quantity) ?? 0;
    
    public void AddItem(CartItem item)
    {
        Items.Add(item);
        OnPropertyChanged(nameof(TotalAmount)); // 총액 자동 업데이트
    }
}

 

 

📚(알면좋고) INotifyPropertyChanged란 무엇인가?

INotifyPropertyChanged는 WPF 데이터 바인딩의 핵심 메커니즘이다.

쉽게 말해서 "데이터가 변경되었다고 UI에게 알려주는 방법"이다.

 

INotifyPropertyChanged 없는 상황

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}
 
<StackPanel>
    <TextBlock Text="{Binding Name}"/>
    <TextBlock Text="{Binding Age}"/>
    <Button Content="나이 증가" Click="IncreaseAge_Click"/>
</StackPanel>
 
public partial class MainWindow : Window
{
    public Person CurrentPerson { get; set; }
    
    public MainWindow()
    {
        InitializeComponent();
        CurrentPerson = new Person { Name = "홍길동", Age = 30 };
        DataContext = CurrentPerson;
    }
    
    private void IncreaseAge_Click(object sender, RoutedEventArgs e)
    {
        CurrentPerson.Age++; // ❌ 나이는 증가하지만 UI에 반영되지 않음!
        Console.WriteLine($"실제 나이: {CurrentPerson.Age}"); // 31로 출력됨
        // 하지만 화면의 TextBlock은 여전히 30을 표시
    }
}

결과: 버튼을 클릭해도 화면의 나이는 변하지 않는다. 데이터는 변했지만 UI가 모르기 때문

 

INotifyPropertyChanged란?

namespace System.ComponentModel
{
    public interface INotifyPropertyChanged
    {
        event PropertyChangedEventHandler PropertyChanged;
    }
}

이 인터페이스는 단 하나의 이벤트만 정의한다.

  • PropertyChanged: "어떤 속성이 변경되었다"고 알려주는 이벤트

INotifyPropertyChanged을 구현하게 되면?

public class Person : INotifyPropertyChanged
{
    private string _name;
    private int _age;
    
    public string Name 
    { 
        get => _name; 
        set 
        { 
            _name = value; 
            OnPropertyChanged(); // "Name이 변경되었다"고 알림
        } 
    }
    
    public int Age 
    { 
        get => _age; 
        set 
        { 
            _age = value; 
            OnPropertyChanged(); // "Age가 변경되었다"고 알림
        } 
    }
    
    // PropertyChanged 이벤트 구현
    public event PropertyChangedEventHandler PropertyChanged;
    
    protected void OnPropertyChanged([CallerMemberName] string name = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
    }
}
private void IncreaseAge_Click(object sender, RoutedEventArgs e)
{
    CurrentPerson.Age++; // ✅ 이제 UI가 자동으로 업데이트됨!
}

내부 작동 과정:

  1. CurrentPerson.Age++ 실행
  2. Age 속성의 setter 호출
  3. OnPropertyChanged() 호출
  4. PropertyChanged 이벤트 발생
  5. WPF가 이 이벤트를 감지
  6. 해당 속성에 바인딩된 모든 UI 요소 자동 업데이트

 

실제 동작 예시

public class ShoppingCartViewModel : INotifyPropertyChanged
{
    private ObservableCollection<CartItem> _items;
    
    public ObservableCollection<CartItem> Items 
    { 
        get => _items; 
        set 
        { 
            _items = value; 
            OnPropertyChanged(); // "Items가 변경되었다"고 알림
            OnPropertyChanged(nameof(TotalAmount)); // "TotalAmount도 재계산하라"고 알림
        } 
    }
    
    // 계산된 속성 - Items가 변경되면 자동으로 재계산되어야 함
    public decimal TotalAmount => Items?.Sum(i => i.Price * i.Quantity) ?? 0;
    
    public void AddItem(CartItem item)
    {
        Items.Add(item); // ObservableCollection이 자동으로 변경 알림
        OnPropertyChanged(nameof(TotalAmount)); // 총액도 다시 계산하라고 알림
    }
    
    public event PropertyChangedEventHandler PropertyChanged;
    
    protected void OnPropertyChanged([CallerMemberName] string name = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
    }
}

 

 

UI에서의 자동 업데이트

<StackPanel DataContext="{Binding ShoppingCartVM}">
    <!-- Items가 변경되면 자동으로 리스트 업데이트 -->
    <ListBox ItemsSource="{Binding Items}"/>
    
    <!-- TotalAmount가 변경되면 자동으로 텍스트 업데이트 -->
    <TextBlock Text="{Binding TotalAmount, StringFormat='총액: {0:C}'}"/>
    
    <Button Content="아이템 추가" Command="{Binding AddItemCommand}"/>
</StackPanel>

 

결과: 아이템을 추가하면

  1. 리스트에 새 아이템이 자동으로 표시됨
  2. 총액이 자동으로 다시 계산되어 표시됨

 

📝정리

INotifyPropertyChanged의 역할

  1. 데이터 변경 감지: 속성 값이 바뀌었을 때 감지
  2. UI에 알림: "이 속성이 변경되었다"고 WPF에게 알림
  3. 자동 업데이트: WPF가 해당 속성에 바인딩된 UI 요소들을 자동으로 업데이트

왜 필요한가?

  • WPF는 데이터가 언제 변경되는지 모름
  • 개발자가 명시적으로 알려줘야 UI가 업데이트됨
  • INotifyPropertyChanged가 그 "알려주는 방법"

DataContext와의 관계

  • DataContext에 설정된 객체가 INotifyPropertyChanged를 구현하면
  • 해당 객체의 모든 속성 변경이 자동으로 UI에 반영
  • 이것이 바로 "데이터 변경 알림의 자동화"