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.
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 참조
| 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) |