如何让 WPF 验证冒泡到父控件?

How do you get WPF validation to bubble up to a parent control?

所以我有一个类似这个简化版本的控件:

<local:ImageMapField x:Class="ImageApp.WPF.Controls.ImageMapContentField"
             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" 
             xmlns:local="clr-namespace:ImageApp.WPF.Controls"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300"
                        x:Name="Me">

    <Grid HorizontalAlignment="Left">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="150" />
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <Label Grid.Column="0" HorizontalAlignment="Stretch" Style="{DynamicResource BaseLabelStyle}">
            <TextBlock Text="{Binding Header, RelativeSource={RelativeSource AncestorType=local:ImageMapContentField, Mode=FindAncestor}}" TextWrapping="WrapWithOverflow"></TextBlock>
        </Label>
        <StackPanel Grid.Column="1">
            <Image />
            <Border Margin="20,5,5,2">
                <ContentPresenter Content="{Binding DataEntryContent, ElementName=Me}" />
            </Border>
        </StackPanel>
    </Grid>

</local:ImageMapField>

我是这样使用它的:

<controls:ImageMapContentField Header="Foo Date" 
                                FieldName="FooDate"
                                ImageSource="{Binding MyImage, Mode=TwoWay}"
                                ItemsSource="{Binding Map.Items}"
                                Zoom="{Binding MapFieldZoom}">
    <controls:ImageMapContentField.DataEntryContent>
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>
            <TextBox Grid.Column="0" Text="{Binding MyDate, StringFormat=MM/dd/yyyy, ValidatesOnDataErrors=True, NotifyOnValidationError=True}">
                <controls:WatermarkService.Watermark>
                    <TextBlock>Date</TextBlock>
                </controls:WatermarkService.Watermark>
            </TextBox>
            <TextBox Grid.Column="1" Text="{Binding MyTime, StringFormat=HH\:mm}">
                <controls:WatermarkService.Watermark>
                    <TextBlock>Time</TextBlock>
                </controls:WatermarkService.Watermark>
            </TextBox>
        </Grid>
    </controls:ImageMapContentField.DataEntryContent>
</controls:ImageMapContentField>

问题是,因为我没有将模型的 属性 绑定到 ImageMapContentField 上的某些内容,所以 ImageMapContentField 上的 Validation.HasError 始终为 false,永远不会触发。

我得到的是默认的 TextBox 验证。

我真正想要的是 ImageMapContentField 有粉红色的背景。这适用于我直接绑定到某些东西的其他控件,但我无法让它适用于具有 ContentPresenter.

的控件

我希望我只是遗漏了一些可以让家长捕获验证的东西。


此处要求的是问题的最小示例:

MainWindow.xaml

<Window x:Class="WpfApp1.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:WpfApp1"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">


    <Window.Resources>
        <Style TargetType="local:CustomTextField">
            <Setter Property="Validation.ErrorTemplate">
                <Setter.Value>
                    <ControlTemplate>
                        <Border BorderBrush="Red" BorderThickness="2" CornerRadius="2">
                            <AdornedElementPlaceholder/>
                        </Border>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
            <Style.Triggers>
                <Trigger Property="Validation.HasError" Value="True">
                    <Setter Property="ToolTip" Value="{Binding RelativeSource={x:Static RelativeSource.Self}, Path=(Validation.Errors).CurrentItem.ErrorContent}"/>
                    <Setter Property="Background" Value="LightPink"/>
                </Trigger>
            </Style.Triggers>
        </Style>

        <Style TargetType="local:CustomContentControl">
            <Setter Property="Validation.ErrorTemplate">
                <Setter.Value>
                    <ControlTemplate>
                        <Border BorderBrush="Red" BorderThickness="2" CornerRadius="2">
                            <AdornedElementPlaceholder/>
                        </Border>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
            <Style.Triggers>
                <Trigger Property="Validation.HasError" Value="True">
                    <Setter Property="ToolTip" Value="{Binding RelativeSource={x:Static RelativeSource.Self}, Path=(Validation.Errors).CurrentItem.ErrorContent}"/>
                    <Setter Property="Background" Value="LightPink"/>
                </Trigger>
            </Style.Triggers>
        </Style>
    </Window.Resources>

    <Window.DataContext>
        <local:MyModel />
    </Window.DataContext>
    <StackPanel>
        <local:CustomTextField LabelText="Number 1" Value="{Binding Number1, Mode=TwoWay, NotifyOnValidationError=True, ValidatesOnDataErrors=True, ValidatesOnNotifyDataErrors=True}" />
        <local:CustomContentControl LabelText="Number 2">
            <local:CustomContentControl.DataEntryContent>
                <TextBox Text="{Binding Number2, Mode=TwoWay, NotifyOnValidationError=True, ValidatesOnDataErrors=True, ValidatesOnNotifyDataErrors=True}" />
            </local:CustomContentControl.DataEntryContent>    
        </local:CustomContentControl>
    </StackPanel>
</Window>

CustomTextField.xaml

<UserControl x:Class="WpfApp1.CustomTextField"
             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" 
             xmlns:local="clr-namespace:WpfApp1"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300"
             x:Name="Me">
        <StackPanel>
            <Label Content="{Binding ElementName=Me, Path=LabelText}" />
            <TextBox Text="{Binding ElementName=Me, Path=Value}" />
        </StackPanel>
</UserControl>

CustomTextField.cs

public partial class CustomTextField : UserControl
    {
        public static readonly DependencyProperty LabelTextProperty = DependencyProperty.Register(
                                                        "LabelText", typeof(string), typeof(CustomTextField), new PropertyMetadata(default(string)));

        public static readonly DependencyProperty ValueProperty = DependencyProperty.Register(
                                                        "Value", typeof(string), typeof(CustomTextField), new PropertyMetadata(default(string)));

        public string Value
        {
            get { return (string) GetValue(ValueProperty); }
            set { SetValue(ValueProperty, value); }
        }

        public string LabelText
        {
            get { return (string) GetValue(LabelTextProperty); }
            set { SetValue(LabelTextProperty, value); }
        }

        public CustomTextField()
        {
            InitializeComponent();
        }
    }

CustomContentControl.xaml

<UserControl x:Class="WpfApp1.CustomContentControl"
             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" 
             xmlns:local="clr-namespace:WpfApp1"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300"
             x:Name="Me">
        <Grid>
            <StackPanel>
                <Label Content="{Binding ElementName=Me, Path=LabelText}" />
                <ContentPresenter Content="{Binding DataEntryContent, ElementName=Me}" />
            </StackPanel>
        </Grid>
</UserControl>

CustomContentControl.cs

public partial class CustomContentControl : UserControl
    {
        public static readonly DependencyProperty LabelTextProperty = DependencyProperty.Register(
                                                        "LabelText", typeof(string), typeof(CustomContentControl), new PropertyMetadata(default(string)));

        public static readonly DependencyProperty DataEntryContentProperty = DependencyProperty.Register(
                                                        "DataEntryContent", typeof(object), typeof(CustomContentControl), new PropertyMetadata(default(object)));

        public object DataEntryContent
        {
            get { return (object) GetValue(DataEntryContentProperty); }
            set { SetValue(DataEntryContentProperty, value); }
        }

        public string LabelText
        {
            get { return (string) GetValue(LabelTextProperty); }
            set { SetValue(LabelTextProperty, value); }
        }

        public CustomContentControl()
        {
            InitializeComponent();
        }
    }

MyModel.cs

public class MyModel : INotifyPropertyChanged
    {
        int _number1;
        int _number2;

        public int Number1
        {
            get { return _number1; }
            set
            {
                _number1 = value;
                OnPropertyChanged();
            }
        }

        public int Number2
        {
            get { return _number2; }
            set
            {
                _number2 = value;
                OnPropertyChanged();
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }

WPF 验证已经冒泡到父控件(即使子控件位于 ContentPresenter 中)- Validation.ErrorEvent

这里的问题是,即使事件冒泡,附加的 属性 Validation.HasError 也不会更新 - 这基本上是因为控件的 属性绑定。因此,您看不到背景变化。

要纠正此问题 - 您可以使用此代码:

更新 MainWindow.xaml

中的样式
    <Style TargetType="local:CustomContentControl">
        <Setter Property="Validation.ErrorTemplate">
            <Setter.Value>
                <ControlTemplate>
                    <Border BorderBrush="Red" BorderThickness="2" CornerRadius="2">
                        <AdornedElementPlaceholder/>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
        <Style.Triggers>
            <Trigger Property="HasErrors" Value="True">
                <Setter Property="Background" Value="LightPink"/>
            </Trigger>
        </Style.Triggers>
    </Style>

并且,更新 CustomContentControl 以添加 HasErrors 依赖项 属性,以及验证错误事件处理程序

    public static readonly DependencyProperty HasErrorsProperty = DependencyProperty.Register("HasErrors", typeof(bool), typeof(CustomContentControl), new PropertyMetadata(false));

    public bool HasErrors
    {
        get { return (bool)GetValue(HasErrorsProperty); }
        set { SetValue(HasErrorsProperty, value); }
    }

    public CustomContentControl()
    {
        InitializeComponent();

        Validation.AddErrorHandler(this, (s, args) => {
            if (args.Action == ValidationErrorEventAction.Added)
            {
                this.ToolTip = args.Error.ErrorContent;
                HasErrors = true;
            }
            else
            {
                this.ToolTip = null;
                HasErrors = false;
            }
        });  
    }

您的背景也会更新。