Ver3.0 ๐จ WPF MVVM ํจํด์ผ๋ก ๋ง๋๋ ๋ฐ์คํฌํฑ ์ฑ ์ํคํ ์ฒ

๐จ WPF MVVM ํจํด์ผ๋ก ๋ง๋๋ ๋ฐ์คํฌํฑ ์ฑ ์ํคํ ์ฒ
์น๊ตฌ์ฒ๋ผ ํธํ๊ฒ ๋ฐฐ์ฐ๋ C# ๋ฐ์คํฌํฑ ๊ฐ๋ฐ์ ์ ์
์๋
! ๐ ์ค๋์ WPF(Windows Presentation Foundation)์ MVVM(Model-View-ViewModel) ํจํด์ ํ์ฉํ ๋ฐ์คํฌํฑ ์ ํ๋ฆฌ์ผ์ด์
์ํคํ
์ฒ ์ค๊ณ์ ๋ํด ์ด์ผ๊ธฐํด๋ณผ ๊ฑฐ์ผ.
ํน์ WPF๋ก ํ๋ก๊ทธ๋จ ๋ง๋ค๋ค๊ฐ ์ฝ๋๊ฐ ์คํ๊ฒํฐ์ฒ๋ผ ์์ผ์ ๋จธ๋ฆฌ ์ํ ๋ ๊ฒฝํ ์์ด? ๐
์๋๋ฉด UI ์ฝ๋์ ๋น์ฆ๋์ค ๋ก์ง์ด ๋ค์์ฌ์ ๋์ค์ ์์ ํ ๋ ์ด๋๋ฅผ ๊ฑด๋๋ ค์ผ ํ ์ง ๋ชฐ๋ผ ๋นํฉํ๋ ์ ์?
๊ทธ๋ฐ ๊ณ ๋ฏผ๋ค์ ํด๊ฒฐํด์ฃผ๋ ๊ฒ ๋ฐ๋ก MVVM ํจํด์ด์ผ. ์ด ํจํด์ ์ ๋๋ก ์ดํดํ๊ณ ์ ์ฉํ๋ฉด, ์ฝ๋๊ฐ ๊น๋ํด์ง๊ณ ์ ์ง๋ณด์๋ ํจ์ฌ ์ฌ์์ ธ. ๋ง์น ์ ์ ๋ฆฌ๋ ์๋์ฅ์ฒ๋ผ ๋ง์ด์ผ! ๐ฆ
๐ฏ MVVM ํจํด์ด ๋ญ๊ธธ๋?
MVVM์ Model-View-ViewModel์ ์ฝ์์ผ. ๋ง์ดํฌ๋ก์ํํธ๊ฐ WPF์ Silverlight๋ฅผ ์ํด 2005๋
์ ์๊ฐํ ์ํคํ
์ฒ ํจํด์ด์ง. ๐๏ธ
๊ฐ๋จํ ๋งํ๋ฉด, ์ ํ๋ฆฌ์ผ์ด์
์ ์ธ ๊ฐ์ง ์ฃผ์ ์ปดํฌ๋ํธ๋ก ๋ถ๋ฆฌํ๋ ๊ฑฐ์ผ:
๐ฑ View (๋ทฐ)
์ฌ์ฉ์๊ฐ ๋ณด๋ ํ๋ฉด์ด์ผ. XAML๋ก ์์ฑ๋๊ณ , ๋ฒํผ, ํ
์คํธ๋ฐ์ค, ๋ฆฌ์คํธ ๊ฐ์ UI ์์๋ค์ด ์ฌ๊ธฐ ์์ด. View๋ "์ด๋ป๊ฒ ๋ณด์ผ๊น?"์๋ง ์ง์คํด.
โ๏ธ ViewModel (๋ทฐ๋ชจ๋ธ)
View์ Model ์ฌ์ด์ ์ค๊ฐ ๋ค๋ฆฌ ์ญํ ์ ํด. View์ ํ์ํ ๋ฐ์ดํฐ๋ฅผ ์ค๋นํ๊ณ , ์ฌ์ฉ์์ ์
๋ ฅ์ ์ฒ๋ฆฌํด์ Model์ ์ ๋ฌํ์ง. "์ด๋ป๊ฒ ๋์ํ ๊น?"๋ฅผ ๋ด๋นํด.
๐พ Model (๋ชจ๋ธ)
์ค์ ๋ฐ์ดํฐ์ ๋น์ฆ๋์ค ๋ก์ง์ด ์๋ ๊ณณ์ด์ผ. ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ ๊ทผ, ๊ณ์ฐ, ์ ํจ์ฑ ๊ฒ์ฌ ๊ฐ์ ํต์ฌ ๊ธฐ๋ฅ๋ค์ด ์ฌ๊ธฐ ์์ด. "๋ฌด์์ ํ ๊น?"๋ฅผ ์ ์ํด.
๐ก ์ ์ด๋ ๊ฒ ๋๋๊น?
์๋ฅผ ๋ค์ด๋ณผ๊ฒ. ๋ค๊ฐ ๋ ์คํ ๋์ ์ด์ํ๋ค๊ณ ์๊ฐํด๋ด. ๐ฝ๏ธ
- View๋ ์๋์ด ๋ณด๋ ๋ฉ๋ดํ๊ณผ ์๋น ์ธํ
๋ฆฌ์ด
- ViewModel์ ์ฃผ๋ฌธ์ ๋ฐ๊ณ ์ฃผ๋ฐฉ์ ์ ๋ฌํ๋ ์จ์ดํฐ
- Model์ ์ค์ ์๋ฆฌ๋ฅผ ๋ง๋๋ ์ฃผ๋ฐฉ
๊ฐ์ ์ญํ ์ด ๋ช
ํํ๋๊น ํจ์จ์ ์ด๊ณ , ํ ๋ถ๋ถ์ ๋ฐ๊ฟ๋ ๋ค๋ฅธ ๋ถ๋ถ์ ์ํฅ์ด ์ ์ด!
๐ WPF๊ฐ MVVM๊ณผ ์ฐฐ๋ก๊ถํฉ์ธ ์ด์
WPF๋ MVVM ํจํด์ ์ ์ฉํ๊ธฐ์ ์ ๋ง ์๋ฒฝํ ํ๋ซํผ์ด์ผ. ์๋ํ๋ฉด WPF๊ฐ ์ ๊ณตํ๋ ๋ช ๊ฐ์ง ๊ฐ๋ ฅํ ๊ธฐ๋ฅ๋ค์ด MVVM์ ์์ฐ์ค๋ฝ๊ฒ ์ง์ํ๊ฑฐ๋ . ๐จ
1๏ธโฃ ๋ฐ์ดํฐ ๋ฐ์ธ๋ฉ (Data Binding)
WPF์ ๊ฐ์ฅ ๊ฐ๋ ฅํ ๊ธฐ๋ฅ ์ค ํ๋์ผ. XAML์์ UI ์์๋ฅผ ViewModel์ ์์ฑ์ ์ง์ ์ฐ๊ฒฐํ ์ ์์ด.
์๋ฅผ ๋ค์ด, ํ
์คํธ๋ฐ์ค์ ๋ด์ฉ์ ViewModel์ ์์ฑ๊ณผ ๋ฐ์ธ๋ฉํ๋ฉด, ์ฌ์ฉ์๊ฐ ์
๋ ฅํ ๋ด์ฉ์ด ์๋์ผ๋ก ViewModel์ ๋ฐ์๋๊ณ , ๋ฐ๋๋ก ViewModel์ ๊ฐ์ด ๋ฐ๋๋ฉด ํ๋ฉด๋ ์๋์ผ๋ก ์
๋ฐ์ดํธ๋ผ. ๋ง์น ๋ง๋ฒ์ฒ๋ผ! โจ
<TextBox Text="{Binding UserName, UpdateSourceTrigger=PropertyChanged}" />
์ด ํ ์ค๋ก TextBox์ ViewModel์ UserName ์์ฑ์ด ์๋ฐฉํฅ์ผ๋ก ์ฐ๊ฒฐ๋ผ!
2๏ธโฃ ์ปค๋งจ๋ (Command)
๋ฒํผ ํด๋ฆญ ๊ฐ์ ์ฌ์ฉ์ ์ก์
์ ์ฒ๋ฆฌํ๋ ๋ฐฉ๋ฒ์ด์ผ. ์ ํต์ ์ธ ์ด๋ฒคํธ ํธ๋ค๋ฌ ๋์ ์ปค๋งจ๋๋ฅผ ์ฌ์ฉํ๋ฉด, UI ๋ก์ง์ ViewModel๋ก ๊น๋ํ๊ฒ ์ฎ๊ธธ ์ ์์ด. ๐ฎ
์ฝ๋-๋นํ์ธ๋์ ์ด๋ฒคํธ ํธ๋ค๋ฌ๋ฅผ ์๋ฉ ์์ฑํ๋ ๋์ , ViewModel์์ ์ปค๋งจ๋๋ก ์ฒ๋ฆฌํ๋ฉด ํ
์คํธ๋ ์ฝ๊ณ ์ฌ์ฌ์ฉ๋ ๊ฐ๋ฅํด!
<Button Content="์ ์ฅ" Command="{Binding SaveCommand}" />
3๏ธโฃ ํ
ํ๋ฆฟ๊ณผ ์คํ์ผ
WPF๋ UI๋ฅผ ๋ฐ์ดํฐ๋ก๋ถํฐ ์์ ํ ๋ถ๋ฆฌํ ์ ์๊ฒ ํด์ค. DataTemplate์ ์ฌ์ฉํ๋ฉด ๊ฐ์ ๋ฐ์ดํฐ๋ฅผ ๋ค์ํ ๋ฐฉ์์ผ๋ก ํํํ ์ ์์ด. ๐ญ
์๋ฅผ ๋ค์ด, ์ฌ์ฉ์ ๋ชฉ๋ก์ ์นด๋ ํํ๋ก ๋ณด์ฌ์ค์ง, ํ
์ด๋ธ ํํ๋ก ๋ณด์ฌ์ค์ง ViewModel์ ์ ํ ์์ ํ์ง ์๊ณ XAML๋ง ๋ฐ๊ฟ์ ๊ฒฐ์ ํ ์ ์์ด!
4๏ธโฃ INotifyPropertyChanged ์ธํฐํ์ด์ค
์ด๊ฑด MVVM์ ํต์ฌ ๋ฉ์ปค๋์ฆ์ด์ผ. ViewModel์ ์์ฑ์ด ๋ณ๊ฒฝ๋์์ ๋ View์๊ฒ ์๋ ค์ฃผ๋ ์ญํ ์ ํด. ๐ข
์ด ์ธํฐํ์ด์ค๋ฅผ ๊ตฌํํ๋ฉด, ์์ฑ ๊ฐ์ด ๋ฐ๋ ๋๋ง๋ค ์๋์ผ๋ก UI๊ฐ ์
๋ฐ์ดํธ๋ผ. ๊ฐ๋ฐ์๊ฐ ์ผ์ผ์ด UI๋ฅผ ๊ฐฑ์ ํ๋ ์ฝ๋๋ฅผ ์์ฑํ ํ์๊ฐ ์์ด!
๐๏ธ MVVM ์ํคํ ์ฒ ์ค๊ณ์ ํต์ฌ ์์น
์ด์ ์ค์ ๋ก MVVM ์ํคํ ์ฒ๋ฅผ ์ค๊ณํ ๋ ์ง์ผ์ผ ํ ํต์ฌ ์์น๋ค์ ์์๋ณด์. ์ด ์์น๋ค์ ์ ๋ฐ๋ฅด๋ฉด ์ ์ง๋ณด์ํ๊ธฐ ์ฝ๊ณ ํ์ฅ ๊ฐ๋ฅํ ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ง๋ค ์ ์์ด! ๐ฏ
์์น 1: View๋ ViewModel๋ง ์์์ผ ํด
View๋ ViewModel์ ๋ํ ์ฐธ์กฐ๋ง ๊ฐ์ง๊ณ ์์ด์ผ ํด. Model์ ์ง์ ์ฐธ์กฐํ๋ฉด ์ ๋ผ! ๐ซ
View์ DataContext์ ViewModel์ ์ค์ ํ๊ณ , ๋ชจ๋ ๋ฐ์ดํฐ์ ์ปค๋งจ๋๋ ๋ฐ์ธ๋ฉ์ ํตํด ์ ๊ทผํด์ผ ํด. ์ด๋ ๊ฒ ํ๋ฉด View์ ๋น์ฆ๋์ค ๋ก์ง์ด ์์ ํ ๋ถ๋ฆฌ๋ผ.
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new MainViewModel();
}
}
์ด๊ฒ ์ ๋ถ์ผ! View์ ์ฝ๋-๋นํ์ธ๋๋ ์ด๋ ๊ฒ ๊ฐ๋จํด์ผ ํด. ๐
์์น 2: ViewModel์ View๋ฅผ ๋ชฐ๋ผ์ผ ํด
์ด๊ฒ ์ง์ง ์ค์ํด! ViewModel์ View์ ๋ํ ์ด๋ค ์ฐธ์กฐ๋ ๊ฐ์ง๋ฉด ์ ๋ผ. using System.Windows ๊ฐ์ UI ๋ค์์คํ์ด์ค๋ ViewModel์์ ์ฌ์ฉํ๋ฉด ์ ๋ผ! ๐
โโ๏ธ
์๋ํ๋ฉด ViewModel์ ์์ํ C# ํด๋์ค์ฌ์ผ ํ๊ฑฐ๋ . ๊ทธ๋์ผ ๋จ์ ํ
์คํธ๋ฅผ ์์ฑํ ์ ์๊ณ , ๋ค๋ฅธ ํ๋ซํผ(์: Xamarin, UWP)์์๋ ์ฌ์ฌ์ฉํ ์ ์์ด.
๋ํ์์๋ฅผ ๋์ฐ๊ฑฐ๋ ํ์ผ์ ์ ํํ๋ ๋ฑ UI์ ์ํธ์์ฉ์ด ํ์ํ ๊ฒฝ์ฐ์๋ ์๋น์ค ์ธํฐํ์ด์ค๋ฅผ ์ฌ์ฉํด์ผ ํด.
์์น 3: Model์ ๋
๋ฆฝ์ ์ด์ด์ผ ํด
Model์ View๋ ViewModel์ ๋ํด ์ ํ ๋ชฐ๋ผ์ผ ํด. ์์ ํ ๋
๋ฆฝ์ ์ธ ๋น์ฆ๋์ค ๋ก์ง๊ณผ ๋ฐ์ดํฐ ๊ตฌ์กฐ์ฌ์ผ ํ์ง. ๐ผ
Model์ ๋ค๋ฅธ ์ ํ๋ฆฌ์ผ์ด์
์์๋ ๊ทธ๋๋ก ์ฌ์ฉํ ์ ์์ ์ ๋๋ก ๋ฒ์ฉ์ ์ด์ด์ผ ํด. ์๋ฅผ ๋ค์ด, ๊ฐ์ Model์ ์น API, ์ฝ์ ์ฑ, WPF ์ฑ์์ ๋ชจ๋ ์ฌ์ฉํ ์ ์์ด์ผ ํด!
์์น 4: ๋จ๋ฐฉํฅ ์์กด์ฑ
์์กด์ฑ์ ํญ์ ํ ๋ฐฉํฅ์ผ๋ก๋ง ํ๋ฌ์ผ ํด: View โ ViewModel โ Model ๐
์ญ๋ฐฉํฅ ์์กด์ฑ์ด ์๊ธฐ๋ฉด ์ ๋ผ! ์ด๊ฑธ ์งํค๋ฉด ๊ฐ ๋ ์ด์ด๋ฅผ ๋
๋ฆฝ์ ์ผ๋ก ํ
์คํธํ๊ณ ์์ ํ ์ ์์ด. ์ํ ์ฐธ์กฐ๊ฐ ์๊ธฐ๋ฉด ์ฝ๋๊ฐ ๋ณต์กํด์ง๊ณ ์ ์ง๋ณด์๊ฐ ์ด๋ ค์์ ธ.
๐ป ์ค์ ! ViewModel ๊ตฌํํ๊ธฐ
์ด์ ์ค์ ๋ก ViewModel์ ์ด๋ป๊ฒ ๊ตฌํํ๋์ง ์์ธํ ์์๋ณด์. ViewModel์ MVVM์ ํต์ฌ์ด๋๊น ์ ๋๋ก ์ดํดํ๋ ๊ฒ ์ค์ํด! ๐
INotifyPropertyChanged ๊ตฌํ
๋จผ์ ๊ธฐ๋ณธ์ด ๋๋ BaseViewModel์ ๋ง๋ค์ด๋ณด์. ๋ชจ๋ ViewModel์ด ์์๋ฐ์ ๊ธฐ๋ณธ ํด๋์ค์ผ:
using System.ComponentModel;
using System.Runtime.CompilerServices;
public class BaseViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
{
if (Equals(storage, value))
return false;
storage = value;
OnPropertyChanged(propertyName);
return true;
}
}
์ด BaseViewModel์ ์์ฑ ๋ณ๊ฒฝ ์๋ฆผ์ ์ฝ๊ฒ ์ฒ๋ฆฌํ ์ ์๊ฒ ํด์ค. SetProperty ๋ฉ์๋๋ฅผ ์ฌ์ฉํ๋ฉด ์์ฑ setter๋ฅผ ๊ฐ๋จํ๊ฒ ์์ฑํ ์ ์์ด! ๐
์์ฑ(Property) ๊ตฌํ
์ด์ ์ค์ ViewModel์์ ์์ฑ์ ์ด๋ป๊ฒ ๊ตฌํํ๋์ง ๋ณด์:
public class UserViewModel : BaseViewModel
{
private string _userName;
public string UserName
{
get => _userName;
set => SetProperty(ref _userName, value);
}
private int _age;
public int Age
{
get => _age;
set
{
if (SetProperty(ref _age, value))
{
// Age๊ฐ ๋ณ๊ฒฝ๋๋ฉด IsAdult๋ ์
๋ฐ์ดํธ
OnPropertyChanged(nameof(IsAdult));
}
}
}
public bool IsAdult => Age >= 18;
}
๋ณด๋ค์ํผ SetProperty๋ฅผ ์ฌ์ฉํ๋ฉด ์ฝ๋๊ฐ ์ ๋ง ๊ฐ๊ฒฐํด์ ธ! ๊ทธ๋ฆฌ๊ณ ํ ์์ฑ์ด ๋ณ๊ฒฝ๋ ๋ ๋ค๋ฅธ ์์ฑ๋ ํจ๊ป ์ ๋ฐ์ดํธํด์ผ ํ๋ค๋ฉด, OnPropertyChanged๋ฅผ ์ถ๊ฐ๋ก ํธ์ถํ๋ฉด ๋ผ. ๐ฏ
๐ก ํ๋ก ํ!
์์ฑ ์ด๋ฆ์ ๋ฌธ์์ด๋ก ์ง์ ์ฐ๋ ๋์ nameof ์ฐ์ฐ์๋ฅผ ์ฌ์ฉํด! ์คํ๋ฅผ ๋ฐฉ์งํ๊ณ ๋ฆฌํฉํ ๋งํ ๋๋ ์์ ํด. ์ปดํ์ผ๋ฌ๊ฐ ์ฒดํฌํด์ฃผ๊ฑฐ๋ ! โ
โก ์ปค๋งจ๋(Command) ํจํด ๋ง์คํฐํ๊ธฐ
์ปค๋งจ๋๋ ์ฌ์ฉ์์ ์ก์ (๋ฒํผ ํด๋ฆญ, ๋ฉ๋ด ์ ํ ๋ฑ)์ ์ฒ๋ฆฌํ๋ MVVM์ ํต์ฌ ๋ฉ์ปค๋์ฆ์ด์ผ. ์ ํต์ ์ธ ์ด๋ฒคํธ ํธ๋ค๋ฌ๋ณด๋ค ํจ์ฌ ๊ฐ๋ ฅํ๊ณ ์ ์ฐํด! ๐
RelayCommand ๊ตฌํ
๊ฐ์ฅ ๋ง์ด ์ฌ์ฉ๋๋ ์ปค๋งจ๋ ๊ตฌํ ๋ฐฉ์์ด์ผ. ์ง์ ๋ง๋ค์ด๋ณด์:
using System;
using System.Windows.Input;
public class RelayCommand : ICommand
{
private readonly Action<object> _execute;
private readonly Func<object, bool> _canExecute;
public RelayCommand(Action<object> execute, Func<object, bool> canExecute = null)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public bool CanExecute(object parameter)
{
return _canExecute == null || _canExecute(parameter);
}
public void Execute(object parameter)
{
_execute(parameter);
}
}
์ด RelayCommand๋ ๋ ๊ฐ์ง ์ค์ํ ๊ธฐ๋ฅ์ ์ ๊ณตํด:
1๏ธโฃ Execute: ์ค์ ๋ก ์ํํ ์์
2๏ธโฃ CanExecute: ์ปค๋งจ๋๋ฅผ ์คํํ ์ ์๋์ง ์ฌ๋ถ (๋ฒํผ ํ์ฑํ/๋นํ์ฑํ)
ViewModel์์ ์ปค๋งจ๋ ์ฌ์ฉํ๊ธฐ
์ด์ ์ค์ ๋ก ViewModel์์ ์ปค๋งจ๋๋ฅผ ์ด๋ป๊ฒ ์ฌ์ฉํ๋์ง ๋ณด์:
public class MainViewModel : BaseViewModel
{
private string _searchText;
public string SearchText
{
get => _searchText;
set => SetProperty(ref _searchText, value);
}
private ICommand _searchCommand;
public ICommand SearchCommand
{
get
{
if (_searchCommand == null)
{
_searchCommand = new RelayCommand(
execute: param => PerformSearch(),
canExecute: param => !string.IsNullOrWhiteSpace(SearchText)
);
}
return _searchCommand;
}
}
private void PerformSearch()
{
// ์ค์ ๊ฒ์ ๋ก์ง
// ์: ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฟผ๋ฆฌ, API ํธ์ถ ๋ฑ
}
}
XAML์์๋ ์ด๋ ๊ฒ ๋ฐ์ธ๋ฉํด:
<TextBox Text="{Binding SearchText, UpdateSourceTrigger=PropertyChanged}" />
<Button Content="๊ฒ์" Command="{Binding SearchCommand}" />
์ด๋ ๊ฒ ํ๋ฉด SearchText๊ฐ ๋น์ด์์ ๋ ๋ฒํผ์ด ์๋์ผ๋ก ๋นํ์ฑํ๋ผ! ๋ณ๋์ ์ฝ๋ ์์ด ๋ง์ด์ผ. ๋ฉ์ง์ง? ๐
๋น๋๊ธฐ ์ปค๋งจ๋
์์ฆ ์ฑ์ ๋๋ถ๋ถ ๋น๋๊ธฐ ์์
์ ํด์ผ ํด. ๋คํธ์ํฌ ํธ์ถ, ํ์ผ I/O, ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฟผ๋ฆฌ ๋ฑ๋ฑ. ๋น๋๊ธฐ ์ปค๋งจ๋๋ฅผ ๋ง๋ค์ด๋ณด์:
public class AsyncRelayCommand : ICommand
{
private readonly Func<object, Task> _execute;
private readonly Func<object, bool> _canExecute;
private bool _isExecuting;
public AsyncRelayCommand(Func<object, Task> execute, Func<object, bool> canExecute = null)
{
_execute = execute;
_canExecute = canExecute;
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public bool CanExecute(object parameter)
{
return !_isExecuting && (_canExecute == null || _canExecute(parameter));
}
public async void Execute(object parameter)
{
_isExecuting = true;
CommandManager.InvalidateRequerySuggested();
try
{
await _execute(parameter);
}
finally
{
_isExecuting = false;
CommandManager.InvalidateRequerySuggested();
}
}
}
์ด ๋น๋๊ธฐ ์ปค๋งจ๋๋ ์คํ ์ค์ ์๋์ผ๋ก ๋นํ์ฑํ๋ผ์ ์ค๋ณต ์คํ์ ๋ฐฉ์งํด์ค! ๐
โ ๏ธ ์ฃผ์์ฌํญ
์ปค๋งจ๋์์ ๊ธด ์์
์ ์ํํ ๋๋ ๋ฐ๋์ ๋น๋๊ธฐ๋ก ์ฒ๋ฆฌํด์ผ ํด! ๊ทธ๋ ์ง ์์ผ๋ฉด UI๊ฐ ๋ฉ์ถฐ์ ์ฌ์ฉ์ ๊ฒฝํ์ด ๋๋น ์ ธ. async/await๋ฅผ ์ ๊ทน ํ์ฉํ์! ๐โโ๏ธ
๐จ ๋ฐ์ดํฐ ๋ฐ์ธ๋ฉ ์ฌํ
๋ฐ์ดํฐ ๋ฐ์ธ๋ฉ์ WPF MVVM์ ๊ฝ์ด์ผ! ๐ธ ์ ๋๋ก ํ์ฉํ๋ฉด ์ ๋ง ๊ฐ๋ ฅํ ๊ธฐ๋ฅ๋ค์ ๊ตฌํํ ์ ์์ด. ์ฌํ ํ ํฌ๋๋ค์ ์์๋ณด์!
๋ฐ์ธ๋ฉ ๋ชจ๋ (Binding Mode)
WPF๋ ์ฌ๋ฌ ๊ฐ์ง ๋ฐ์ธ๋ฉ ๋ชจ๋๋ฅผ ์ ๊ณตํด:
| ๋ชจ๋ | ์ค๋ช | ์ฌ์ฉ ์์ |
|---|---|---|
| OneWay | ViewModel โ View ๋จ๋ฐฉํฅ | ์ฝ๊ธฐ ์ ์ฉ ํ ์คํธ, ๋ ์ด๋ธ |
| TwoWay | ์๋ฐฉํฅ ๋๊ธฐํ | TextBox, CheckBox ๋ฑ ์ ๋ ฅ ์ปจํธ๋กค |
| OneTime | ์ด๊ธฐ ํ ๋ฒ๋ง | ๋ณํ์ง ์๋ ์ ์ ๋ฐ์ดํฐ |
| OneWayToSource | View โ ViewModel ๋จ๋ฐฉํฅ | ํน์ํ ๊ฒฝ์ฐ์๋ง ์ฌ์ฉ |
<!-- OneWay: ์ฝ๊ธฐ ์ ์ฉ -->
<TextBlock Text="{Binding UserName, Mode=OneWay}" />
<!-- TwoWay: ์๋ฐฉํฅ -->
<TextBox Text="{Binding UserName, Mode=TwoWay}" />
<!-- OneTime: ์ด๊ธฐํ ์ ํ ๋ฒ๋ง -->
<TextBlock Text="{Binding AppTitle, Mode=OneTime}" />
UpdateSourceTrigger
์ธ์ ViewModel์ ์
๋ฐ์ดํธํ ์ง ๊ฒฐ์ ํ๋ ์์ฑ์ด์ผ:
<!-- PropertyChanged: ์ฆ์ ์
๋ฐ์ดํธ (ํ์ดํํ ๋๋ง๋ค) -->
<TextBox Text="{Binding SearchText, UpdateSourceTrigger=PropertyChanged}" />
<!-- LostFocus: ํฌ์ปค์ค๋ฅผ ์์ ๋ (๊ธฐ๋ณธ๊ฐ) -->
<TextBox Text="{Binding UserName, UpdateSourceTrigger=LostFocus}" />
<!-- Explicit: ๋ช
์์ ์ผ๋ก ํธ์ถํ ๋๋ง -->
<TextBox Text="{Binding Email, UpdateSourceTrigger=Explicit}" />
์ค์๊ฐ ๊ฒ์ ๊ธฐ๋ฅ์ ๋ง๋ค ๋๋ PropertyChanged๋ฅผ ์ฌ์ฉํ๊ณ , ์ผ๋ฐ ์ ๋ ฅ ํผ์์๋ LostFocus๋ฅผ ์ฌ์ฉํ๋ ๊ฒ ์ข์. ์ฑ๋ฅ๊ณผ ์ฌ์ฉ์ ๊ฒฝํ์ ๊ท ํ์ ๋ง์ถฐ์ผ ํด! โ๏ธ
์ปฌ๋ ์
๋ฐ์ธ๋ฉ
๋ฆฌ์คํธ๋ ๊ทธ๋ฆฌ๋์ ๋ฐ์ดํฐ๋ฅผ ํ์ํ ๋๋ ObservableCollection์ ์ฌ์ฉํด์ผ ํด:
using System.Collections.ObjectModel;
public class MainViewModel : BaseViewModel
{
public ObservableCollection<UserModel> Users { get; set; }
public MainViewModel()
{
Users = new ObservableCollection<UserModel>();
LoadUsers();
}
private void LoadUsers()
{
// ๋ฐ์ดํฐ ๋ก๋
Users.Add(new UserModel { Name = "ํ๊ธธ๋", Age = 25 });
Users.Add(new UserModel { Name = "๊น์ฒ ์", Age = 30 });
}
}
XAML์์๋:
<ListBox ItemsSource="{Binding Users}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Name}" FontWeight="Bold" />
<TextBlock Text="{Binding Age}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
ObservableCollection์ ํญ๋ชฉ์ด ์ถ๊ฐ/์ญ์ ๋ ๋ ์๋์ผ๋ก UI๋ฅผ ์ ๋ฐ์ดํธํด์ค! ์ผ๋ฐ List๋ฅผ ์ฌ์ฉํ๋ฉด ์ ๋ผ! ๐ซ
๊ฐ ๋ณํ๊ธฐ (Value Converter)
๋๋ก๋ ViewModel์ ๋ฐ์ดํฐ๋ฅผ View์ ํ์ํ๊ธฐ ์ ์ ๋ณํํด์ผ ํ ๋๊ฐ ์์ด. ์๋ฅผ ๋ค์ด, bool ๊ฐ์ "์/์๋์ค"๋ก ํ์ํ๊ฑฐ๋, ๋ ์ง ํ์์ ๋ฐ๊พธ๋ ๋ฑ์ด์ง.
using System;
using System.Globalization;
using System.Windows.Data;
public class BoolToYesNoConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is bool boolValue)
{
return boolValue ? "์" : "์๋์ค";
}
return "์ ์ ์์";
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is string strValue)
{
return strValue == "์";
}
return false;
}
}
XAML์์ ์ฌ์ฉ:
<Window.Resources>
<local:BoolToYesNoConverter x:Key="BoolToYesNo" />
</Window.Resources>
<TextBlock Text="{Binding IsActive, Converter={StaticResource BoolToYesNo}}" />
์ปจ๋ฒํฐ๋ฅผ ์ ํ์ฉํ๋ฉด ViewModel์ ๊น๋ํ๊ฒ ์ ์งํ๋ฉด์๋ ๋ค์ํ ํํ์ด ๊ฐ๋ฅํด! ๐จ
๐๏ธ ํ๋ก์ ํธ ๊ตฌ์กฐ ์ค๊ณ
์ค์ ํ๋ก์ ํธ์์ MVVM์ ์ ์ฉํ ๋ ์ด๋ป๊ฒ ํด๋์ ํ์ผ์ ๊ตฌ์กฐํํ๋์ง ์์๋ณด์. ์ข์ ๊ตฌ์กฐ๋ ์ ์ง๋ณด์๋ฅผ ํจ์ฌ ์ฝ๊ฒ ๋ง๋ค์ด์ค! ๐
๊ธฐ๋ณธ ํ๋ก์ ํธ ๊ตฌ์กฐ
์ผ๋ฐ์ ์ผ๋ก ์ด๋ฐ ๊ตฌ์กฐ๋ฅผ ๋ง์ด ์ฌ์ฉํด:
MyWpfApp/
โโโ Models/
โ โโโ User.cs
โ โโโ Product.cs
โ โโโ Order.cs
โโโ ViewModels/
โ โโโ BaseViewModel.cs
โ โโโ MainViewModel.cs
โ โโโ UserViewModel.cs
โ โโโ ProductViewModel.cs
โโโ Views/
โ โโโ MainWindow.xaml
โ โโโ UserView.xaml
โ โโโ ProductView.xaml
โโโ Services/
โ โโโ IDataService.cs
โ โโโ DataService.cs
โ โโโ IDialogService.cs
โ โโโ DialogService.cs
โโโ Commands/
โ โโโ RelayCommand.cs
โ โโโ AsyncRelayCommand.cs
โโโ Converters/
โ โโโ BoolToVisibilityConverter.cs
โ โโโ DateTimeConverter.cs
โโโ Helpers/
โ โโโ ValidationHelper.cs
โโโ App.xaml
๊ฐ ํด๋์ ์ญํ ์ ๋ช ํํ ๊ตฌ๋ถํ๋ฉด ํ ์์ ํ ๋๋ ํผ๋์ด ์์ด! ๐ฅ
๋ ์ด์ด๋ณ ์ฑ
์
๊ฐ ๋ ์ด์ด๊ฐ ์ด๋ค ์ฑ
์์ ๊ฐ์ง๋์ง ์ ๋ฆฌํด๋ณด์:
๐ฑ Views (๋ทฐ)
- XAML ํ์ผ๊ณผ ์ต์ํ์ ์ฝ๋-๋นํ์ธ๋
- UI ๋ ์ด์์๊ณผ ์คํ์ผ ์ ์
- ์ ๋๋ฉ์ด์
๊ณผ ์๊ฐ ํจ๊ณผ
- DataContext ์ค์
โ๏ธ ViewModels (๋ทฐ๋ชจ๋ธ)
- ํ๋ฉด์ ํ์ํ ๋ฐ์ดํฐ ์ค๋น
- ์ฌ์ฉ์ ์
๋ ฅ ์ฒ๋ฆฌ (์ปค๋งจ๋)
- ์ ํจ์ฑ ๊ฒ์ฌ
- Model๊ณผ View ์ฌ์ด์ ๋ฐ์ดํฐ ๋ณํ
๐พ Models (๋ชจ๋ธ)
- ๋น์ฆ๋์ค ์ํฐํฐ ์ ์
- ๋ฐ์ดํฐ ๊ตฌ์กฐ
- ๋น์ฆ๋์ค ๊ท์น
- ๋ฐ์ดํฐ ์ ํจ์ฑ ๊ฒ์ฌ ๋ก์ง
๐ง Services (์๋น์ค)
- ๋ฐ์ดํฐ ์ก์ธ์ค (๋ฐ์ดํฐ๋ฒ ์ด์ค, API)
- ํ์ผ I/O
- ๋ํ์์ ํ์
- ๋ค๋น๊ฒ์ด์
- ๋ก๊น
๐ก ์ค๋ฌด ํ
ํ๋ก์ ํธ๊ฐ ์ปค์ง๋ฉด ๊ฐ ํด๋ ์์ ํ์ ํด๋๋ฅผ ๋ง๋ค์ด์ ๋ ์ธ๋ถํํ ์ ์์ด. ์๋ฅผ ๋ค์ด:
- Views/User/ (์ฌ์ฉ์ ๊ด๋ จ ๋ทฐ๋ค)
- Views/Product/ (์ ํ ๊ด๋ จ ๋ทฐ๋ค)
- ViewModels/User/ (์ฌ์ฉ์ ๊ด๋ จ ๋ทฐ๋ชจ๋ธ๋ค)
์ด๋ ๊ฒ ๊ธฐ๋ฅ๋ณ๋ก ๊ทธ๋ฃนํํ๋ฉด ์ฐพ๊ธฐ๊ฐ ํจ์ฌ ์ฌ์์ ธ! ๐
๐ ์์กด์ฑ ์ฃผ์ (Dependency Injection)
์์กด์ฑ ์ฃผ์ ์ MVVM ์ํคํ ์ฒ๋ฅผ ๋์ฑ ๊ฐ๋ ฅํ๊ฒ ๋ง๋ค์ด์ฃผ๋ ํจํด์ด์ผ. ์ฝ๋์ ๊ฒฐํฉ๋๋ฅผ ๋ฎ์ถ๊ณ ํ ์คํธ ๊ฐ๋ฅ์ฑ์ ๋์ฌ์ค! ๐
์ ์์กด์ฑ ์ฃผ์
์ด ํ์ํ ๊น?
์๋ฅผ ๋ค์ด, ViewModel์์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ๊ทผํด์ผ ํ๋ค๊ณ ์๊ฐํด๋ณด์. ์ง์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ํด๋์ค๋ฅผ ์์ฑํ๋ฉด:
// โ ๋์ ์
public class UserViewModel : BaseViewModel
{
private DatabaseService _database;
public UserViewModel()
{
_database = new DatabaseService(); // ๊ฐํ ๊ฒฐํฉ!
}
}
์ด๋ ๊ฒ ํ๋ฉด ๋ช ๊ฐ์ง ๋ฌธ์ ๊ฐ ์๊ฒจ:
1๏ธโฃ ๋จ์ ํ
์คํธ๊ฐ ์ด๋ ค์ (์ค์ DB๊ฐ ํ์)
2๏ธโฃ ๋ค๋ฅธ ๋ฐ์ดํฐ ์์ค๋ก ๋ฐ๊พธ๊ธฐ ์ด๋ ค์
3๏ธโฃ ViewModel์ด ๊ตฌ์ฒด์ ์ธ ๊ตฌํ์ ์์กดํด
๋์ ์ธํฐํ์ด์ค๋ฅผ ์ฌ์ฉํ๊ณ ์ธ๋ถ์์ ์ฃผ์
๋ฐ์:
// โ
์ข์ ์
public interface IDataService
{
Task<List<User>> GetUsersAsync();
Task SaveUserAsync(User user);
}
public class UserViewModel : BaseViewModel
{
private readonly IDataService _dataService;
public UserViewModel(IDataService dataService)
{
_dataService = dataService; // ์์กด์ฑ ์ฃผ์
!
}
public async Task LoadUsers()
{
var users = await _dataService.GetUsersAsync();
// ...
}
}
์ด์ ํ ์คํธํ ๋ ๊ฐ์ง(Mock) ์๋น์ค๋ฅผ ์ฃผ์ ํ ์ ์์ด! ๐ญ
DI ์ปจํ
์ด๋ ์ฌ์ฉํ๊ธฐ
์๋์ผ๋ก ์์กด์ฑ์ ๊ด๋ฆฌํ๋ ๊ฑด ๋ณต์กํด. DI ์ปจํ
์ด๋๋ฅผ ์ฌ์ฉํ๋ฉด ์๋์ผ๋ก ์ฒ๋ฆฌํด์ค! Microsoft.Extensions.DependencyInjection์ ์ฌ์ฉํด๋ณด์:
using Microsoft.Extensions.DependencyInjection;
public partial class App : Application
{
private ServiceProvider _serviceProvider;
public App()
{
var services = new ServiceCollection();
ConfigureServices(services);
_serviceProvider = services.BuildServiceProvider();
}
private void ConfigureServices(IServiceCollection services)
{
// Services ๋ฑ๋ก
services.AddSingleton<IDataService, DatabaseService>();
services.AddSingleton<IDialogService, DialogService>();
// ViewModels ๋ฑ๋ก
services.AddTransient<MainViewModel>();
services.AddTransient<UserViewModel>();
// Views ๋ฑ๋ก
services.AddTransient<MainWindow>();
}
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
var mainWindow = _serviceProvider.GetRequiredService<MainWindow>();
mainWindow.Show();
}
}
์ด์ MainWindow์ ์์ฑ์์์ ViewModel์ ์ฃผ์ ๋ฐ์ ์ ์์ด:
public partial class MainWindow : Window
{
public MainWindow(MainViewModel viewModel)
{
InitializeComponent();
this.DataContext = viewModel;
}
}
์์ ์๋ํ! ๐ค DI ์ปจํ ์ด๋๊ฐ ์์์ ํ์ํ ์์กด์ฑ๋ค์ ์์ฑํ๊ณ ์ฃผ์ ํด์ค.
์๋น์ค ์๋ช
(Service Lifetime)
DI ์ปจํ
์ด๋์ ์๋น์ค๋ฅผ ๋ฑ๋กํ ๋ ์ธ ๊ฐ์ง ์๋ช
์ค ํ๋๋ฅผ ์ ํํ ์ ์์ด:
| ์๋ช | ์ค๋ช | ์ฌ์ฉ ์๊ธฐ |
|---|---|---|
| Singleton | ์ฑ ์ ์ฒด์์ ํ๋์ ์ธ์คํด์ค๋ง | ์ค์ , ๋ก๊น , ์บ์ ๋ฑ |
| Transient | ์์ฒญํ ๋๋ง๋ค ์ ์ธ์คํด์ค | ViewModel, ๊ฐ๋ฒผ์ด ์๋น์ค |
| Scoped | ๋ฒ์ ๋ด์์ ํ๋์ ์ธ์คํด์ค | ์น ์ฑ์์ ์ฃผ๋ก ์ฌ์ฉ (WPF์์๋ ๋๋ฌผ๊ฒ) |
// Singleton: ์ฑ ์ ์ฒด์์ ํ๋๋ง
services.AddSingleton<IDataService, DatabaseService>();
// Transient: ๋งค๋ฒ ์๋ก ์์ฑ
services.AddTransient<MainViewModel>();
// Scoped: ๋ฒ์ ๋ด์์ ํ๋
services.AddScoped<IUnitOfWork, UnitOfWork>();
โ ๏ธ ์ฃผ์!
Singleton ์๋น์ค๋ ์ฑ์ด ์ข
๋ฃ๋ ๋๊น์ง ๋ฉ๋ชจ๋ฆฌ์ ๋จ์์์ด. ํฐ ๊ฐ์ฒด๋ฅผ Singleton์ผ๋ก ๋ฑ๋กํ๋ฉด ๋ฉ๋ชจ๋ฆฌ ๋์๊ฐ ๋ฐ์ํ ์ ์์ผ๋ ์กฐ์ฌํด! ๐ง
๐งช ํ ์คํธ ๊ฐ๋ฅํ ์ฝ๋ ์์ฑํ๊ธฐ
MVVM ํจํด์ ํฐ ์ฅ์ ์ค ํ๋๋ ํ ์คํธํ๊ธฐ ์ฝ๋ค๋ ๊ฑฐ์ผ! ViewModel์ UI์ ๋ ๋ฆฝ์ ์ด๋๊น ๋จ์ ํ ์คํธ๋ฅผ ์์ฑํ๊ธฐ ์๋ฒฝํด. ๐ฏ
ViewModel ๋จ์ ํ
์คํธ
xUnit๊ณผ Moq๋ฅผ ์ฌ์ฉํด์ ViewModel์ ํ
์คํธํด๋ณด์:
using Xunit;
using Moq;
using System.Threading.Tasks;
public class UserViewModelTests
{
[Fact]
public async Task LoadUsers_ShouldPopulateUsersList()
{
// Arrange
var mockDataService = new Mock<IDataService>();
mockDataService.Setup(x => x.GetUsersAsync())
.ReturnsAsync(new List<User>
{
new User { Name = "ํ๊ธธ๋", Age = 25 },
new User { Name = "๊น์ฒ ์", Age = 30 }
});
var viewModel = new UserViewModel(mockDataService.Object);
// Act
await viewModel.LoadUsersCommand.Execute(null);
// Assert
Assert.Equal(2, viewModel.Users.Count);
Assert.Equal("ํ๊ธธ๋", viewModel.Users[0].Name);
}
[Fact]
public void UserName_WhenChanged_ShouldRaisePropertyChanged()
{
// Arrange
var mockDataService = new Mock<IDataService>();
var viewModel = new UserViewModel(mockDataService.Object);
bool propertyChangedRaised = false;
viewModel.PropertyChanged += (sender, e) =>
{
if (e.PropertyName == nameof(viewModel.UserName))
propertyChangedRaised = true;
};
// Act
viewModel.UserName = "์์ด๋ฆ";
// Assert
Assert.True(propertyChangedRaised);
Assert.Equal("์์ด๋ฆ", viewModel.UserName);
}
}
๋ณด๋ค์ํผ UI ์์ด๋ ViewModel์ ๋ชจ๋ ๊ธฐ๋ฅ์ ํ ์คํธํ ์ ์์ด! ์ด๊ฒ MVVM์ ์ง์ ํ ํ์ด์ผ. ๐ช
์ปค๋งจ๋ ํ
์คํธ
์ปค๋งจ๋์ ์คํ๊ณผ CanExecute๋ ํ
์คํธํ ์ ์์ด:
[Fact]
public void SaveCommand_WhenUserNameIsEmpty_ShouldNotExecute()
{
// Arrange
var mockDataService = new Mock<IDataService>();
var viewModel = new UserViewModel(mockDataService.Object);
viewModel.UserName = "";
// Act
bool canExecute = viewModel.SaveCommand.CanExecute(null);
// Assert
Assert.False(canExecute);
}
[Fact]
public async Task SaveCommand_WhenExecuted_ShouldCallDataService()
{
// Arrange
var mockDataService = new Mock<IDataService>();
var viewModel = new UserViewModel(mockDataService.Object);
viewModel.UserName = "ํ๊ธธ๋";
// Act
viewModel.SaveCommand.Execute(null);
await Task.Delay(100); // ๋น๋๊ธฐ ์์
๋๊ธฐ
// Assert
mockDataService.Verify(x => x.SaveUserAsync(It.IsAny<User>()), Times.Once);
}
๐ก ํ
์คํธ ์์ฑ ํ
1๏ธโฃ ๊ฐ ํ
์คํธ๋ ํ๋์ ๊ธฐ๋ฅ๋ง ๊ฒ์ฆํด์ผ ํด
2๏ธโฃ ํ
์คํธ ์ด๋ฆ์ ๋ช
ํํ๊ฒ (๋ฌด์์_์ธ์ _์ด๋ป๊ฒ)
3๏ธโฃ AAA ํจํด ์ฌ์ฉ: Arrange(์ค๋น), Act(์คํ), Assert(๊ฒ์ฆ)
4๏ธโฃ Mock ๊ฐ์ฒด๋ฅผ ํ์ฉํด์ ์ธ๋ถ ์์กด์ฑ ์ ๊ฑฐ
5๏ธโฃ ํ
์คํธ๋ ๋น ๋ฅด๊ณ ๋
๋ฆฝ์ ์ด์ด์ผ ํด! โก
๐ญ ๋ํ์์์ ๋ค๋น๊ฒ์ด์ ์ฒ๋ฆฌ
MVVM์์ ๊ฐ์ฅ ๊น๋ค๋ก์ด ๋ถ๋ถ ์ค ํ๋๊ฐ ๋ํ์์์ ๋ค๋น๊ฒ์ด์ ์ฒ๋ฆฌ์ผ. ViewModel์ View๋ฅผ ๋ชฐ๋ผ์ผ ํ๋๋ฐ, ์ด๋ป๊ฒ ๋ํ์์๋ฅผ ๋์ธ๊น? ๐ค
Dialog Service ํจํด
์ธํฐํ์ด์ค๋ฅผ ํตํด ๋ํ์์๋ฅผ ์ถ์ํํ์:
public interface IDialogService
{
void ShowMessage(string message, string title = "์๋ฆผ");
bool ShowConfirmation(string message, string title = "ํ์ธ");
string ShowOpenFileDialog(string filter = "๋ชจ๋ ํ์ผ|*.*");
string ShowSaveFileDialog(string filter = "๋ชจ๋ ํ์ผ|*.*");
}
public class DialogService : IDialogService
{
public void ShowMessage(string message, string title = "์๋ฆผ")
{
MessageBox.Show(message, title, MessageBoxButton.OK, MessageBoxImage.Information);
}
public bool ShowConfirmation(string message, string title = "ํ์ธ")
{
var result = MessageBox.Show(message, title, MessageBoxButton.YesNo, MessageBoxImage.Question);
return result == MessageBoxResult.Yes;
}
public string ShowOpenFileDialog(string filter = "๋ชจ๋ ํ์ผ|*.*")
{
var dialog = new OpenFileDialog { Filter = filter };
return dialog.ShowDialog() == true ? dialog.FileName : null;
}
public string ShowSaveFileDialog(string filter = "๋ชจ๋ ํ์ผ|*.*")
{
var dialog = new SaveFileDialog { Filter = filter };
return dialog.ShowDialog() == true ? dialog.FileName : null;
}
}
ViewModel์์ ์ฌ์ฉ:
public class MainViewModel : BaseViewModel
{
private readonly IDialogService _dialogService;
public MainViewModel(IDialogService dialogService)
{
_dialogService = dialogService;
}
private void DeleteUser()
{
bool confirmed = _dialogService.ShowConfirmation(
"์ ๋ง ์ญ์ ํ์๊ฒ ์ต๋๊น?",
"์ญ์ ํ์ธ"
);
if (confirmed)
{
// ์ญ์ ๋ก์ง
_dialogService.ShowMessage("์ญ์ ๋์์ต๋๋ค.");
}
}
}
์ด์ ViewModel์ ํ ์คํธํ ๋ Mock DialogService๋ฅผ ์ฃผ์ ํ๋ฉด ๋ผ! ๐
Navigation Service
ํ๋ฉด ์ ํ๋ ์๋น์ค๋ก ์ถ์ํํ ์ ์์ด:
public interface INavigationService
{
void NavigateTo<TViewModel>() where TViewModel : BaseViewModel;
void NavigateTo<TViewModel>(object parameter) where TViewModel : BaseViewModel;
void GoBack();
bool CanGoBack { get; }
}
public class NavigationService : INavigationService
{
private readonly IServiceProvider _serviceProvider;
private readonly Frame _frame;
private readonly Stack<Type> _navigationStack;
public NavigationService(IServiceProvider serviceProvider, Frame frame)
{
_serviceProvider = serviceProvider;
_frame = frame;
_navigationStack = new Stack<Type>();
}
public void NavigateTo<TViewModel>() where TViewModel : BaseViewModel
{
var viewModel = _serviceProvider.GetRequiredService<TViewModel>();
var viewType = GetViewTypeForViewModel(typeof(TViewModel));
var view = (Page)Activator.CreateInstance(viewType);
view.DataContext = viewModel;
_navigationStack.Push(typeof(TViewModel));
_frame.Navigate(view);
}
public void GoBack()
{
if (CanGoBack)
{
_navigationStack.Pop();
_frame.GoBack();
}
}
public bool CanGoBack => _navigationStack.Count > 1;
private Type GetViewTypeForViewModel(Type viewModelType)
{
// ViewModel ์ด๋ฆ์์ View ์ด๋ฆ ์ ์ถ
var viewName = viewModelType.Name.Replace("ViewModel", "View");
var viewType = Type.GetType($"MyApp.Views.{viewName}");
return viewType;
}
}
์ฌ์ฉ ์์:
public class MainViewModel : BaseViewModel
{
private readonly INavigationService _navigationService;
public ICommand NavigateToUserCommand { get; }
public MainViewModel(INavigationService navigationService)
{
_navigationService = navigationService;
NavigateToUserCommand = new RelayCommand(_ => NavigateToUser());
}
private void NavigateToUser()
{
_navigationService.NavigateTo<UserViewModel>();
}
}
โ ๏ธ ์ค๋ฌด ๊ณ ๋ ค์ฌํญ
Navigation Service๋ฅผ ๊ตฌํํ ๋๋ ๋ค์์ ๊ณ ๋ คํด์ผ ํด:
- ๋ค๋ก ๊ฐ๊ธฐ ์คํ ๊ด๋ฆฌ
- ํ์ด์ง ๊ฐ ๋ฐ์ดํฐ ์ ๋ฌ
- ๋ค๋น๊ฒ์ด์
์ด๋ฒคํธ (์ง์
/์ดํ)
- ๋ฉ๋ชจ๋ฆฌ ๊ด๋ฆฌ (ํ์ด์ง ์บ์ฑ)
๋ณต์กํ ๋ค๋น๊ฒ์ด์
์ด ํ์ํ๋ค๋ฉด Prism ๊ฐ์ ํ๋ ์์ํฌ ์ฌ์ฉ์ ๊ณ ๋ คํด๋ด! ๐
๐ MVVM ํ๋ ์์ํฌ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
์ฒ์๋ถํฐ ๋ชจ๋ ๊ฑธ ์ง์ ๋ง๋ค ํ์๋ ์์ด! ๊ฒ์ฆ๋ MVVM ํ๋ ์์ํฌ๋ค์ด ๋ง์ด ์๊ฑฐ๋ . ๊ฐ๊ฐ์ ํน์ง์ ์์๋ณด์. ๐ ๏ธ
1๏ธโฃ Prism
๊ฐ์ฅ ์ธ๊ธฐ ์๋ WPF MVVM ํ๋ ์์ํฌ์ผ. Microsoft Patterns & Practices ํ์ด ๋ง๋ค์์ด.
์ฃผ์ ๊ธฐ๋ฅ:
โ
๊ฐ๋ ฅํ DI ์ปจํ
์ด๋ ํตํฉ
โ
๋ชจ๋ํ ์์คํ
โ
์ด๋ฒคํธ ์ ๊ทธ๋ฆฌ๊ฒ์ดํฐ
โ
๋ค๋น๊ฒ์ด์
์๋น์ค
โ
๋ํ์์ ์๋น์ค
โ
์ปค๋งจ๋ ๊ตฌํ์ฒด ์ ๊ณต
// Prism ์ฌ์ฉ ์์
public class MainViewModel : BindableBase
{
private string _title;
public string Title
{
get => _title;
set => SetProperty(ref _title, value);
}
public DelegateCommand SaveCommand { get; }
public MainViewModel()
{
SaveCommand = new DelegateCommand(ExecuteSave, CanExecuteSave)
.ObservesProperty(() => Title);
}
private void ExecuteSave()
{
// ์ ์ฅ ๋ก์ง
}
private bool CanExecuteSave()
{
return !string.IsNullOrEmpty(Title);
}
}
์ฅ์ : ๊ธฐ๋ฅ์ด ํ๋ถํ๊ณ ๋๊ท๋ชจ ํ๋ก์ ํธ์ ์ ํฉ
๋จ์ : ํ์ต ๊ณก์ ์ด ์๊ณ ํ๋ก์ ํธ๊ฐ ๋ฌด๊ฑฐ์์ง ์ ์์ด
2๏ธโฃ MVVM Light Toolkit
๊ฐ๋ณ๊ณ ๋ฐฐ์ฐ๊ธฐ ์ฌ์ด ํ๋ ์์ํฌ์ผ. ์ต๊ทผ์๋ ๊ฐ๋ฐ์ด ์ค๋จ๋์์ง๋ง ์ฌ์ ํ ๋ง์ด ์ฌ์ฉ๋ผ.
์ฃผ์ ๊ธฐ๋ฅ:
โ
ViewModelBase ํด๋์ค
โ
RelayCommand ๊ตฌํ
โ
Messenger (์ด๋ฒคํธ ๋ฒ์ค)
โ
๊ฐ๋จํ DI ์ปจํ
์ด๋
โ
๋์์ธ ํ์ ๋ฐ์ดํฐ ์ง์
// MVVM Light ์ฌ์ฉ ์์
public class MainViewModel : ViewModelBase
{
private string _userName;
public string UserName
{
get => _userName;
set => Set(ref _userName, value);
}
public RelayCommand SaveCommand { get; }
public MainViewModel()
{
SaveCommand = new RelayCommand(Save, CanSave);
}
}
์ฅ์ : ๊ฐ๋ณ๊ณ ๋ฐฐ์ฐ๊ธฐ ์ฌ์
๋จ์ : ๋ ์ด์ ์
๋ฐ์ดํธ๋์ง ์์
3๏ธโฃ CommunityToolkit.Mvvm (MVVM Toolkit)
Microsoft๊ฐ ๊ณต์์ ์ผ๋ก ์ง์ํ๋ ์ต์ MVVM ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ผ! MVVM Light์ ์ ์ ์ ํ๊ณ์๋ผ๊ณ ๋ณผ ์ ์์ด. ๐
์ฃผ์ ๊ธฐ๋ฅ:
โ
Source Generator ํ์ฉ (์ฝ๋ ์๋ ์์ฑ)
โ
ObservableObject ๊ธฐ๋ณธ ํด๋์ค
โ
RelayCommand์ AsyncRelayCommand
โ
Messenger (๋ฉ์์ง ์์คํ
)
โ
IoC ์ปจํ
์ด๋ ์ง์
โ
.NET 6+ ์ต์ ํ
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
// Source Generator๊ฐ ์๋์ผ๋ก ์ฝ๋ ์์ฑ!
public partial class MainViewModel : ObservableObject
{
[ObservableProperty]
private string _userName;
[ObservableProperty]
private int _age;
[RelayCommand]
private void Save()
{
// ์ ์ฅ ๋ก์ง
}
[RelayCommand(CanExecute = nameof(CanDelete))]
private void Delete()
{
// ์ญ์ ๋ก์ง
}
private bool CanDelete() => Age >= 18;
}
์ ์ฝ๋์์ [ObservableProperty] ์ดํธ๋ฆฌ๋ทฐํธ๋ฅผ ๋ถ์ด๋ฉด, Source Generator๊ฐ ์๋์ผ๋ก ์ ์ฒด ์์ฑ ์ฝ๋๋ฅผ ์์ฑํด์ค! ๋ณด์ผ๋ฌํ๋ ์ดํธ ์ฝ๋๊ฐ ํ ์ค์ด๋ค์ง? ๐
์ฅ์ : ์ต์ ๊ธฐ์ , ์ ์ ์ฝ๋, Microsoft ๊ณต์ ์ง์
๋จ์ : .NET 6 ์ด์ ํ์
4๏ธโฃ Caliburn.Micro
์ปจ๋ฒค์
๊ธฐ๋ฐ์ ํ๋ ์์ํฌ์ผ. ๋ช
๋ช
๊ท์น๋ง ๋ฐ๋ฅด๋ฉด ๋ง์ ๊ฑธ ์๋์ผ๋ก ์ฒ๋ฆฌํด์ค.
์ฃผ์ ๊ธฐ๋ฅ:
โ
์ปจ๋ฒค์
๊ธฐ๋ฐ ๋ฐ์ธ๋ฉ
โ
ํ๋ฉด ๊ด๋ฆฌ
โ
์ด๋ฒคํธ ์ ๊ทธ๋ฆฌ๊ฒ์ดํฐ
โ
์ฝ๋ฃจํด ์ง์
โ
๋ถํธ์คํธ๋ํผ
// Caliburn.Micro๋ ๋ช
๋ช
๊ท์น์ผ๋ก ์๋ ๋ฐ์ธ๋ฉ
public class ShellViewModel : Screen
{
private string _name;
public string Name
{
get => _name;
set
{
_name = value;
NotifyOfPropertyChange(() => Name);
NotifyOfPropertyChange(() => CanSayHello);
}
}
// ๋ฒํผ ์ด๋ฆ์ด "SayHello"๋ฉด ์๋์ผ๋ก ์ฐ๊ฒฐ!
public void SayHello()
{
MessageBox.Show($"Hello {Name}!");
}
// Can + ๋ฉ์๋๋ช
= ํ์ฑํ ์กฐ๊ฑด
public bool CanSayHello => !string.IsNullOrEmpty(Name);
}
์ฅ์ : ์ฝ๋๊ฐ ๊ฐ๊ฒฐํ๊ณ ์ง๊ด์
๋จ์ : ์ปจ๋ฒค์
์ ๋ฐฐ์์ผ ํ๊ณ , ๋๋ฒ๊น
์ด ์ด๋ ค์ธ ์ ์์ด
๐ก ์ด๋ค ๊ฑธ ์ ํํด์ผ ํ ๊น?
์๊ท๋ชจ ํ๋ก์ ํธ: CommunityToolkit.Mvvm (๊ฐ๋ณ๊ณ ํ๋์ )
์ค๋๊ท๋ชจ ํ๋ก์ ํธ: Prism (๊ธฐ๋ฅ์ด ํ๋ถ)
๋น ๋ฅธ ํ๋กํ ํ์
: Caliburn.Micro (์ปจ๋ฒค์
๊ธฐ๋ฐ)
ํ์ต ๋ชฉ์ : ์ง์ ๊ตฌํํด๋ณด๊ธฐ! (์๋ฆฌ ์ดํด์ ์ต๊ณ ) ๐
๊ฐ์ธ์ ์ผ๋ก๋ CommunityToolkit.Mvvm์ ์ถ์ฒํด! Microsoft๊ฐ ๊ณต์ ์ง์ํ๊ณ , Source Generator๋ก ์์ฐ์ฑ์ด ์ ๋ง ์ข๊ฑฐ๋ . ๐
๐จ ๊ณ ๊ธ XAML ๋ฐ์ธ๋ฉ ํ ํฌ๋
XAML ๋ฐ์ธ๋ฉ์ ๋ง์คํฐํ๋ฉด ์ ๋ง ๊ฐ๋ ฅํ UI๋ฅผ ๋ง๋ค ์ ์์ด! ๊ณ ๊ธ ํ ํฌ๋๋ค์ ์์๋ณด์. ๐
MultiBinding (๋ค์ค ๋ฐ์ธ๋ฉ)
์ฌ๋ฌ ์์ฑ์ ํ๋๋ก ํฉ์ณ์ ํ์ํ ์ ์์ด:
<!-- Converter ์ ์ -->
public class FullNameConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values.Length == 2 && values[0] is string firstName && values[1] is string lastName)
{
return $"{firstName} {lastName}";
}
return string.Empty;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
<!-- XAML์์ ์ฌ์ฉ -->
<Window.Resources>
<local:FullNameConverter x:Key="FullNameConverter" />
</Window.Resources>
<TextBlock>
<TextBlock.Text>
<MultiBinding Converter="{StaticResource FullNameConverter}">
<Binding Path="FirstName" />
<Binding Path="LastName" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
์ด๋ ๊ฒ ํ๋ฉด FirstName๊ณผ LastName์ด ๋ณ๊ฒฝ๋ ๋๋ง๋ค ์๋์ผ๋ก ์ ์ฒด ์ด๋ฆ์ด ์ ๋ฐ์ดํธ๋ผ! โจ
StringFormat (๋ฌธ์์ด ํฌ๋งท)
๊ฐ๋จํ ํฌ๋งทํ
์ Converter ์์ด๋ ๊ฐ๋ฅํด:
<!-- ์ซ์ ํฌ๋งท -->
<TextBlock Text="{Binding Price, StringFormat=๊ฐ๊ฒฉ: {0:C}}" />
<!-- ์ถ๋ ฅ: ๊ฐ๊ฒฉ: โฉ10,000 -->
<!-- ๋ ์ง ํฌ๋งท -->
<TextBlock Text="{Binding BirthDate, StringFormat=์๋
์์ผ: {0:yyyy-MM-dd}}" />
<!-- ์ถ๋ ฅ: ์๋
์์ผ: 1990-01-01 -->
<!-- ํผ์ผํธ -->
<TextBlock Text="{Binding Progress, StringFormat={}{0:P0}}" />
<!-- ์ถ๋ ฅ: 75% -->
<!-- ์ฌ๋ฌ ๊ฐ ์กฐํฉ -->
<TextBlock>
<TextBlock.Text>
<MultiBinding StringFormat="{}{0}๋์ ๋์ด๋ {1}์ธ์
๋๋ค.">
<Binding Path="Name" />
<Binding Path="Age" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
StringFormat์ ์ฌ์ฉํ๋ฉด ๊ฐ๋จํ ํฌ๋งทํ ์ ์ํด Converter๋ฅผ ๋ง๋ค ํ์๊ฐ ์์ด! ํธํ์ง? ๐
FallbackValue์ TargetNullValue
๋ฐ์ธ๋ฉ์ด ์คํจํ๊ฑฐ๋ null์ผ ๋ ํ์ํ ๊ฐ์ ์ง์ ํ ์ ์์ด:
<!-- ๋ฐ์ธ๋ฉ ์คํจ ์ ๊ธฐ๋ณธ๊ฐ -->
<TextBlock Text="{Binding UserName, FallbackValue=์ ์ ์์}" />
<!-- null์ผ ๋ ํ์ํ ๊ฐ -->
<TextBlock Text="{Binding Description, TargetNullValue=์ค๋ช
์์}" />
<!-- ๋ ๋ค ์ฌ์ฉ -->
<TextBlock Text="{Binding Email,
TargetNullValue=์ด๋ฉ์ผ ์์,
FallbackValue=์ค๋ฅ}" />
์ด๋ ๊ฒ ํ๋ฉด null ์ฒดํฌ ์ฝ๋๋ฅผ ViewModel์ ์์ฑํ ํ์๊ฐ ์์ด! ๐ฏ
RelativeSource (์๋ ์์ค)
ํ์ฌ ์์๋ฅผ ๊ธฐ์ค์ผ๋ก ์๋์ ์ธ ์์น์ ์์๋ฅผ ๋ฐ์ธ๋ฉํ ์ ์์ด:
<!-- ์๊ธฐ ์์ ์ ๋ค๋ฅธ ์์ฑ ๋ฐ์ธ๋ฉ -->
<TextBox x:Name="myTextBox"
Text="Hello"
ToolTip="{Binding RelativeSource={RelativeSource Self}, Path=Text}" />
<!-- ๋ถ๋ชจ ์์ ๋ฐ์ธ๋ฉ -->
<Grid x:Name="parentGrid" Tag="ParentData">
<TextBlock Text="{Binding RelativeSource={RelativeSource AncestorType=Grid}, Path=Tag}" />
</Grid>
<!-- DataTemplate ์์์ ๋ถ๋ชจ ViewModel ์ ๊ทผ -->
<ListBox ItemsSource="{Binding Users}">
<ListBox.ItemTemplate>
<DataTemplate>
<Button Content="์ญ์ "
Command="{Binding DataContext.DeleteCommand,
RelativeSource={RelativeSource AncestorType=ListBox}}"
CommandParameter="{Binding}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
RelativeSource๋ ํนํ DataTemplate ์์์ ๋ถ๋ชจ ViewModel์ ์ปค๋งจ๋๋ฅผ ํธ์ถํ ๋ ์ ์ฉํด! ๐ก
ValidationRules (์ ํจ์ฑ ๊ฒ์ฌ)
XAML์์ ์ง์ ์ ํจ์ฑ ๊ฒ์ฌ ๊ท์น์ ์ ์ํ ์ ์์ด:
// ์ปค์คํ
ValidationRule
public class AgeValidationRule : ValidationRule
{
public int MinAge { get; set; } = 0;
public int MaxAge { get; set; } = 150;
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
if (int.TryParse(value?.ToString(), out int age))
{
if (age < MinAge || age > MaxAge)
{
return new ValidationResult(false,
$"๋์ด๋ {MinAge}์์ {MaxAge} ์ฌ์ด์ฌ์ผ ํฉ๋๋ค.");
}
return ValidationResult.ValidResult;
}
return new ValidationResult(false, "์ฌ๋ฐ๋ฅธ ์ซ์๋ฅผ ์
๋ ฅํ์ธ์.");
}
}
// XAML์์ ์ฌ์ฉ
<TextBox>
<TextBox.Text>
<Binding Path="Age" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<local:AgeValidationRule MinAge="18" MaxAge="100" />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
<TextBox.Style>
<Style TargetType="TextBox">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="True">
<Setter Property="ToolTip"
Value="{Binding RelativeSource={RelativeSource Self},
Path=(Validation.Errors)[0].ErrorContent}" />
<Setter Property="BorderBrush" Value="Red" />
</Trigger>
</Style.Triggers>
</Style>
</TextBox.Style>
</TextBox>
์ ํจ์ฑ ๊ฒ์ฌ ์ค๋ฅ๊ฐ ์์ผ๋ฉด ์๋์ผ๋ก ๋นจ๊ฐ ํ ๋๋ฆฌ๊ฐ ํ์๋๊ณ ํดํ์ ์ค๋ฅ ๋ฉ์์ง๊ฐ ๋ํ๋! ๐ด
โก ์ฑ๋ฅ ์ต์ ํ ์ ๋ต
MVVM ํจํด์ ์ฌ์ฉํ๋ฉด์ ์ฑ๋ฅ์ ์ต์ ํํ๋ ๋ฐฉ๋ฒ๋ค์ ์์๋ณด์. ๋๊ท๋ชจ ๋ฐ์ดํฐ๋ฅผ ๋ค๋ฃฐ ๋ ํนํ ์ค์ํด! ๐
๊ฐ์ํ (Virtualization)
์์ฒ, ์๋ง ๊ฐ์ ํญ๋ชฉ์ ํ์ํ ๋๋ ๊ฐ์ํ๊ฐ ํ์์ผ:
<!-- ListBox ๊ฐ์ํ -->
<ListBox ItemsSource="{Binding LargeDataSet}"
VirtualizingPanel.IsVirtualizing="True"
VirtualizingPanel.VirtualizationMode="Recycling"
VirtualizingPanel.CacheLength="20,20"
VirtualizingPanel.CacheLengthUnit="Item">
<!-- ItemTemplate -->
</ListBox>
<!-- DataGrid ๊ฐ์ํ -->
<DataGrid ItemsSource="{Binding Users}"
EnableRowVirtualization="True"
EnableColumnVirtualization="True"
VirtualizingPanel.IsVirtualizing="True"
VirtualizingPanel.VirtualizationMode="Recycling" />
๊ฐ์ํ๋ฅผ ์ฌ์ฉํ๋ฉด ํ๋ฉด์ ๋ณด์ด๋ ํญ๋ชฉ๋ง ๋ ๋๋งํด์ ๋ฉ๋ชจ๋ฆฌ์ ์ฑ๋ฅ์ด ํฌ๊ฒ ๊ฐ์ ๋ผ! 10,000๊ฐ ํญ๋ชฉ๋ ๋ถ๋๋ฝ๊ฒ ์คํฌ๋กคํ ์ ์์ด. ๐ข
๋น๋๊ธฐ ๋ฐ์ดํฐ ๋ก๋ฉ
๋ฌด๊ฑฐ์ด ์์
์ ๋ฐ๋์ ๋น๋๊ธฐ๋ก ์ฒ๋ฆฌํด์ผ ํด:
public class MainViewModel : BaseViewModel
{
private ObservableCollection<User> _users;
public ObservableCollection<User> Users
{
get => _users;
set => SetProperty(ref _users, value);
}
private bool _isLoading;
public bool IsLoading
{
get => _isLoading;
set => SetProperty(ref _isLoading, value);
}
public ICommand LoadDataCommand { get; }
public MainViewModel(IDataService dataService)
{
LoadDataCommand = new AsyncRelayCommand(async () => await LoadDataAsync());
}
private async Task LoadDataAsync()
{
IsLoading = true;
try
{
var users = await _dataService.GetUsersAsync();
// UI ์ค๋ ๋์์ ์คํ
await Application.Current.Dispatcher.InvokeAsync(() =>
{
Users = new ObservableCollection<User>(users);
});
}
finally
{
IsLoading = false;
}
}
}
XAML์์ ๋ก๋ฉ ํ์:
<Grid>
<ListBox ItemsSource="{Binding Users}"
Visibility="{Binding IsLoading, Converter={StaticResource BoolToVisibilityInverse}}" />
<ProgressBar IsIndeterminate="True"
Visibility="{Binding IsLoading, Converter={StaticResource BoolToVisibility}}" />
</Grid>
PropertyChanged ์ด๋ฒคํธ ์ต์ ํ
๋ถํ์ํ PropertyChanged ์ด๋ฒคํธ๋ฅผ ์ค์ด์:
// โ ๋์ ์: ๋งค๋ฒ ์ด๋ฒคํธ ๋ฐ์
private string _name;
public string Name
{
get => _name;
set
{
_name = value;
OnPropertyChanged(); // ๊ฐ์ด ๊ฐ์๋ ๋ฐ์!
}
}
// โ
์ข์ ์: ๊ฐ์ด ์ค์ ๋ก ๋ณ๊ฒฝ๋ ๋๋ง
private string _name;
public string Name
{
get => _name;
set => SetProperty(ref _name, value); // ๊ฐ์ด ๋ค๋ฅผ ๋๋ง ์ด๋ฒคํธ ๋ฐ์
}
// โ
๋ ์ข์ ์: ์ฌ๋ฌ ์์ฑ์ ํ ๋ฒ์ ์
๋ฐ์ดํธ
public void UpdateUser(string name, int age, string email)
{
// ์ด๋ฒคํธ๋ฅผ ์ผ์์ ์ผ๋ก ์ค๋จ
_suspendNotifications = true;
Name = name;
Age = age;
Email = email;
_suspendNotifications = false;
// ํ ๋ฒ์ ์๋ฆผ
OnPropertyChanged(nameof(Name));
OnPropertyChanged(nameof(Age));
OnPropertyChanged(nameof(Email));
}
์ปฌ๋ ์
์
๋ฐ์ดํธ ์ต์ ํ
๋๋์ ํญ๋ชฉ์ ์ถ๊ฐ/์ญ์ ํ ๋๋ ์ผ๊ด ์ฒ๋ฆฌํ์:
// โ ๋์ ์: ํ๋์ฉ ์ถ๊ฐ (๋งค๋ฒ UI ์
๋ฐ์ดํธ)
foreach (var user in newUsers)
{
Users.Add(user); // 1000๋ฒ UI ์
๋ฐ์ดํธ!
}
// โ
์ข์ ์: ์ผ๊ด ์ถ๊ฐ
public class RangeObservableCollection<T> : ObservableCollection<T>
{
public void AddRange(IEnumerable<T> items)
{
CheckReentrancy();
foreach (var item in items)
{
Items.Add(item);
}
OnCollectionChanged(new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Reset));
}
public void RemoveRange(IEnumerable<T> items)
{
CheckReentrancy();
foreach (var item in items)
{
Items.Remove(item);
}
OnCollectionChanged(new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Reset));
}
}
// ์ฌ์ฉ
Users.AddRange(newUsers); // ํ ๋ฒ๋ง UI ์
๋ฐ์ดํธ!
๋ฉ๋ชจ๋ฆฌ ๋์ ๋ฐฉ์ง
์ด๋ฒคํธ ํธ๋ค๋ฌ์ ์ฝํ ์ฐธ์กฐ๋ฅผ ์ฃผ์ํด์ผ ํด:
// โ ๋ฉ๋ชจ๋ฆฌ ๋์ ์ํ
public class MainViewModel : BaseViewModel
{
public MainViewModel()
{
// ๊ฐํ ์ฐธ์กฐ๋ก ์ธํ ๋ฉ๋ชจ๋ฆฌ ๋์!
SomeStaticEvent.EventOccurred += OnEventOccurred;
}
private void OnEventOccurred(object sender, EventArgs e)
{
// ...
}
}
// โ
์ด๋ฒคํธ ํด์
public class MainViewModel : BaseViewModel, IDisposable
{
public MainViewModel()
{
SomeStaticEvent.EventOccurred += OnEventOccurred;
}
public void Dispose()
{
SomeStaticEvent.EventOccurred -= OnEventOccurred;
}
}
// โ
์ฝํ ์ด๋ฒคํธ ํจํด ์ฌ์ฉ
public class MainViewModel : BaseViewModel
{
public MainViewModel()
{
WeakEventManager<SomeClass, EventArgs>
.AddHandler(someInstance, nameof(SomeClass.EventOccurred), OnEventOccurred);
}
}
โ ๏ธ ์ฑ๋ฅ ์ธก์ ํ๊ธฐ
์ต์ ํํ๊ธฐ ์ ์ ๋ฐ๋์ ํ๋กํ์ผ๋ง์ ํด์ผ ํด! ์ถ์ธก์ผ๋ก ์ต์ ํํ๋ฉด ์ ๋ผ.
Visual Studio์ ์ฑ๋ฅ ํ๋กํ์ผ๋ฌ๋ฅผ ์ฌ์ฉํด์:
- CPU ์ฌ์ฉ๋
- ๋ฉ๋ชจ๋ฆฌ ํ ๋น
- UI ์๋ต์ฑ
์ ์ธก์ ํ๊ณ , ์ค์ ๋ณ๋ชฉ ์ง์ ์ ์ฐพ์์ ์ต์ ํํ์! ๐
๐ฏ ์ค์ ํ๋ก์ ํธ ์์
์ง๊ธ๊น์ง ๋ฐฐ์ด ๋ด์ฉ์ ์ข ํฉํด์ ์ค์ ํ๋ก์ ํธ๋ฅผ ๋ง๋ค์ด๋ณด์! ๊ฐ๋จํ ํ ์ผ ๊ด๋ฆฌ ์ฑ์ MVVM ํจํด์ผ๋ก ๊ตฌํํด๋ณผ๊ฒ. ๐
1๋จ๊ณ: Model ์ ์
// Models/TodoItem.cs
public class TodoItem
{
public int Id { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public bool IsCompleted { get; set; }
public DateTime CreatedDate { get; set; }
public DateTime? DueDate { get; set; }
public Priority Priority { get; set; }
}
public enum Priority
{
Low,
Normal,
High,
Urgent
}
// Services/ITodoService.cs
public interface ITodoService
{
Task<List<TodoItem>> GetAllTodosAsync();
Task<TodoItem> GetTodoByIdAsync(int id);
Task AddTodoAsync(TodoItem todo);
Task UpdateTodoAsync(TodoItem todo);
Task DeleteTodoAsync(int id);
}
// Services/TodoService.cs
public class TodoService : ITodoService
{
private readonly List<TodoItem> _todos = new();
private int _nextId = 1;
public Task<List<TodoItem>> GetAllTodosAsync()
{
return Task.FromResult(_todos.ToList());
}
public Task<TodoItem> GetTodoByIdAsync(int id)
{
var todo = _todos.FirstOrDefault(t => t.Id == id);
return Task.FromResult(todo);
}
public Task AddTodoAsync(TodoItem todo)
{
todo.Id = _nextId++;
todo.CreatedDate = DateTime.Now;
_todos.Add(todo);
return Task.CompletedTask;
}
public Task UpdateTodoAsync(TodoItem todo)
{
var existing = _todos.FirstOrDefault(t => t.Id == todo.Id);
if (existing != null)
{
var index = _todos.IndexOf(existing);
_todos[index] = todo;
}
return Task.CompletedTask;
}
public Task DeleteTodoAsync(int id)
{
var todo = _todos.FirstOrDefault(t => t.Id == id);
if (todo != null)
{
_todos.Remove(todo);
}
return Task.CompletedTask;
}
}
2๋จ๊ณ: ViewModel ๊ตฌํ
// ViewModels/TodoItemViewModel.cs
public class TodoItemViewModel : BaseViewModel
{
private readonly TodoItem _model;
public TodoItemViewModel(TodoItem model)
{
_model = model;
}
public int Id => _model.Id;
public string Title
{
get => _model.Title;
set
{
if (_model.Title != value)
{
_model.Title = value;
OnPropertyChanged();
}
}
}
public bool IsCompleted
{
get => _model.IsCompleted;
set
{
if (_model.IsCompleted != value)
{
_model.IsCompleted = value;
OnPropertyChanged();
OnPropertyChanged(nameof(StatusText));
}
}
}
public string StatusText => IsCompleted ? "์๋ฃ" : "์งํ์ค";
public TodoItem GetModel() => _model;
}
// ViewModels/MainViewModel.cs
public class MainViewModel : BaseViewModel
{
private readonly ITodoService _todoService;
private readonly IDialogService _dialogService;
public ObservableCollection<TodoItemViewModel> Todos { get; }
private TodoItemViewModel _selectedTodo;
public TodoItemViewModel SelectedTodo
{
get => _selectedTodo;
set => SetProperty(ref _selectedTodo, value);
}
private string _newTodoTitle;
public string NewTodoTitle
{
get => _newTodoTitle;
set => SetProperty(ref _newTodoTitle, value);
}
public ICommand LoadTodosCommand { get; }
public ICommand AddTodoCommand { get; }
public ICommand DeleteTodoCommand { get; }
public ICommand ToggleCompleteCommand { get; }
public MainViewModel(ITodoService todoService, IDialogService dialogService)
{
_todoService = todoService;
_dialogService = dialogService;
Todos = new ObservableCollection<TodoItemViewModel>();
LoadTodosCommand = new AsyncRelayCommand(LoadTodosAsync);
AddTodoCommand = new AsyncRelayCommand(AddTodoAsync, CanAddTodo);
DeleteTodoCommand = new AsyncRelayCommand<TodoItemViewModel>(DeleteTodoAsync);
ToggleCompleteCommand = new AsyncRelayCommand<TodoItemViewModel>(ToggleCompleteAsync);
// ์ด๊ธฐ ๋ก๋
_ = LoadTodosAsync();
}
private async Task LoadTodosAsync()
{
var todos = await _todoService.GetAllTodosAsync();
Todos.Clear();
foreach (var todo in todos)
{
Todos.Add(new TodoItemViewModel(todo));
}
}
private bool CanAddTodo()
{
return !string.IsNullOrWhiteSpace(NewTodoTitle);
}
private async Task AddTodoAsync()
{
var newTodo = new TodoItem
{
Title = NewTodoTitle,
IsCompleted = false,
Priority = Priority.Normal
};
await _todoService.AddTodoAsync(newTodo);
Todos.Add(new TodoItemViewModel(newTodo));
NewTodoTitle = string.Empty;
_dialogService.ShowMessage("ํ ์ผ์ด ์ถ๊ฐ๋์์ต๋๋ค!");
}
private async Task DeleteTodoAsync(TodoItemViewModel todoVm)
{
if (todoVm == null) return;
bool confirmed = _dialogService.ShowConfirmation(
"์ ๋ง ์ญ์ ํ์๊ฒ ์ต๋๊น?",
"์ญ์ ํ์ธ"
);
if (confirmed)
{
await _todoService.DeleteTodoAsync(todoVm.Id);
Todos.Remove(todoVm);
}
}
private async Task ToggleCompleteAsync(TodoItemViewModel todoVm)
{
if (todoVm == null) return;
todoVm.IsCompleted = !todoVm.IsCompleted;
await _todoService.UpdateTodoAsync(todoVm.GetModel());
}
}
3๋จ๊ณ: View (XAML) ๊ตฌํ
<Window x:Class="TodoApp.Views.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="ํ ์ผ ๊ด๋ฆฌ" Height="600" Width="800">
<Window.Resources>
<BooleanToVisibilityConverter x:Key="BoolToVis" />
</Window.Resources>
<Grid Margin="20">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- ์ ๋ชฉ -->
<TextBlock Grid.Row="0"
Text="๐ ํ ์ผ ๊ด๋ฆฌ"
FontSize="28"
FontWeight="Bold"
Margin="0,0,0,20" />
<!-- ์ ํ ์ผ ์ถ๊ฐ -->
<Grid Grid.Row="1" Margin="0,0,0,20">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBox Grid.Column="0"
Text="{Binding NewTodoTitle, UpdateSourceTrigger=PropertyChanged}"
FontSize="16"
Padding="10"
VerticalContentAlignment="Center">
<TextBox.InputBindings>
<KeyBinding Key="Enter" Command="{Binding AddTodoCommand}" />
</TextBox.InputBindings>
</TextBox>
<Button Grid.Column="1"
Content="โ ์ถ๊ฐ"
Command="{Binding AddTodoCommand}"
Margin="10,0,0,0"
Padding="20,10"
FontSize="16"
Background="#667eea"
Foreground="White"
BorderThickness="0"
Cursor="Hand" />
</Grid>
<!-- ํ ์ผ ๋ชฉ๋ก -->
<ListBox Grid.Row="2"
ItemsSource="{Binding Todos}"
SelectedItem="{Binding SelectedTodo}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListBox.ItemTemplate>
<DataTemplate>
<Border BorderBrush="#e9ecef"
BorderThickness="1"
CornerRadius="8"
Padding="15"
Margin="0,5"
Background="White">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<!-- ์๋ฃ ์ฒดํฌ๋ฐ์ค -->
<CheckBox Grid.Column="0"
IsChecked="{Binding IsCompleted}"
Command="{Binding DataContext.ToggleCompleteCommand,
RelativeSource={RelativeSource AncestorType=ListBox}}"
CommandParameter="{Binding}"
VerticalAlignment="Center"
Margin="0,0,15,0" />
<!-- ์ ๋ชฉ๊ณผ ์ํ -->
<StackPanel Grid.Column="1" VerticalAlignment="Center">
<TextBlock Text="{Binding Title}"
FontSize="16"
FontWeight="SemiBold">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Style.Triggers>
<DataTrigger Binding="{Binding IsCompleted}" Value="True">
<Setter Property="TextDecorations" Value="Strikethrough" />
<Setter Property="Foreground" Value="#adb5bd" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
<TextBlock Text="{Binding StatusText}"
FontSize="12"
Foreground="#6c757d"
Margin="0,5,0,0" />
</StackPanel>
<!-- ์ญ์ ๋ฒํผ -->
<Button Grid.Column="2"
Content="๐๏ธ"
Command="{Binding DataContext.DeleteTodoCommand,
RelativeSource={RelativeSource AncestorType=ListBox}}"
CommandParameter="{Binding}"
Background="Transparent"
BorderThickness="0"
FontSize="18"
Cursor="Hand"
Padding="10" />
</Grid>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Window>
4๋จ๊ณ: ์์กด์ฑ ์ฃผ์ ์ค์
// App.xaml.cs
public partial class App : Application
{
private ServiceProvider _serviceProvider;
public App()
{
var services = new ServiceCollection();
ConfigureServices(services);
_serviceProvider = services.BuildServiceProvider();
}
private void ConfigureServices(IServiceCollection services)
{
// Services
services.AddSingleton<ITodoService, TodoService>();
services.AddSingleton<IDialogService, DialogService>();
// ViewModels
services.AddTransient<MainViewModel>();
// Views
services.AddTransient<MainWindow>();
}
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
var mainWindow = _serviceProvider.GetRequiredService<MainWindow>();
mainWindow.Show();
}
protected override void OnExit(ExitEventArgs e)
{
_serviceProvider?.Dispose();
base.OnExit(e);
}
}
๐ก ์ด ์์ ์์ ๋ฐฐ์ด ๊ฒ๋ค
โ
Model-View-ViewModel ๋ถ๋ฆฌ
โ
์์กด์ฑ ์ฃผ์
ํ์ฉ
โ
๋น๋๊ธฐ ์ปค๋งจ๋ ์ฒ๋ฆฌ
โ
๋ฐ์ดํฐ ๋ฐ์ธ๋ฉ (์๋ฐฉํฅ, ์ปค๋งจ๋ ํ๋ผ๋ฏธํฐ)
โ
RelativeSource๋ฅผ ํตํ ๋ถ๋ชจ ViewModel ์ ๊ทผ
โ
DataTrigger๋ฅผ ํ์ฉํ ์กฐ๊ฑด๋ถ ์คํ์ผ๋ง
โ
ObservableCollection ์ฌ์ฉ
์ด ํจํด์ ๊ธฐ๋ฐ์ผ๋ก ๋ ๋ณต์กํ ๊ธฐ๋ฅ๋ค์ ์ถ๊ฐํ ์ ์์ด! ๐
๐ MVVM ํ์ต ๋ก๋๋งต๊ณผ ๋ฆฌ์์ค
MVVM์ ๋ง์คํฐํ๊ธฐ ์ํ ํ์ต ๊ฒฝ๋ก์ ์ ์ฉํ ๋ฆฌ์์ค๋ค์ ์ ๋ฆฌํด๋ดค์ด. ๋จ๊ณ๋ณ๋ก ์ฐจ๊ทผ์ฐจ๊ทผ ํ์ตํ๋ฉด ๋ผ! ๐
ํ์ต ๋จ๊ณ
๐ฑ ์ด๊ธ (1-2๊ฐ์)
1๏ธโฃ C# ๊ธฐ์ด ๋ฌธ๋ฒ ์๋ฒฝํ ์ดํดํ๊ธฐ
2๏ธโฃ WPF ๊ธฐ๋ณธ ๊ฐ๋
(XAML, ๋ ์ด์์, ์ปจํธ๋กค)
3๏ธโฃ ๋ฐ์ดํฐ ๋ฐ์ธ๋ฉ ๊ธฐ์ด
4๏ธโฃ INotifyPropertyChanged ์ดํดํ๊ณ ๊ตฌํ
5๏ธโฃ ๊ฐ๋จํ ์ปค๋งจ๋ ํจํด ๊ตฌํ
6๏ธโฃ ๊ธฐ๋ณธ์ ์ธ MVVM ๊ตฌ์กฐ๋ก ๊ฐ๋จํ ์ฑ ๋ง๋ค๊ธฐ
๐ฟ ์ค๊ธ (2-3๊ฐ์)
1๏ธโฃ ์์กด์ฑ ์ฃผ์
์ดํด์ ์ ์ฉ
2๏ธโฃ ์๋น์ค ํจํด (Dialog, Navigation)
3๏ธโฃ ๊ณ ๊ธ ๋ฐ์ดํฐ ๋ฐ์ธ๋ฉ (MultiBinding, Converter)
4๏ธโฃ ๋น๋๊ธฐ ํ๋ก๊ทธ๋๋ฐ (async/await)
5๏ธโฃ ๋จ์ ํ
์คํธ ์์ฑ
6๏ธโฃ MVVM ํ๋ ์์ํฌ ์ฌ์ฉ (CommunityToolkit.Mvvm)
7๏ธโฃ ์ค๊ฐ ๊ท๋ชจ ํ๋ก์ ํธ ์์ฑ
๐ณ ๊ณ ๊ธ (3-6๊ฐ์)
1๏ธโฃ ๋๊ท๋ชจ ์ ํ๋ฆฌ์ผ์ด์
์ํคํ
์ฒ ์ค๊ณ
2๏ธโฃ ๋ชจ๋ํ์ ํ๋ฌ๊ทธ์ธ ์์คํ
3๏ธโฃ ์ฑ๋ฅ ์ต์ ํ ๊ธฐ๋ฒ
4๏ธโฃ ๊ณ ๊ธ XAML ํ
ํฌ๋
5๏ธโฃ ๋ฉ์์ง ํจํด๊ณผ ์ด๋ฒคํธ ์ ๊ทธ๋ฆฌ๊ฒ์ดํฐ
6๏ธโฃ ์ค๋ฌด ํ๋ก์ ํธ ๊ฒฝํ
7๏ธโฃ ์คํ์์ค ํ๋ก์ ํธ ๊ธฐ์ฌ
์ถ์ฒ ํ์ต ๋ฆฌ์์ค
๐ ๊ณต์ ๋ฌธ์
- Microsoft Learn WPF ํํ ๋ฆฌ์ผ
- .NET API ๋ธ๋ผ์ฐ์
- CommunityToolkit.Mvvm ๋ฌธ์
๐ฅ ์จ๋ผ์ธ ๊ฐ์
- Pluralsight: WPF MVVM ์๋ฆฌ์ฆ
- Udemy: Complete WPF & MVVM
- YouTube: IAmTimCorey, SingletonSean
๐ ์ถ์ฒ ๋์
- "Pro WPF in C#" by Matthew MacDonald
- "WPF 4.5 Unleashed" by Adam Nathan
- "MVVM Survival Guide" by Muhammad Shujaat Siddiqi
๐ป ์ค์ต ํ๋ก์ ํธ ์์ด๋์ด
- ํ ์ผ ๊ด๋ฆฌ ์ฑ (TodoList)
- ๋ฉ๋ชจ์ฅ ์ ํ๋ฆฌ์ผ์ด์
- ์ฐ๋ฝ์ฒ ๊ด๋ฆฌ ์์คํ
- ๊ฐ๋จํ POS ์์คํ
- ํ์ผ ํ์๊ธฐ
- ์์
ํ๋ ์ด์ด
- ์ฑํ
์ ํ๋ฆฌ์ผ์ด์
์ปค๋ฎค๋ํฐ์ ๋์ ๋ฐ๊ธฐ
๐ ์จ๋ผ์ธ ์ปค๋ฎค๋ํฐ
- Stack Overflow (wpf, mvvm ํ๊ทธ)
- Reddit: r/csharp, r/dotnet
- GitHub Discussions
- Discord: C# ์๋ฒ๋ค
๐ค ํ๊ตญ ์ปค๋ฎค๋ํฐ
- OKKY
- ์ํ์ฝ๋ฉ
- ์ธํ๋ฐ ์ง๋ฌธ ๊ฒ์ํ
- ์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์ ๋ฉํ ์ฐพ๊ธฐ
ํน์ WPF MVVM ๊ฐ๋ฐ์ ๋ฐฐ์ฐ๊ณ ์ถ์๋ฐ ํผ์ ํ๊ธฐ ์ด๋ ต๋ค๋ฉด, ์ฌ๋ฅ๋ท์์ ๊ฒฝํ ๋ง์ ๊ฐ๋ฐ์๋ค์๊ฒ ๋ฉํ ๋ง์ ๋ฐ์๋ณด๋ ๊ฒ๋ ์ข์ ๋ฐฉ๋ฒ์ด์ผ! ์ค๋ฌด ๊ฒฝํ์ ๊ฐ์ง ์ ๋ฐฐ ๊ฐ๋ฐ์๋ค์ด 1:1๋ก ์ฝ๋ ๋ฆฌ๋ทฐ๋ ํด์ฃผ๊ณ , ํ๋ก์ ํธ ์ค๊ณ๋ ๋์์ค ์ ์๊ฑฐ๋ . ๐ฏ
๐ ์ค๋ฌด์์์ MVVM ๋ฒ ์คํธ ํ๋ํฐ์ค
์ค์ ์ ๋ฌด์์ MVVM์ ์ ์ฉํ ๋ ์์๋๋ฉด ์ข์ ์ค๋ฌด ํ๋ค์ ๊ณต์ ํ ๊ฒ! ์ด๊ฑด ๊ฒฝํ์์ ๋์จ ์ง์ง ๋ ธํ์ฐ์ผ. ๐
1. ๋ช
๋ช
๊ท์น ์ผ๊ด์ฑ ์ ์ง
ํ ์ ์ฒด๊ฐ ๊ฐ์ ๋ช
๋ช
๊ท์น์ ๋ฐ๋ผ์ผ ํด:
// ViewModel ๋ช
๋ช
MainViewModel, UserListViewModel, SettingsViewModel
// View ๋ช
๋ช
MainWindow, UserListView, SettingsView
// Command ๋ช
๋ช
SaveCommand, DeleteCommand, LoadDataCommand (๋์ฌ + Command)
// Property ๋ช
๋ช
UserName, IsEnabled, SelectedItem (๋ช
์ฌ ๋๋ ํ์ฉ์ฌ)
// Service ๋ช
๋ช
IDataService, IDialogService, INavigationService (I + ๋ช
์ฌ + Service)
2. ViewModel์ ๊ฐ๋ณ๊ฒ ์ ์ง
ViewModel์ ๋๋ฌด ๋ง์ ๋ก์ง์ ๋ฃ์ง ๋ง! ๋น์ฆ๋์ค ๋ก์ง์ Service๋ Model๋ก ๋ถ๋ฆฌํด:
// โ ๋์ ์: ViewModel์ ๋น์ฆ๋์ค ๋ก์ง
public class UserViewModel : BaseViewModel
{
public void SaveUser()
{
// ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๊ฒฐ
using var connection = new SqlConnection(connectionString);
connection.Open();
// SQL ์ฟผ๋ฆฌ ์คํ
var command = new SqlCommand("INSERT INTO Users...", connection);
command.ExecuteNonQuery();
// ํ์ผ ์ ์ฅ
File.WriteAllText("user.json", JsonSerializer.Serialize(user));
}
}
// โ
์ข์ ์: Service๋ก ๋ถ๋ฆฌ
public class UserViewModel : BaseViewModel
{
private readonly IUserService _userService;
public async Task SaveUser()
{
await _userService.SaveUserAsync(User);
}
}
3. ์๋ฌ ์ฒ๋ฆฌ ์ ๋ต
์ฌ์ฉ์์๊ฒ ์น์ ํ ์๋ฌ ๋ฉ์์ง๋ฅผ ๋ณด์ฌ์ค์ผ ํด:
public class MainViewModel : BaseViewModel
{
private readonly IDialogService _dialogService;
private readonly ILogger _logger;
private string _errorMessage;
public string ErrorMessage
{
get => _errorMessage;
set => SetProperty(ref _errorMessage, value);
}
private bool _hasError;
public bool HasError
{
get => _hasError;
set => SetProperty(ref _hasError, value);
}
public ICommand SaveCommand { get; }
private async Task SaveAsync()
{
try
{
HasError = false;
ErrorMessage = string.Empty;
await _dataService.SaveAsync(Data);
_dialogService.ShowMessage("์ ์ฅ๋์์ต๋๋ค!");
}
catch (ValidationException ex)
{
// ์ ํจ์ฑ ๊ฒ์ฌ ์ค๋ฅ - ์ฌ์ฉ์์๊ฒ ํ์
HasError = true;
ErrorMessage = ex.Message;
_logger.LogWarning(ex, "Validation failed");
}
catch (Exception ex)
{
// ์์์น ๋ชปํ ์ค๋ฅ - ๋ก๊ทธ ๊ธฐ๋ก ํ ์ผ๋ฐ ๋ฉ์์ง
HasError = true;
ErrorMessage = "์ ์ฅ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค. ๋ค์ ์๋ํด์ฃผ์ธ์.";
_logger.LogError(ex, "Save failed");
_dialogService.ShowMessage(ErrorMessage, "์ค๋ฅ");
}
}
}
4. ๋ฉ๋ชจ๋ฆฌ ๊ด๋ฆฌ
ํฐ ๋ฐ์ดํฐ๋ฅผ ๋ค๋ฃฐ ๋๋ ๋ฉ๋ชจ๋ฆฌ ๊ด๋ฆฌ์ ์ ๊ฒฝ ์จ์ผ ํด:
public class LargeDataViewModel : BaseViewModel, IDisposable
{
private ObservableCollection<DataItem> _items;
private CancellationTokenSource _cts;
public async Task LoadDataAsync()
{
_cts = new CancellationTokenSource();
try
{
// ํ์ด์ง ์ฒ๋ฆฌ
const int pageSize = 100;
var page = 0;
while (!_cts.Token.IsCancellationRequested)
{
var items = await _dataService.GetPageAsync(page, pageSize, _cts.Token);
if (items.Count == 0) break;
foreach (var item in items)
{
Items.Add(item);
}
page++;
}
}
catch (OperationCanceledException)
{
// ์ทจ์๋จ
}
}
public void Dispose()
{
_cts?.Cancel();
_cts?.Dispose();
// ํฐ ์ปฌ๋ ์
์ ๋ฆฌ
Items?.Clear();
}
}
5. ๋์์ธ ํ์ ๋ฐ์ดํฐ
๋์์ด๋์์ ๋ฏธ๋ฆฌ๋ณด๊ธฐ๋ฅผ ์ํ ์ํ ๋ฐ์ดํฐ๋ฅผ ์ ๊ณตํด:
public class MainViewModel : BaseViewModel
{
public ObservableCollection<User> Users { get; set; }
public MainViewModel() : this(null)
{
}
public MainViewModel(IUserService userService)
{
if (userService == null)
{
// ๋์์ธ ํ์ ๋ฐ์ดํฐ
Users = new ObservableCollection<User>
{
new User { Name = "ํ๊ธธ๋", Age = 25 },
new User { Name = "๊น์ฒ ์", Age = 30 },
new User { Name = "์ด์ํฌ", Age = 28 }
};
}
else
{
// ๋ฐํ์ ๋ฐ์ดํฐ
Users = new ObservableCollection<User>();
LoadUsers(userService);
}
}
}
// XAML์์ ๋์์ธ ํ์ ViewModel ์ค์
<Window xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:MyApp.ViewModels"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance Type=vm:MainViewModel, IsDesignTimeCreatable=True}">
<!-- UI ์์๋ค -->
</Window>
6. ์ฝ๋ ๋ฆฌ๋ทฐ ์ฒดํฌ๋ฆฌ์คํธ
MVVM ์ฝ๋๋ฅผ ๋ฆฌ๋ทฐํ ๋ ํ์ธํ ์ฌํญ๋ค:
โ
View๊ฐ ViewModel๋ง ์ฐธ์กฐํ๋๊ฐ?
โ
ViewModel์ด View๋ฅผ ์ฐธ์กฐํ์ง ์๋๊ฐ?
โ
๋น์ฆ๋์ค ๋ก์ง์ด Service๋ก ๋ถ๋ฆฌ๋์ด ์๋๊ฐ?
โ
๋ชจ๋ ์์ฑ์ด PropertyChanged๋ฅผ ๋ฐ์์ํค๋๊ฐ?
โ
์ปค๋งจ๋์ CanExecute๊ฐ ์ ์ ํ ๊ตฌํ๋์ด ์๋๊ฐ?
โ
๋น๋๊ธฐ ์์
์ด ์ฌ๋ฐ๋ฅด๊ฒ ์ฒ๋ฆฌ๋๋๊ฐ?
โ
์๋ฌ ์ฒ๋ฆฌ๊ฐ ์ ์ ํ๊ฐ?
โ
๋ฉ๋ชจ๋ฆฌ ๋์ ๊ฐ๋ฅ์ฑ์ ์๋๊ฐ?
โ
๋จ์ ํ
์คํธ๊ฐ ์์ฑ๋์ด ์๋๊ฐ?
โ
๋ช
๋ช
๊ท์น์ด ์ผ๊ด์ ์ธ๊ฐ?
๐ก ์ค๋ฌด ์กฐ์ธ
์๋ฒฝํ MVVM์ ์ถ๊ตฌํ๋ค๊ฐ ์คํ๋ ค ์์ฐ์ฑ์ด ๋จ์ด์ง ์ ์์ด. ๋๋ก๋ ์ค์ฉ์ ์ธ ํํ์ด ํ์ํด!
์๋ฅผ ๋ค์ด:
- ์์ฃผ ๊ฐ๋จํ UI ๋ก์ง์ ์ฝ๋-๋นํ์ธ๋์ ๋๋ ๊ฒ ๋์ ์ ์์ด
- ๋ชจ๋ ๊ฑธ ์ถ์ํํ ํ์๋ ์์ด (YAGNI ์์น)
- ํ์ ์๋ จ๋์ ๋ง์ถฐ ์ ์ง์ ์ผ๋ก ๋์
ํด
์ค์ํ ๊ฑด ์ ์ง๋ณด์ ๊ฐ๋ฅํ๊ณ ํ
์คํธ ๊ฐ๋ฅํ ์ฝ๋๋ฅผ ์์ฑํ๋ ๊ฑฐ์ผ! ๐ฏ
๐ฌ ๋ง๋ฌด๋ฆฌํ๋ฉฐ
์! ์ฌ๊ธฐ๊น์ง ์ ๋ง ๊ธด ์ฌ์ ์ด์์ด. ๐ WPF MVVM ํจํด์ ๋ํด ์ ๋ง ๋ง์ ๊ฑธ ๋ค๋ค์ง?
์ฒ์์๋ ๋ณต์กํด ๋ณด์ผ ์ ์์ง๋ง, MVVM์ ํ ๋ฒ ์ต์ํด์ง๋ฉด ์ ๋ง ๊ฐ๋ ฅํ ๋๊ตฌ์ผ. ์ฝ๋๊ฐ ๊น๋ํด์ง๊ณ , ํ
์คํธํ๊ธฐ ์ฌ์์ง๊ณ , ํ ํ์
๋ ํจ์ฌ ์์ํด์ ธ. ๐
ํต์ฌ ์์ฝ ์ ๋ฆฌ
1๏ธโฃ MVVM์ ์ธ ๊ฐ์ง ๋ ์ด์ด
- Model: ๋ฐ์ดํฐ์ ๋น์ฆ๋์ค ๋ก์ง
- View: ์ฌ์ฉ์ ์ธํฐํ์ด์ค (XAML)
- ViewModel: ์ค๊ฐ ์ฐ๊ฒฐ ๊ณ์ธต
2๏ธโฃ ํต์ฌ ๋ฉ์ปค๋์ฆ
- ๋ฐ์ดํฐ ๋ฐ์ธ๋ฉ์ผ๋ก View์ ViewModel ์ฐ๊ฒฐ
- INotifyPropertyChanged๋ก ๋ณ๊ฒฝ ์๋ฆผ
- Command ํจํด์ผ๋ก ์ฌ์ฉ์ ์ก์
์ฒ๋ฆฌ
3๏ธโฃ ์ค๊ณ ์์น
- View๋ ViewModel๋ง ์์์ผ ํจ
- ViewModel์ View๋ฅผ ๋ชฐ๋ผ์ผ ํจ
- ๋จ๋ฐฉํฅ ์์กด์ฑ ์ ์ง
- ์์กด์ฑ ์ฃผ์
ํ์ฉ
4๏ธโฃ ์ค๋ฌด ํ
- ๋ช
๋ช
๊ท์น ์ผ๊ด์ฑ
- ViewModel์ ๊ฐ๋ณ๊ฒ
- ์ ์ ํ ์๋ฌ ์ฒ๋ฆฌ
- ์ฑ๋ฅ ์ต์ ํ
- ํ
์คํธ ์์ฑ
MVVM์ ๋จ์ํ ์ฝ๋ฉ ํจํด์ด ์๋๋ผ, ์ข์ ์ํํธ์จ์ด๋ฅผ ๋ง๋ค๊ธฐ ์ํ ์ฌ๊ณ ๋ฐฉ์์ด์ผ. ๊ด์ฌ์ฌ์ ๋ถ๋ฆฌ, ํ
์คํธ ๊ฐ๋ฅ์ฑ, ์ ์ง๋ณด์์ฑ ๊ฐ์ ์ํํธ์จ์ด ๊ณตํ์ ํต์ฌ ์์น๋ค์ ์ค์ฒํ๋ ๋ฐฉ๋ฒ์ด์ง. ๐ก
์ฒ์์๋ ์ด๋ ต๊ฒ ๋๊ปด์ง ์ ์์ด. "์ด๋ ๊ฒ๊น์ง ํด์ผ ํ๋?" ์ถ์ ์๋ ์๊ณ . ํ์ง๋ง ํ๋ก์ ํธ๊ฐ ์ปค์ง๊ณ ํ์์ด ๋์ด๋๋ฉด, MVVM์ ์ง๊ฐ๋ฅผ ์๊ฒ ๋ ๊ฑฐ์ผ. 6๊ฐ์ ํ์ ์์ ์ด ์์ฑํ ์ฝ๋๋ฅผ ๋ค์ ๋ดค์ ๋, "์, ์ด ์ฝ๋ ์ ๋ง ์ ์งฐ๋ค!" ํ๊ณ ๊ฐํํ๊ฒ ๋ ๊ฑฐ์ผ. ๐
๐ฏ ๋ค์ ๋จ๊ณ
์ด์ ์ง์ ํ๋ก์ ํธ๋ฅผ ๋ง๋ค์ด๋ณด์!
1๏ธโฃ ์์ ํ๋ก์ ํธ๋ถํฐ ์์ (ํ ์ผ ๊ด๋ฆฌ, ๋ฉ๋ชจ์ฅ ๋ฑ)
2๏ธโฃ ์ ์ง์ ์ผ๋ก ๊ธฐ๋ฅ ์ถ๊ฐ
3๏ธโฃ ์ฝ๋ ๋ฆฌํฉํ ๋ง ์ฐ์ต
4๏ธโฃ ๋จ์ ํ
์คํธ ์์ฑ
5๏ธโฃ GitHub์ ์ฌ๋ฆฌ๊ณ ํผ๋๋ฐฑ ๋ฐ๊ธฐ
ํผ์ ํ๊ธฐ ์ด๋ ต๋ค๋ฉด ์ฌ๋ฅ๋ท์์ ๋ฉํ ๋ฅผ ์ฐพ์๋ณด๋ ๊ฒ๋ ์ข์ ๋ฐฉ๋ฒ์ด์ผ! ๊ฒฝํ ๋ง์ ๊ฐ๋ฐ์๋ค์ด ์ฝ๋ ๋ฆฌ๋ทฐ๋ ํด์ฃผ๊ณ , ์ค๋ฌด ๋
ธํ์ฐ๋ ๊ณต์ ํด์ค ๊ฑฐ์ผ. ๐ค
๊ธฐ์ตํด! ์๋ฒฝํ ์ฝ๋๋ฅผ ์ฒ์๋ถํฐ ์์ฑํ๋ ์ฌ๋์ ์์ด. ๊ณ์ ๋ฐฐ์ฐ๊ณ , ์ค์ํ๊ณ , ๊ฐ์ ํ๋ฉด์ ์ฑ์ฅํ๋ ๊ฑฐ์ผ. MVVM๋ ๋ง์ฐฌ๊ฐ์ง์ผ. ์ฒ์์ ์ํด๋ฌ๋ ๊ด์ฐฎ์. ๊ณ์ ์ฐ์ตํ๋ค ๋ณด๋ฉด ์ด๋์ ์์ฐ์ค๋ฝ๊ฒ MVVM์ผ๋ก ์๊ฐํ๊ณ ์ฝ๋ฉํ๊ฒ ๋ ๊ฑฐ์ผ! ๐ช
WPF MVVM ๊ฐ๋ฐ์๋ก์์ ์ฌ์ ์ ์์ํ ๊ฒ! ํ์ดํ
! ๐โจ
๊ถ๊ธํ ์ ์ด ์๊ฑฐ๋ ๋งํ๋ ๋ถ๋ถ์ด ์์ผ๋ฉด ์ธ์ ๋ ์ปค๋ฎค๋ํฐ์ ์ง๋ฌธํ๊ณ , ๋ค๋ฅธ ๊ฐ๋ฐ์๋ค์ ์ฝ๋๋ ๋ง์ด ์ฝ์ด๋ด. ๊ทธ๊ฒ ๊ฐ์ฅ ๋น ๋ฅธ ์ฑ์ฅ ๋ฐฉ๋ฒ์ด์ผ! ๐
์, ์ด์ ์๋ํฐ๋ฅผ ์ด๊ณ ๋ฉ์ง WPF ์ ํ๋ฆฌ์ผ์ด์
์ ๋ง๋ค์ด๋ณด์! ๐
๐ Happy Coding with WPF MVVM! ๐
๋น์ ์ ๊ฐ๋ฐ ์ฌ์ ์ ์์ํฉ๋๋ค!
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.