From wpf-dev-pack
Implements WPF data validation using ValidationRule, IDataErrorInfo, INotifyDataErrorInfo for building forms, validating user input, and displaying UI errors with XAML examples.
How this skill is triggered — by the user, by Claude, or both
Slash command
/wpf-dev-pack:implementing-wpf-validationsonnetThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
> **MVVM Framework Rule**: `.claude/rules/dotnet/wpf/mvvm-framework.md` 설정에 따라 코드 스타일이 결정됩니다.
MVVM Framework Rule:
.claude/rules/dotnet/wpf/mvvm-framework.md설정에 따라 코드 스타일이 결정됩니다. Prism 9 사용 시 → PRISM.md 참조
| Approach | Location | Pros | Cons |
|---|---|---|---|
ValidationRule | XAML (Binding) | Simple, declarative XAML | Hard to separate from ViewModel |
IDataErrorInfo | ViewModel | ViewModel integration | Synchronous validation only |
INotifyDataErrorInfo | ViewModel | Async support, multiple errors | Complex implementation |
ExceptionValidationRule | XAML | Exception-based | Potential performance impact |
public sealed partial class EmailValidationRule : ValidationRule
{
[GeneratedRegex(@"^[^@\s]+@[^@\s]+\.[^@\s]+$", RegexOptions.IgnoreCase)]
private static partial Regex EmailPattern();
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
if (value is not string email || string.IsNullOrWhiteSpace(email))
{
return new ValidationResult(false, "Please enter an email address.");
}
if (!EmailPattern().IsMatch(email))
{
return new ValidationResult(false, "Invalid email format.");
}
return ValidationResult.ValidResult;
}
}
Note: Uses
GeneratedRegexAttributefor compile-time regex. Seeusing-generated-regexskill.
<TextBox>
<TextBox.Text>
<Binding Path="Email" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<local:EmailValidationRule ValidatesOnTargetUpdated="True"/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<Style TargetType="TextBox">
<Setter Property="Validation.ErrorTemplate">
<Setter.Value>
<ControlTemplate>
<DockPanel>
<TextBlock DockPanel.Dock="Right" Foreground="Red" Text="!"
FontWeight="Bold" Margin="5,0"/>
<Border BorderBrush="Red" BorderThickness="1">
<AdornedElementPlaceholder/>
</Border>
</DockPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="True">
<Setter Property="ToolTip"
Value="{Binding RelativeSource={RelativeSource Self},
Path=(Validation.Errors)[0].ErrorContent}"/>
</Trigger>
</Style.Triggers>
</Style>
public partial class UserViewModel : ObservableObject, IDataErrorInfo
{
[ObservableProperty] private string _name = string.Empty;
[ObservableProperty] private int _age;
public string Error => string.Empty;
public string this[string columnName]
{
get
{
return columnName switch
{
nameof(Name) when string.IsNullOrWhiteSpace(Name) =>
"Please enter a name.",
nameof(Name) when Name.Length < 2 =>
"Name must be at least 2 characters.",
nameof(Age) when Age < 0 || Age > 150 =>
"Please enter a valid age.",
_ => string.Empty
};
}
}
}
<TextBox Text="{Binding Name,
UpdateSourceTrigger=PropertyChanged,
ValidatesOnDataErrors=True}"/>
public abstract partial class ValidatableViewModelBase : ObservableObject, INotifyDataErrorInfo
{
private readonly Dictionary<string, List<string>> _errors = [];
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
: Enumerable.Empty<string>();
}
protected void AddError(string propertyName, string error)
{
if (!_errors.ContainsKey(propertyName))
{
_errors[propertyName] = [];
}
if (!_errors[propertyName].Contains(error))
{
_errors[propertyName].Add(error);
OnErrorsChanged(propertyName);
}
}
protected void ClearErrors(string propertyName)
{
if (_errors.Remove(propertyName))
{
OnErrorsChanged(propertyName);
}
}
protected void ClearAllErrors()
{
var properties = _errors.Keys.ToList();
_errors.Clear();
foreach (var prop in properties)
{
OnErrorsChanged(prop);
}
}
private void OnErrorsChanged(string propertyName)
{
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
OnPropertyChanged(nameof(HasErrors));
}
}
public partial class RegistrationViewModel : ValidatableViewModelBase
{
[ObservableProperty] private string _email = string.Empty;
[ObservableProperty] private string _password = string.Empty;
[ObservableProperty] private string _confirmPassword = string.Empty;
partial void OnEmailChanged(string value)
{
ValidateEmail();
}
partial void OnPasswordChanged(string value)
{
ValidatePassword();
ValidateConfirmPassword();
}
partial void OnConfirmPasswordChanged(string value)
{
ValidateConfirmPassword();
}
private void ValidateEmail()
{
ClearErrors(nameof(Email));
if (string.IsNullOrWhiteSpace(Email))
{
AddError(nameof(Email), "Please enter an email address.");
}
else if (!Email.Contains('@'))
{
AddError(nameof(Email), "Invalid email format.");
}
}
private void ValidatePassword()
{
ClearErrors(nameof(Password));
if (Password.Length < 8)
{
AddError(nameof(Password), "Password must be at least 8 characters.");
}
if (!Password.Any(char.IsDigit))
{
AddError(nameof(Password), "Password must contain a digit.");
}
}
private void ValidateConfirmPassword()
{
ClearErrors(nameof(ConfirmPassword));
if (ConfirmPassword != Password)
{
AddError(nameof(ConfirmPassword), "Passwords do not match.");
}
}
[RelayCommand(CanExecute = nameof(CanSubmit))]
private void Submit()
{
ValidateAll();
if (!HasErrors)
{
// Submit logic
}
}
private bool CanSubmit() => !HasErrors && !string.IsNullOrEmpty(Email);
private void ValidateAll()
{
ValidateEmail();
ValidatePassword();
ValidateConfirmPassword();
}
}
<TextBox Text="{Binding Email,
UpdateSourceTrigger=PropertyChanged,
ValidatesOnNotifyDataErrors=True}"/>
<!-- Error list display -->
<ItemsControl ItemsSource="{Binding (Validation.Errors),
RelativeSource={RelativeSource Self}}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding ErrorContent}" Foreground="Red"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
CommunityToolkit.Mvvm 8.0+ provides ObservableValidator.
public partial class UserViewModel : ObservableValidator
{
[Required(ErrorMessage = "Please enter a name.")]
[MinLength(2, ErrorMessage = "Name must be at least 2 characters.")]
[ObservableProperty] private string _name = string.Empty;
[Required]
[Range(1, 150, ErrorMessage = "Please enter a valid age.")]
[ObservableProperty] private int _age;
[EmailAddress(ErrorMessage = "Invalid email format.")]
[ObservableProperty] private string _email = string.Empty;
partial void OnNameChanged(string value) => ValidateProperty(value, nameof(Name));
partial void OnAgeChanged(int value) => ValidateProperty(value, nameof(Age));
partial void OnEmailChanged(string value) => ValidateProperty(value, nameof(Email));
[RelayCommand]
private void Submit()
{
ValidateAllProperties();
if (!HasErrors)
{
// Submit logic
}
}
}
| Requirement | Recommended Approach |
|---|---|
| Simple XAML validation | ValidationRule |
| ViewModel-based validation | INotifyDataErrorInfo |
| DataAnnotations usage | ObservableValidator (CommunityToolkit) |
| Async validation | INotifyDataErrorInfo |
| Legacy compatibility | IDataErrorInfo |
| Complex business rules | FluentValidation (validating-with-fluentvalidation skill) |
| Service layer errors | ErrorOr (handling-errors-with-erroror skill) |
npx claudepluginhub christian289/dotnet-with-claudecode --plugin wpf-dev-packIntegrates FluentValidation with WPF MVVM via INotifyDataErrorInfo bridge for form validation. Use for complex RuleFor, AbstractValidator rules or CommunityToolkit.Mvvm integration.
Generates FluentValidation validators for commands and queries in .NET, with built-in rules, custom validators, async validation, and MediatR pipeline behavior integration.
Building WPF on .NET 8+. Host builder, MVVM Toolkit, Fluent theme, performance, modern C# patterns.