From wpf-dev-pack
Integrates FluentValidation with WPF MVVM via INotifyDataErrorInfo bridge for form validation. Use for complex RuleFor, AbstractValidator rules or CommunityToolkit.Mvvm integration.
npx claudepluginhub christian289/dotnet-with-claudecode --plugin wpf-dev-packThis skill uses the workspace's default tool permissions.
> **MVVM Framework Rule**: `.claude/rules/dotnet/wpf/mvvm-framework.md` 설정에 따라 코드 스타일이 결정됩니다.
Generates design tokens/docs from CSS/Tailwind/styled-components codebases, audits visual consistency across 10 dimensions, detects AI slop in UI.
Records polished WebM UI demo videos of web apps using Playwright with cursor overlay, natural pacing, and three-phase scripting. Activates for demo, walkthrough, screen recording, or tutorial requests.
Delivers idiomatic Kotlin patterns for null safety, immutability, sealed classes, coroutines, Flows, extensions, DSL builders, and Gradle DSL. Use when writing, reviewing, refactoring, or designing Kotlin code.
MVVM Framework Rule:
.claude/rules/dotnet/wpf/mvvm-framework.md설정에 따라 코드 스타일이 결정됩니다. Prism 9 사용 시 → PRISM.md 참조
FluentValidation 12.x를 WPF MVVM 패턴에 통합하는 가이드.
<PackageReference Include="FluentValidation" Version="12.1.*" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.1.*" />
⚠️ FluentValidation 12는 .NET 8 이상 필수 (.NET Standard, .NET 5/6/7 지원 제거)
namespace MyApp.Core.Validators;
public sealed class UserValidator : AbstractValidator<User>
{
public UserValidator()
{
RuleFor(x => x.Name)
.NotEmpty().WithMessage("이름을 입력하세요.")
// Name is required.
.MinimumLength(2).WithMessage("이름은 2자 이상이어야 합니다.");
// Name must be at least 2 characters.
RuleFor(x => x.Email)
.NotEmpty().WithMessage("이메일을 입력하세요.")
// Email is required.
.EmailAddress().WithMessage("올바른 이메일 형식이 아닙니다.");
// Invalid email format.
RuleFor(x => x.Age)
.InclusiveBetween(1, 150).WithMessage("유효한 나이를 입력하세요.");
// Please enter a valid age.
}
}
FluentValidation → WPF 바인딩 연결을 위한 Base ViewModel:
namespace MyApp.ViewModels;
public abstract partial class ValidatableViewModel<TModel> : ObservableObject, INotifyDataErrorInfo
{
private readonly IValidator<TModel> _validator;
private readonly Dictionary<string, List<string>> _errors = [];
protected ValidatableViewModel(IValidator<TModel> validator)
{
_validator = validator;
}
public bool HasErrors => _errors.Count > 0;
public event EventHandler<DataErrorsChangedEventArgs>? ErrorsChanged;
public IEnumerable GetErrors(string? propertyName)
{
if (string.IsNullOrEmpty(propertyName))
return _errors.SelectMany(e => e.Value);
return _errors.TryGetValue(propertyName, out var errors)
? errors
: [];
}
protected void ValidateProperty(TModel model, string propertyName)
{
var result = _validator.Validate(model);
ClearErrors(propertyName);
foreach (var error in result.Errors.Where(e => e.PropertyName == propertyName))
{
AddError(propertyName, error.ErrorMessage);
}
}
protected bool ValidateAll(TModel model)
{
var result = _validator.Validate(model);
// 전체 에러 초기화 후 재설정
// Clear all errors and rebuild
var previousProperties = _errors.Keys.ToList();
_errors.Clear();
foreach (var prop in previousProperties)
OnErrorsChanged(prop);
foreach (var error in result.Errors)
{
AddError(error.PropertyName, error.ErrorMessage);
}
return result.IsValid;
}
private void AddError(string propertyName, string error)
{
if (!_errors.ContainsKey(propertyName))
_errors[propertyName] = [];
if (!_errors[propertyName].Contains(error))
{
_errors[propertyName].Add(error);
OnErrorsChanged(propertyName);
}
}
private void ClearErrors(string propertyName)
{
if (_errors.Remove(propertyName))
OnErrorsChanged(propertyName);
}
private void OnErrorsChanged(string propertyName)
{
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
OnPropertyChanged(nameof(HasErrors));
}
}
namespace MyApp.ViewModels;
public sealed partial class UserFormViewModel : ValidatableViewModel<User>
{
private readonly User _user = new();
public UserFormViewModel(IValidator<User> validator) : base(validator) { }
[ObservableProperty] private string _name = string.Empty;
[ObservableProperty] private string _email = string.Empty;
[ObservableProperty] private int _age;
partial void OnNameChanged(string value)
{
_user.Name = value;
ValidateProperty(_user, nameof(Name));
}
partial void OnEmailChanged(string value)
{
_user.Email = value;
ValidateProperty(_user, nameof(Email));
}
partial void OnAgeChanged(int value)
{
_user.Age = value;
ValidateProperty(_user, nameof(Age));
}
[RelayCommand(CanExecute = nameof(CanSubmit))]
private void Submit()
{
if (ValidateAll(_user))
{
// 제출 처리
// Handle submission
}
}
private bool CanSubmit() => !HasErrors;
}
services.AddValidatorsFromAssemblyContaining<UserValidator>();
// 기본 수명: Scoped → WPF에서는 Singleton 권장
// Default lifetime: Scoped → Singleton recommended for WPF
services.AddValidatorsFromAssemblyContaining<UserValidator>(ServiceLifetime.Singleton);
services.AddTransient<UserFormViewModel>();
<TextBox Text="{Binding Name,
UpdateSourceTrigger=PropertyChanged,
ValidatesOnNotifyDataErrors=True}" />
<TextBox Text="{Binding Email,
UpdateSourceTrigger=PropertyChanged,
ValidatesOnNotifyDataErrors=True}" />
⚠️
UpdateSourceTrigger=PropertyChanged필수. 없으면 포커스 이탈 시에만 검증.
| 비교 | ObservableValidator | FluentValidation |
|---|---|---|
| 규칙 정의 | DataAnnotations ([Required]) | Fluent API (RuleFor) |
| 복잡한 규칙 | 제한적 | 교차 필드, 조건부, 컬렉션 검증 |
| DI 통합 | 불필요 | AddValidatorsFromAssembly |
| 적합한 경우 | 단순 폼 검증 | 복잡한 비즈니스 규칙 |
혼용 금지: ObservableValidator와 FluentValidation을 동시 사용하면 에러가 중복/누락됩니다. 하나만 선택하세요.
| 실수 | 올바른 방법 |
|---|---|
| Model 대신 ViewModel을 직접 검증 | AbstractValidator<Model> 사용, ViewModel에서 Model 동기화 |
| 매 변경마다 전체 Validate 호출 | ValidateProperty로 해당 속성만 필터링 |
| ErrorsChanged 미발생 | _errors 변경 후 반드시 ErrorsChanged 이벤트 |
| Singleton Validator에 Scoped 의존성 주입 | Captive Dependency 주의, WPF에서는 Singleton 권장 |
| UpdateSourceTrigger 미설정 | PropertyChanged 명시 필수 |
ValidateProperty() 호출INotifyDataErrorInfo 브릿지로 WPF 바인딩 연결AddValidatorsFromAssemblyContaining<T>() 으로 DI 일괄 등록implementing-wpf-validation (WPF 기본 검증), handling-errors-with-erroror (서비스 레이어 에러 처리)