| name | add-viewmodel |
| description | This skill should be used when the user requests adding a new screen, dialog, page, window, or any UI component that needs a ViewModel in a WinUI 3 / WPF / MAUI project using CommunityToolkit.Mvvm. Triggers on phrases like "ViewModel ์ถ๊ฐ", "์ ํ๋ฉด", "๋ค์ด์ผ๋ก๊ทธ ์ถ๊ฐ", "ํ์ด์ง ๋ง๋ค๊ธฐ", "add screen/page/dialog/window". Generates ViewModel + View skeleton with proper MVVM bindings and DI registration. Do NOT trigger for non-XAML stacks (React/web, ASP.NET WebAPI controllers), simple UI text/label/style tweaks on an existing view, or debugging an existing ViewModel (use pjc-systematic-debugging). Android Jetpack ViewModel is out of scope. |
| argument-hint | <ํ๋ฉด ์ด๋ฆ ๋๋ ๋ชฉ์ > |
Add ViewModel
WinUI 3 / WPF / MAUI ํ๋ก์ ํธ์ MVVM ํจํด(CommunityToolkit.Mvvm)์ผ๋ก
View + ViewModel ์ค์ผ๋ ํค์ ์ถ๊ฐํ๋ค.
ํธ์ถ ํ๋ฆ
์ด skill์ pjc:implement-task์ Phase I ์์์ ํธ์ถ๋๊ฑฐ๋, ์ฌ์ฉ์๊ฐ ์ง์ /pjc:add-viewmodel๋ก ํธ์ถํ ์ ์๋ค.
| ํธ์ถ ๋ฐฉ์ | ํ๋ฆ |
|---|
| implement-task Phase I ์ | plan.md task๊ฐ "ViewModel ์ถ๊ฐ" ํจํด์ด๋ฉด ์๋ ํธ์ถ. ์ด skill์ด boilerplate ์์ฑ ํ implement-task์ Phase V๊ฐ ๊ฒ์ฆ์ ์ด์ด๋ฐ์. |
| ์ฌ์ฉ์ ์ง์ ํธ์ถ | plan.md ์์ด ๋จ๋
์ฌ์ฉ. ๋จ, require-plan-for-write hook์ด ์ฐจ๋จํ ์ ์์ผ๋ฏ๋ก $env:CLAUDE_HARNESS_QUICK = '1' ํ์. |
์ด skill์ ์ฑ
์ ๋ฒ์: ViewModel/View boilerplate, DI ๋ฑ๋ก, ๊ธฐ๋ณธ ํ
์คํธ ์ค์ผ๋ ํค ์์ฑ๊น์ง.
์ฑ
์ ๋ฒ์ ๋ฐ: ๋น์ฆ๋์ค ๋ก์ง, ๋ฐ์ดํฐ ๋ฐ์ธ๋ฉ ์์ธ, ํตํฉ ๊ฒ์ฆ โ implement-task๊ฐ ๋ด๋น.
Android์ Jetpack ViewModel์ ๋น๋์. Android์ ๊ฒฝ์ฐ implement-task๊ฐ ์ง์ ๊ตฌํ.
์ฌ์ ์กฐ๊ฑด
์ด skill์ ํธ์ถํ๊ธฐ ์ ์ plan-feature๋ก ๋ค์์ด ๊ฒฐ์ ๋์ด ์์ด์ผ ํ๋ค:
- ํ๋ฉด ์ด๋ฆ (์:
Settings, UserDetail)
- ํ๋ฉด ์ข
๋ฅ (Page / Window / UserControl / ContentDialog)
- ์์น (์ด๋ ๋ชจ๋/ํ๋ก์ ํธ)
- ์์ ๋ค๋น๊ฒ์ด์
๊ณผ์ ์ฐ๊ฒฐ ๋ฐฉ์
- ํ์ํ ์์กด์ฑ ์๋น์ค (์๋ค๋ฉด)
์ ์ ๋ณด๊ฐ ์์ผ๋ฉด ์ฌ์ฉ์์๊ฒ ๋ฌป๊ฑฐ๋ plan-feature๋ก ๋ณต๊ท.
์ ๋ ๊ท์น
- AGENTS.md ์ฐ์ . ํ๋ก์ ํธ๊ฐ ๋ค๋ฅธ ํจํด(ReactiveUI, MVVM Light ๋ฑ)์ ๋ช
์ํ๋ค๋ฉด ์ด skill์ ์ฌ์ฉํ์ง ์๋๋ค.
- DDD ์ค์. ViewModel์ UI ๋ ์ด์ด. ๋น์ฆ๋์ค ๋ก์ง์ Domain ์๋น์ค๋ฅผ ํธ์ถํ๊ธฐ๋ง ํ๋ค.
- DI ๋ฑ๋ก ๋๋ฝ ๊ธ์ง. ViewModel์ ๋ฐ๋์
ConfigureServices์ ๋ฑ๋ก.
- ํ๊ธ ์ฃผ์. XML ๋ฌธ์ ์ฃผ์ ํฌํจ ๋ชจ๋ ํ๊ธ.
- UTF-8 (BOM ์์).
- WinUI 3 ํ๋ก์ ํธ๋ฉด ๋์์ธยท๋ค๊ตญ์ด ๊ท์น ์ค์. AGENTS.md(winui3 ํ
ํ๋ฆฟ)์ ๋์์ธ ๊ท์น(ํ ํฐํ, ํฐํธ ๋ฏธ์ง์ , ์์คํ
ํค ์ฐ์ )๊ณผ ๋ค๊ตญ์ด ๊ท์น(๋ฌธ๊ตฌ๋
x:Uid+.resw, ํ๋์ฝ๋ฉ ๊ธ์ง)์ ๋ฐ๋ฅธ๋ค. ์์ธ๋ docs/WINUI3-DESIGN-GUIDE.md๊ฐ ์์ผ๋ฉด ์ฐธ์กฐ. View์ ๋ฌธ๊ตฌ๋ฅผ ์ฝ๋/XAML์ ์ง์ ์ฐ์ง ์๋๋ค.
์คํ ๋จ๊ณ
Step 1. ์ปจํ
์คํธ ํ์
๋ค์์ ํ์ธ:
- ๊ธฐ์กด ViewModel ์์น (์:
src/*/ViewModels/)
- ๊ธฐ์กด View ์์น (์:
src/*/Views/)
- DI ๋ฑ๋ก ์ง์
์ (๋ณดํต
App.xaml.cs ๋๋ Program.cs์ ConfigureServices)
- ๋ค๋น๊ฒ์ด์
์๋น์ค ํจํด (
INavigationService ๋ฑ ์กด์ฌ ์ฌ๋ถ)
- ๊ธฐ์กด ViewModel ํ ๊ฐ๋ฅผ ์ฝ์ด ์ปจ๋ฒค์
ํ์
(๋ค์ด๋ฐ, ๋ฒ ์ด์ค ํด๋์ค, ์ฃผ์ ์คํ์ผ)
CommunityToolkit.Mvvm PackageReference ์กด์ฌ ํ์ธ(csproj grep) โ ์์ผ๋ฉด Halt(์๋ Halt ์กฐ๊ฑด์ด ์ ๋ณธ: ์์กด์ฑ ์ถ๊ฐ๋ ์น์ธ ํ์์ด๋ฏ๋ก ์์ ์ถ๊ฐยท์ปดํ์ผ ๋ถ๊ฐ ์ฝ๋ ์์ฑ ๊ธ์ง).
Step 2. ViewModel ์์ฑ
๊ธฐ๋ณธ ํ
ํ๋ฆฟ:
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Extensions.Logging;
namespace <ProjectNamespace>.ViewModels;
public sealed partial class <Name>ViewModel : ObservableObject
{
private readonly ILogger<<Name>ViewModel> _logger;
[ObservableProperty]
private string _title = "<๊ธฐ๋ณธ ์ ๋ชฉ>";
[ObservableProperty]
private bool _isBusy;
public <Name>ViewModel(
ILogger<<Name>ViewModel> logger
)
{
_logger = logger;
}
[RelayCommand]
private async Task LoadAsync()
{
if (IsBusy) return;
try
{
IsBusy = true;
_logger.LogInformation("<Name> ํ๋ฉด ๋ก๋ฉ ์๋ฃ");
}
catch (Exception ex)
{
_logger.LogError(ex, "<Name> ํ๋ฉด ๋ก๋ฉ ์คํจ");
}
finally
{
IsBusy = false;
}
}
}
Step 3. View ์์ฑ
WinUI 3 Page ๋๋ Window (WPF๋ ์๋ ์ฃผ์ ์ฐธ์กฐ)
XAML (<Name>Page.xaml):
<Page
x:Class="<ProjectNamespace>.Views.<Name>Page"
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"
mc:Ignorable="d">
<Grid Padding="16" RowDefinitions="Auto,*">
<TextBlock Grid.Row="0"
Text="{x:Bind ViewModel.Title, Mode=OneWay}"
Style="{StaticResource TitleTextBlockStyle}"/>
<ProgressRing Grid.Row="1"
IsActive="{x:Bind ViewModel.IsBusy, Mode=OneWay}"
HorizontalAlignment="Center"/>
</Grid>
</Page>
WPF ์ฐจ์ด: WPF์๋ x:BindยทProgressRingยทTitleTextBlockStyle์ด ์๋ค. WPF View๋ {Binding Title}(DataContext์ VM ์ฃผ์
), ProgressRing ๋์ ProgressBar IsIndeterminate="True", namespace๋ System.Windows.Controls.Page, Style์ ํ๋ก์ ํธ/WPF-UI ๋ฆฌ์์ค๋ฅผ ์ฌ์ฉํ๋ค. ๋ํ WPF๋ Grid์ RowDefinitions="Auto,*" ์ถ์ฝ ๋ฌธ๋ฒ๊ณผ Grid Padding์ ์ง์ํ์ง ์๋๋ค โ <Grid.RowDefinitions>๋ฅผ ์ ๊ฐํด <RowDefinition Height="Auto"/><RowDefinition Height="*"/>๋ก ์ฐ๊ณ , Padding ๋์ ์์ ์์์ Margin์ ์ค๋ค(๋๋ Grid๋ฅผ Border Padding์ผ๋ก ๊ฐ์ผ๋ค).
MAUI ์ฐจ์ด: MAUI๋ Page ๋์ ContentPage(Microsoft.Maui.Controls). x:Bind๊ฐ ์์ด {Binding Title}(BindingContext์ VM ์ฃผ์
, x:DataType์ผ๋ก ์ปดํ์ผ ๋ฐ์ธ๋ฉ ๊ถ์ฅ), ProgressRing ๋์ ActivityIndicator, ์ฝ๋๋นํ์ธ๋ namespace๋ Microsoft.Maui.Controls, DI๋ MauiProgram์ builder.Services(App.GetService ๋์ ์์ฑ์ ์ฃผ์
). ViewModel(Step 2)์ CommunityToolkit.Mvvm ๊ทธ๋๋ก ์ฌ์ฉํ๋ค.
์ฝ๋๋นํ์ธ๋ (<Name>Page.xaml.cs):
using Microsoft.UI.Xaml.Controls;
namespace <ProjectNamespace>.Views;
public sealed partial class <Name>Page : Page
{
public <Name>ViewModel ViewModel { get; }
public <Name>Page()
{
ViewModel = App.GetService<<Name>ViewModel>();
InitializeComponent();
Loaded += async (_, _) => await ViewModel.LoadCommand.ExecuteAsync(null);
}
}
์ฃผ์: App.GetService<T>()๋ App.xaml.cs์ ์ ์๋ ์ ์ ํฌํผ๋ผ๊ณ ๊ฐ์ . ํ๋ก์ ํธ๊ฐ ๋ค๋ฅธ ๋ฐฉ์(์์ฑ์ ์ฃผ์
, IPageFactory ๋ฑ)์ ์ฐ๋ฉด ๊ทธ์ชฝ์ ๋ฐ๋ฅธ๋ค.
Step 4. DI ๋ฑ๋ก
App.xaml.cs (๋๋ Program.cs) ์ ConfigureServices์ ์ถ๊ฐ:
private static IServiceProvider ConfigureServices()
{
var services = new ServiceCollection();
services.AddTransient<<Name>ViewModel>();
services.AddTransient<<Name>Page>();
return services.BuildServiceProvider();
}
์๋ช
: ์ผ๋ฐ์ ์ผ๋ก ViewModel์ Transient. ์ฑ ์ ์ฒด์์ ์ํ๋ฅผ ์ ์งํด์ผ ํ๋ฉด Singleton ๊ฒํ .
Step 5. ๋ค๋น๊ฒ์ด์
์ฐ๊ฒฐ (ํด๋นํ๋ ๊ฒฝ์ฐ)
๊ธฐ์กด ๋ค๋น๊ฒ์ด์
ํจํด์ ๋ฐ๋ผ:
Frame.Navigate(typeof(<Name>Page))
INavigationService.NavigateTo("<Name>")
- ๋ฉ๋ด/์ฌ์ด๋๋ฐ์ ํญ๋ชฉ ์ถ๊ฐ
์ด ๋จ๊ณ๋ plan.md์ ๋ช
์๋ ์ง์
์ ์ ๋ฐ๋ผ ์งํ. ์ถ์ธก ๊ธ์ง.
Step 6. ํ
์คํธ ์ค์บํด๋
tests/<Project>.Tests/ViewModels/<Name>ViewModelTests.cs:
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;
namespace <ProjectNamespace>.Tests.ViewModels;
public class <Name>ViewModelTests
{
private static <Name>ViewModel CreateSut()
{
return new <Name>ViewModel(
NullLogger<<Name>ViewModel>.Instance
);
}
[Fact]
public async Task LoadCommand_์ด๊ธฐ์คํ_IsBusy_๊ฐ_๋ณต์๋๋ค()
{
var sut = CreateSut();
await sut.LoadCommand.ExecuteAsync(null);
Assert.False(sut.IsBusy);
}
}
Step 7. ๊ฒ์ฆ
๋ค์์ ๋ชจ๋ ํต๊ณผํด์ผ ์๋ฃ:
๋ณํ (Variants)
A. ContentDialog (๋ชจ๋ฌ)
Page ๋์ ContentDialog ์ฌ์ฉ:
public sealed partial class <Name>Dialog : ContentDialog
{
public <Name>ViewModel ViewModel { get; }
public <Name>Dialog(<Name>ViewModel viewModel)
{
ViewModel = viewModel;
InitializeComponent();
}
}
์์ฑ์ ์ฃผ์
๊ฐ๋ฅ (DI์์ ์ง์ ํด์).
B. UserControl (์ฌ์ฌ์ฉ ๋ถํ)
ViewModel์ ์ธ๋ถ(๋ถ๋ชจ View)์์ DataContext๋ก ์ฃผ์
๋ฐ๋ ํํ. UserControl ๋ด๋ถ์์๋ ์์๋ DataContext์ {Binding}์ผ๋ก ๋ฐ์ธ๋ฉํ๊ณ , ์์ฒด์ ์ผ๋ก DataContext๋ฅผ ๋ฎ์ด์ฐ์ง ์๋๋ค.
<UserControl ...>
<Grid>
...
</Grid>
</UserControl>
C. Settings / ์์ํ๊ฐ ํ์ํ ๊ฒฝ์ฐ
Singleton ViewModel + ISettingsService ์์กด์ฑ ์ฃผ์
.
์ํฐํจํด (๊ธ์ง)
| ์ํฐํจํด | ์ฌ๋ฐ๋ฅธ ํ๋ |
|---|
INotifyPropertyChanged ์๋ ๊ตฌํ | [ObservableProperty] ์ฌ์ฉ |
ICommand๋ฅผ ์๋ ๊ตฌํ | [RelayCommand] ์ฌ์ฉ |
ViewModel์์ MessageBox ์ง์ ํธ์ถ | IDialogService ๋ฑ์ผ๋ก ์ถ์ํ |
ViewModel์์ HttpClient ์ง์ ์ฌ์ฉ | Domain/Application ์๋น์ค ๊ฒฝ์ |
| ์ฝ๋๋นํ์ธ๋์ ๋น์ฆ๋์ค ๋ก์ง ์์ฑ | ViewModel๋ก ์ด๋ |
๋๊ธฐ Wait(), .Result ํธ์ถ | async/await |
LoadAsync๋ฅผ ์์ฑ์์์ ์ง์ ํธ์ถ | Loaded ์ด๋ฒคํธ ๋๋ ๋ช
์์ ์ปค๋งจ๋ |
DI ๋ฑ๋ก ์์ด new ViewModel()ยท์๋ฌธ XML doc ์ฃผ์ ๊ธ์ง๋ ์ ๋ ๊ท์น 3ยท4๊ฐ ์ ๋ณธ(์ค๋ณต ํ ์ ๊ฑฐ).
Halt ์กฐ๊ฑด
๋ค์ ๋ฐ๊ฒฌ ์ ์ฌ์ฉ์์๊ฒ ๋ณด๊ณ ํ๊ณ ์ค์ง:
- ๊ธฐ์กด ViewModel์ด ๋ค๋ฅธ ๋ฒ ์ด์ค ํด๋์ค(
BindableBase, ReactiveObject ๋ฑ)๋ฅผ ์ฐ๊ณ ์์
- DI ์ปจํ
์ด๋๊ฐ ์๊ฑฐ๋, ServiceLocator๋ฅผ ๋จ์ฉํ๊ณ ์์(์ 3์ Service Locator ๋ผ์ด๋ธ๋ฌ๋ฆฌยท์ ์ญ ์ ์ ์ปจํ
์ด๋๋ฅผ ์ฌ๊ธฐ์ ๊ธฐ์ ์ง์ ๋ค์ง๋ ์ํฐํจํด). ๋จ
App.GetService<T>() ๊ฐ์ ํ๋ก์ ํธ ๊ด๋ก์ ์ ์ ํฌํผ๋ ๋จ์ฉ์ด ์๋๋ค โ WinUI 3 ํ
ํ๋ฆฟ์ ํ์ค ํจํด์ด๋ฏ๋ก ๊ทธ๋๋ก ์ฌ์ฉํ๋ค(์์ฑ์ ์ฃผ์
์ด ๊ธฐ๋ณธ์ด๋, View ์ฝ๋๋นํ์ธ๋์์ VM์ ์ป๋ App.GetService ๊ด๋ก๋ ํ์ฉ). Halt๋ "๊ด๋ก ์๋ ์ ์ญ ๋ก์ผ์ดํฐ ๋จ์ฉ"์ ํํ๋ค
- ๋ค๋น๊ฒ์ด์
ํจํด์ด plan.md์ ๋ช
์๋์ง ์์๊ณ ์ฝ๋๋ฒ ์ด์ค์์๋ ๋จ์ผ ํจํด์ด ๋ณด์ด์ง ์์
- View๊ฐ ์ฝ๋ ์์ฑ๊ธฐ๋ก ๋ง๋ค์ด์ง๋ ๊ฒฝ์ฐ (
*.Generated.*)
CommunityToolkit.Mvvm ํจํค์ง๊ฐ ํ๋ก์ ํธ์ ์์ โ [ObservableProperty]ยท[RelayCommand]ยทObservableObject๊ฐ ์ปดํ์ผ๋์ง ์๋๋ค. ์์กด์ฑ ์ถ๊ฐ๋ ์น์ธ ํ์์ด๋ฏ๋ก ์์๋ก ์ถ๊ฐํ์ง ๋ง๊ณ ์ฌ์ฉ์์๊ฒ ํ์ธ(๋๋ plan์ ํจํค์ง ์ถ๊ฐ๋ฅผ ๋ช
์)