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

1

6

So I have a control like this simplified version:

<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>

And am using it like so:

<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>

The problem is that because I am not binding my model's property to something on the ImageMapContentField, Validation.HasError on the ImageMapContentField is always false and never triggers.

What I get instead is the default TextBox validation.

What I really want is the ImageMapContentField to have a pink background. This works for my other controls where I am binding directly to something, but I cannot get this to work for the controls that have a ContentPresenter.

I am hoping I am just missing something that would allow the parent to capture the validation.


As requested here is a minimal example of the issue:

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));
        }
    }

Wintertide answered 21/4, 2017 at 23:2 Comment(2)
Do you have a complete project that reproduces the problem?Ezara
@SimonMourier Added an example.Wintertide
T
6

The WPF validation is already bubbling up to the parent control (even when the child control is inside a ContentPresenter) - Validation.ErrorEvent

The problem here is that even though the event bubbles up, the attached property Validation.HasError doesn't get updated - that's basically due to the fact that there is no error in the control's property bindings. And hence, you don't see the background change.

To rectify this - you can use this code:

Update style in 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>

And, update CustomContentControl to add a HasErrors dependency property, and validation error event handler

    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;
            }
        });  
    }

And your background will be updated. enter image description here

Tortuosity answered 25/4, 2017 at 0:41 Comment(2)
Thank you! I knew there had to be something I was missing. Was hoping to somehow trigger the standard Validation.HasError so it was just like everything else, but this will work and is pretty simple.Wintertide
Yes - my first instinct too was to manually add an error to Validation.Errors collection to trigger an update in Validation.HasError. But introducing a custom dependency property seemed much simpler; especially if you have only one binding validation to track. If you have multiple bindings to track (multiple child controls) - then would recommend using the Validation.Errors to keep track of errors. #2174751Tortuosity

© 2022 - 2024 — McMap. All rights reserved.