This automatically turns all the object's data annotation attributes into ValidationRule
s, and can be applied once in the app on all TextBox
es:
using System;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using DaValidationResult = System.ComponentModel.DataAnnotations.ValidationResult;
using WinValidationResult = System.Windows.Controls.ValidationResult;
public sealed class DataAnnotationsBehavior
{
public static bool GetValidateDataAnnotations(DependencyObject obj) =>
(bool)obj.GetValue(ValidateDataAnnotationsProperty);
public static void SetValidateDataAnnotations(DependencyObject obj, bool value) =>
obj.SetValue(ValidateDataAnnotationsProperty, value);
public static readonly DependencyProperty ValidateDataAnnotationsProperty =
DependencyProperty.RegisterAttached("ValidateDataAnnotations", typeof(bool),
typeof(DataAnnotationsBehavior), new PropertyMetadata(false, OnPropertyChanged));
private static void OnPropertyChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
var boolean = (bool)e.NewValue;
if (!(d is TextBox textBox))
throw new NotSupportedException(
@$"The behavior " +
"'{typeof(DataAnnotationsBehavior)}' can only be applied " +
"on elements of type '{typeof(TextBox)}'.");
var bindingExpression =
textBox.GetBindingExpression(TextBox.TextProperty);
if (boolean)
{
var dataItem = bindingExpression.DataItem;
if (bindingExpression.DataItem == null)
return;
var type = dataItem.GetType();
var prop = type.GetProperty(bindingExpression.ResolvedSourcePropertyName);
if (prop == null)
return;
var allAttributes = prop.GetCustomAttributes(typeof(ValidationAttribute), true);
foreach (var validationAttr in allAttributes.OfType<ValidationAttribute>())
{
var context = new ValidationContext(dataItem, null, null)
{ MemberName = bindingExpression.ResolvedSourcePropertyName };
bindingExpression
.ParentBinding
.ValidationRules
.Add(new AttributesValidationRule(context, validationAttr));
}
}
else
{
var das =
bindingExpression
.ParentBinding
.ValidationRules
.OfType<AttributesValidationRule>()
.ToList();
if (das != null)
foreach (var da in das)
bindingExpression.ParentBinding.ValidationRules.Remove(da);
}
}
abstract class DaValidationRule : ValidationRule
{
public ValidationContext ValidationContext { get; }
public DaValidationRule(ValidationContext validationContext)
{
ValidationContext = validationContext;
}
}
class AttributesValidationRule : DaValidationRule
{
public ValidationAttribute ValidationAttribute { get; }
public AttributesValidationRule(ValidationContext validationContext,
ValidationAttribute attribute)
: base(validationContext) =>
ValidationAttribute = attribute;
public override WinValidationResult Validate(object value, CultureInfo cultureInfo)
{
var result = ValidationAttribute.GetValidationResult(value, ValidationContext);
return result == DaValidationResult.Success
? WinValidationResult.ValidResult
: new WinValidationResult(false, result.ErrorMessage);
}
}
}
Usage:
<Window.DataContext>
<local:ViewModel />
</Window.DataContext>
<Window.Resources>
<ResourceDictionary>
<Style TargetType="TextBox">
<Setter
Property="local:DataAnnotationsBehavior.ValidateDataAnnotations"
Value="True"/>
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="True">
<Setter
Property="ToolTip"
Value="{Binding (Validation.Errors)[0].ErrorContent,
RelativeSource={RelativeSource Self}}"/>
</Trigger>
</Style.Triggers>
</Style>
</ResourceDictionary>
</Window.Resources>
<StackPanel DataContext="{Binding Model}">
<TextBox Text="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
</StackPanel>
I made the DaValidationRule
class abstract because I think I might want to expand that in the future so it also covers IValidationAttribute
and maybe other scenarios.
For the version that covers TextBox
es within DataGridTextColumn
in edit mode, and for full code, see this.