📚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가 자동으로 업데이트됨!
}
내부 작동 과정:
- CurrentPerson.Age++ 실행
- Age 속성의 setter 호출
- OnPropertyChanged() 호출
- PropertyChanged 이벤트 발생
- WPF가 이 이벤트를 감지
- 해당 속성에 바인딩된 모든 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>
결과: 아이템을 추가하면
- 리스트에 새 아이템이 자동으로 표시됨
- 총액이 자동으로 다시 계산되어 표시됨
📝정리
INotifyPropertyChanged의 역할
- 데이터 변경 감지: 속성 값이 바뀌었을 때 감지
- UI에 알림: "이 속성이 변경되었다"고 WPF에게 알림
- 자동 업데이트: WPF가 해당 속성에 바인딩된 UI 요소들을 자동으로 업데이트
왜 필요한가?
- WPF는 데이터가 언제 변경되는지 모름
- 개발자가 명시적으로 알려줘야 UI가 업데이트됨
- INotifyPropertyChanged가 그 "알려주는 방법"
DataContext와의 관계
- DataContext에 설정된 객체가 INotifyPropertyChanged를 구현하면
- 해당 객체의 모든 속성 변경이 자동으로 UI에 반영됨
- 이것이 바로 "데이터 변경 알림의 자동화"
'C# + WPF' 카테고리의 다른 글
| [C#] - WPF Dispatcher 패턴의 탄생 배경과 WPF에서의 필요성 (0) | 2025.07.28 |
|---|---|
| [C#] Null 조건부 연산자(null-conditional operator) Elvis 연산자 (0) | 2025.07.23 |
| 📝[WPF] CommunityToolkit.Mvvm에서 Observable Property와 RelayCommand (1) | 2025.06.26 |
| 📝C# vs Java 빠르게 비교 + WPF 기본 구조 (0) | 2025.04.06 |