我在WPF应用程序中创建了一个可重用的UserControl,该控件中有一个Textbox(在实际项目中为其他组件)。我正在使用Fluent Validation和INotifyDataErrorInfo来验证TextBoxes中的用户输入。我的问题是,当属性绑定到UserControl的TextBox的模型出错时,TextBox的样式不会根据设置的样式触发。看来,我的样式触发了UserControl的文本框,无法从模型中正确读取Validation.HasError值。那么有没有一种方法可以触发样式并获取UserControl的文本框的错误工具提示?
多年来,其他几个人都曾问过这个问题,我查看了其中的每个人,但没有一个人真正为我工作。我试图起作用的一件事是UserControl.xaml中用于文本框绑定的常规ValidationRule,但这不允许模型特定的规则。我希望一些WPF专家最终能够挑战并解决这个问题! :)
如果从我提供的代码中创建一个示例项目,并将Height属性设置为小于10,您会看到普通的TextBox带有工具提示会触发错误样式,而UserControl的TextBox则会显示基本的红色边框: Sample app with the cursor over the first textbox.
这是我的简化代码: UserControl:
<UserControl x:Class="UserControlValidationTest.DataInputUserControl"
x:Name="parentControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<UserControl.Resources>
<Style TargetType="TextBox" x:Key="TextBoxStyle">
<Style.Triggers>
<Trigger Property= "Validation.HasError" Value="true">
<Setter Property="Background" Value="Pink"/>
<Setter Property="ToolTip" Value="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}" />
</Trigger>
</Style.Triggers>
</Style>
</UserControl.Resources>
<StackPanel Orientation="Horizontal" DataContext="{Binding ElementName=parentControl}">
<TextBox Name="ValueBox" Text="{Binding Value, UpdateSourceTrigger=PropertyChanged}" Width="60" Style="{StaticResource TextBoxStyle}"/>
</StackPanel>
public partial class DataInputUserControl : UserControl
{
public DataInputUserControl()
{
InitializeComponent();
Validation.SetValidationAdornerSite(this, ValueBox);
}
public double Value
{
get => (double)GetValue(ValueProperty);
set => SetValue(ValueProperty, value);
}
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register("Value", typeof(double), typeof(DataInputUserControl), new FrameworkPropertyMetadata(0d, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
}
MainWindow.xaml:
<Window x:Class="UserControlValidationTest.MainWindow"
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"
xmlns:local="clr-namespace:UserControlValidationTest"
mc:Ignorable="d"
Title="MainWindow" Height="100" Width="200">
<Window.DataContext>
<local:MainWindowViewModel/>
</Window.DataContext>
<Window.Resources>
<Style TargetType="TextBox">
<Style.Triggers>
<Trigger Property= "Validation.HasError" Value="true">
<Setter Property="Background" Value="Pink"/>
<Setter Property="ToolTip" Value="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}" />
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>
<StackPanel>
<TextBox Text="{Binding User.Height, UpdateSourceTrigger=PropertyChanged}" Width="60" Margin="10"/>
<local:DataInputUserControl Value="{Binding User.Height}" HorizontalAlignment="Center"/>
</StackPanel>
查看模型:
public class MainWindowViewModel
{
public UserModel User { get; set; }
public MainWindowViewModel()
{
User = new UserModel();
}
}
用户模型:
public class UserModel : ValidatableModel
{
public double Height { get => GetPropertyValue<double>(); set => SetPropertyValue(value); }
public UserModel()
{
ModelValidator = new UserValidator();
Height = 180;
}
}
用户验证器:
public class UserValidator : AbstractValidator<UserModel>
{
public UserValidator()
{
RuleFor(x => x.Height)
.GreaterThan(10);
}
}
可验证模型:
using FluentValidation;
using FluentValidation.Results;
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
public abstract class ValidatableModel : INotifyDataErrorInfo, INotifyPropertyChanged
{
private readonly Dictionary<string, object> propertyBackingDictionary = new Dictionary<string, object>();
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string parameter = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(parameter));
}
protected T GetPropertyValue<T>([CallerMemberName] string propertyName = null)
{
if (propertyBackingDictionary.TryGetValue(propertyName, out object value))
{
return (T)value;
}
return default(T);
}
protected bool SetPropertyValue<T>(T newValue, [CallerMemberName] string propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(newValue, GetPropertyValue<T>(propertyName)))
{
return false;
}
propertyBackingDictionary[propertyName] = newValue;
OnPropertyChanged(propertyName);
Validate();
return true;
}
private ConcurrentDictionary<string, List<string>> errors = new ConcurrentDictionary<string, List<string>>();
public IValidator ModelValidator { get; set; }
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
public void OnErrorsChanged(string propertyName)
{
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
}
public IEnumerable GetErrors([CallerMemberName] string propertyName = null)
{
errors.TryGetValue(propertyName, out List<string> errorsForName);
return errorsForName;
}
public bool HasErrors => errors.Count > 0;
public void Validate()
{
errors.Clear();
var validationResults = ModelValidator.Validate(this);
foreach (var item in validationResults.Errors)
{
errors.TryAdd(item.PropertyName, new List<string> { item.ErrorMessage });
OnErrorsChanged(item.PropertyName);
}
}
}
}