Using converter in Validation.Errors binding - wpf

In my WPF application (.Net 4.5) I want to provide extended visual feedback about validation results to the UI. The data layer's validation engine returns warnings and errors via INotifyDataErrorInfo interface.
I have the following XAML to display red or orange border depending on the error type and the list of error messages. Here errorToColor is a resource key of the value converter which returns red brush if there is at least one error in the Validation.Errors collection and orange brush if there are only warnings.
<TextBox Name="MappingName" Text="{Binding Path=Mapping.Name, NotifyOnValidationError=True}" >
<Validation.ErrorTemplate>
<ControlTemplate>
<DockPanel>
<Border BorderBrush="{Binding Converter={StaticResource errorsToColor}}" BorderThickness="1">
<AdornedElementPlaceholder />
</Border>
<ListView DisplayMemberPath="ErrorContent" ItemsSource="{Binding}" />
</DockPanel>
</ControlTemplate>
</Validation.ErrorTemplate>
</TextBox>
Now let's see what happens when I type some 'not valid' text in the TextBox.
Typed 'Text1' and changed focus.Debugger stepped into the converter and both validators resulting in two items in the ListView (1 error and 1 warning) and a red border. [OK]
Typed 'Text' to correct the error, changed focus.
The value converter wasn't even hit! Of course, the same red border. But the ListView has changed and shows only one warning.
Can somebody explain what's going on? Why the ListView receives collection change notification and the Border not? Is it because ListView is an ItemsControl and the Validation.Errors is wrapped into the CollectionView?

For those who are interested. The errorsToColor converter wasn't fired because the Validation.Errors collection didn't raise PropertyChanged event (needed trigger binding coverter) when errors were added or removed.
In order to raise PropertyChanged event we need to bind to a property which is changed on each error is added, for example Count. I still need the Errors collection itself in the converter, so I used a multi-binding here.
<Border BorderThickness="1">
<Border.BorderBrush>
<MultiBinding Converter="{StaticResource errorsToColor}">
<Binding Path="." />
<Binding Path=".Count" />
</MultiBinding>
</Border.BorderBrush>
<AdornedElementPlaceholder Name="adornedElement" />
</Border>
Now the errorsToColor converter (which is now implements IMultiValueConverter) is executed every time new error is added / removed.

Related

WPF FindAncestor binding finds source in Release, fails in Debug

I have a binding error that makes no sense. It always works in Release Mode. It sometimes -- only sometimes -- fails to find the source via FindAncestor in Debug Mode
Its for Style for a WPF Path that I use only when that Path is inside a specific custom control named LayerView. Below is the style. Note the 3 bindings that look for the parent source object LayerView are the ones that sometimes fail
<Style x:Key="LayerViewGuidePathStyle" TargetType="{x:Type Path}">
<Setter Property="Data">
<Setter.Value>
<MultiBinding Converter="{StaticResource CvtGuideOption}">
<Binding Source="{svc:ViewSettings}, Path=GuideOption}" />
<Binding RelativeSource="{RelativeSource AncestorType={x:Type ctrl:LayerView}}" Path="ScanWidth" Converter="{ctrl:LogValueConverter}"/>
<Binding RelativeSource="{RelativeSource AncestorType={x:Type ctrl:LayerView}}" Path="ScanHeight" Converter="{ctrl:LogValueConverter}"/>
<Binding RelativeSource="{RelativeSource AncestorType={x:Type ctrl:LayerView}}" Path="SceneTransform" Converter="{ctrl:LogValueConverter}"/>
</MultiBinding>
</Setter.Value>
</Setter>
</Style>
When they fail in Debug mode, this is the warning
System.Windows.Data Warning: 4 : Cannot find source for binding with
reference 'RelativeSource FindAncestor,
AncestorType='MyControls.LayerView', AncestorLevel='1''.
BindingExpression:Path=ScanHeight; DataItem=null; target element is
'Path' (Name='GuidePath'); target property is 'Data' (type 'Geometry')
Here is how I use this style inside my custom control LayerView
<ctrl:LayerView x:Name="MainLayerView" ItemsSource="{Binding Shapes}">
<ctrl:LayerView.Layers>
<Path x:Name="GuidePath" Style="{StaticResource LayerViewGuidePathStyle}" />
</ctrl:LayerView.Layers>
</ctrl:LayerView>
As you can probably tell, LayerView is an ItemsControl: Specifically a MultiSelector. My custom version merely adds non-dependency property named Layers which is a Collection<object>. The idea is that the user will set these statically in XAML, as I did above, and I will display them over the standard items of the control.
public class LayerView : MultiSelector
{
static LayerView()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(LayerView), new FrameworkPropertyMetadata(typeof(LayerView)));
}
private Collection<object>? _layers;
public Collection<object> Layers => _layers ??= new Collection<object>();
}
Finally, here is the ControlTemplate for LayerView. In addition to the required ItemsPresenter, I add an ItemsControl to show the contents of Layers as basically non-hit-detectable "overlays"
<ControlTemplate x:Key="LayerViewTemplate" TargetType="{x:Type gcl:LayerView}">
<Canvas x:Name="PART_MainCanvas" Background="Transparent">
<!-- We are an ItemsControl so present our items -->
<ItemsPresenter x:Name="PART_Items" />
<!-- Now present our custom layers. -->
<ItemsControl x:Name="PART_Layers"
ItemsSource="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Layers}"
IsHitTestVisible="False"/>
</Canvas>
</ControlTemplate
I have no moves here. I can usually fix bindings that fail, but I would expect consistent failure or success. Not a mix. And especially not intermittent failures It's almost as if it's a race condition.
Can anyone point me in the right direction as to why this would fail? Is my Layers property of the wrong type or is it missing some crucial attribute or something?
Without reproducing this error, of course, it is difficult to say exactly what the cause is. But I have some guesses.
The elements of the Layers collection are initialized BEFORE being added to the collection, but the binding is resolved after the element is loaded;
Layers is a regular CLR collection and information about loading ItemsControl (LayerView ) is not passed to it. Therefore, the elements of this collection, as I think, are initialized completely at once, including bindings. And this can happen before these items are included in the ItemsControl panel.
As I think, these errors also occur in Release, it's just that not all errors are caught in this mode. It is guaranteed that only errors that are critical for the operation of the application are caught. For example, exceptions.
At a minimum, you need to replace the collection with a FreezableCollection and implement a read-only DependecyProperty.
True, in this case, you can only include DependencyObject in it.
private static readonly DependencyPropertyKey LayersPropertyKey
= DependencyProperty.RegisterReadOnly(
nameof(Layers),
typeof(FreezableCollection<DependencyObject>),
typeof(LayerView),
new PropertyMetadata(null, (d, e) => ((LayerView)d).protectedLayers = (FreezableCollection<DependencyObject>)e.NewValue));
public static readonly DependencyProperty LayersProperty
= LayersPropertyKey.DependencyProperty;
public FreezableCollection<DependencyObject> Layers
{
get => (FreezableCollection<DependencyObject>)GetValue(LayersProperty);
}
// An optional auxiliary property for quickly obtaining a collection
// in the internal logic of the LayerView and its derivatives.
protected FreezableCollection<DependencyObject> protectedLayers {get; private set;}
// Initializing a Collection in a Constructor.
public LayerView() => SetValue(LayersPropertyKey, new FreezableCollection<DependencyObject>());
Supplement
Unfortunately, I could not immediately check your version - there was no "at hand" computer with the Studio.
Now I checked it.
I did not change the code of the LayerView element itself, I did not set the template in the style.
I got the Binding error only if the Element Template is not set.
<Window x:Class="Core2023.SO.Joe.BindingErrors.BindingErrorsWindow"
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:Core2023.SO.Joe.BindingErrors"
mc:Ignorable="d"
Title="BindingErrorsWindow" Height="150" Width="400">
<Window.Resources>
<ControlTemplate x:Key="LayerViewTemplate" TargetType="{x:Type local:LayerView}">
<ItemsControl x:Name="PART_Layers"
ItemsSource="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Layers}"/>
</ControlTemplate>
</Window.Resources>
<Grid>
<local:LayerView>
<local:LayerView.Layers>
<TextBlock Text="{Binding ActualHeight, RelativeSource={RelativeSource AncestorType={x:Type local:LayerView}}}"/>
</local:LayerView.Layers>
</local:LayerView>
</Grid>
</Window>
In order to accurately diagnose the cause of your problem, I need more complete code to reproduce the problem.

How do I properly set this button's binding to the parent ListView ItemsControl?

I have a custom button set up inside a ListView ItemTemplate. The Listview's ItemSource is bound to a collection of items, pretty standard. I have a few labels in the listview as well, and everything works fine except the button.
Binding the button to one of the properties won't work at all using {Binding buttonName} but it will sort of work if I use {Binding Items/buttonName, ElementName=listView} - the only problem is, when I do it this way, every single button in that listView will have the exact same buttonName.
Now the issue stems from my custom button's DataContext being set to Self; unfortunately, it has to be set to Self because the custom style I'm using needs this. If I try to change the button to a UserControl instead (with the button as a child, and the DataContext set on that), then I can't use the Command property of the button for some reason.
Here's a simplified version of my ListView making use of the custom button:
<ListView x:Name="listView" ItemsSource="{Binding MyPeopleData}">
<ListView.ItemTemplate>
<DataTemplate>
<Grid>
<Label Content="{Binding PersonName}"/>
<ct:RevealButton Content="{Binding Items/recommendation, ElementName=listView}"/>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
As I said above, this will make every item in the listview use the same recommendation property rather than using it's own one.
If I try to use
<ct:RevealButton Content="{Binding recommendation}"/>
It just won't work, which makes sense given the DataContext of the custom button, which is below:
<Button x:Class="RevealButton" Width="{Binding Width}" Height="{Binding Height}" Background="{Binding ButtonBackground}" DataContext="{Binding RelativeSource={RelativeSource Self}}" Style="{DynamicResource ButtonRevealStyleC}" mc:Ignorable="d">
<Button.ContentTemplate>
<DataTemplate>
<ContentPresenter Content="{TemplateBinding Content}" />
</DataTemplate>
</Button.ContentTemplate>
</Button>
So this ended up being an XY Problem. Because the modified style I was using had a poorly bound property, it was failing when the parent control didn't have a DataContext set to self.
This is what it was:
<SolidColorBrush Opacity="0.8" Color="{Binding ButtonBackground.Color}" />
And ButtonBackground was a dependency property exposed by my custom Button, so binding the style in this way meant it only worked if the Button's context was itself. Changing it to this:
<SolidColorBrush Opacity="0.8" Color="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=ButtonBackground.Color}" />
Fixed the DataContext dependency, which in turn fixes the heirarchy of problems above.

WPF: Validation.ErrorTemplate not displaying

I have a template for displaying a red border and an error message around controls. It works (tested on TextBoxes and ComboBoxes). But on two particular comboboxes they don't.
Well let's see what's different in the VM:
since I have common validation implementation in my base class, no change there
the same kind of asynchronously loaded data is displayed that works well with validation with just one panel over
so in short, no difference in VM.
The view is completely the same, the same style is applied, so in short no difference there either.
So I added NotifyOnValidationError=True to the ValidatesOnDataErrors=True already there, and subscribed to Validation.Error... And it fired!!! Yet the template still doesn't show. I'm out of ideas, please suggest me things to check!
EDIT: further research:
I've decompiled DataErrorValidationRule, and recompiled it as MyDataErrorValidationRule to match the original as close as possible. I removed ValidatesOnDataErrors=True, and added my ValidationRule to debug. It returned new ValidationResult(false, (object)str); with str containing the correct error message twice - once for setting the property to null, and once for forcefully validating the entire object. Template still not showing.
I've also checked Validation.GetErrorTemplate on the control (at the time of the first firing of Validation.Error) and it was NOT NULL, so it's not the DynamicResource that failed either.
EDIT: working example:
<ItemsControl ItemsSource="{Binding QuestionAnswers}">
<ItemsControl.Resources>
<!-- ... -->
<DataTemplate DataType="{x:Type Model:QuestionAnswerModel}">
<StackPanel>
<!-- here is the combo box -->
<ComboBox Margin="8,4" Padding="8" MinWidth="120" HorizontalAlignment="Left"
Validation.ErrorTemplate="{DynamicResource DefaultValidationErrorTemplate}"
ItemsSource="{Binding Options.Source}"
DisplayMemberPath="ItemName" SelectedValuePath="ItemID"
SelectedValue="{Binding Options.SelectedID, ValidatesOnDataErrors=true}" />
</StackPanel>
</DataTemplate>
</ItemsControl.Resources>
</ItemsControl>
non-working example:
<ComboBox Margin="8,4" Padding="8" MinWidth="120" HorizontalAlignment="Left"
Validation.ErrorTemplate="{DynamicResource DefaultValidationErrorTemplate}"
SelectedItem="{Binding Type.SelectedItem, ValidatesOnDataErrors=True, NotifyOnValidationError=True}" Validation.Error="ComboBox_Error"
ItemsSource="{Binding Type.Source}"
DisplayMemberPath="Localized"
>
They are from the same xaml file, the ItemsControl containing the working ComboBox is in the same Grid as the non-working ComboBox.
The only difference is whether SelectedItem or SelectedValue is bound, but that shouldn't have any bearings on the validation...
I had the exact problem with the error template not displaying even though the event was firiing, and could never figure out why it only happened for some controls and not others.
The workaround I eventually found was to set the ValidationStep to ConvertedProposedValue on the ValidationRule for the binding:
<TextBox>
<TextBox.Text>
<Binding Path="MyField">
<Binding.ValidationRules>
<Validation:MyValidationRule ValidationStep="ConvertedProposedValue" Message="Please enter a value." />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
That seemed to do the trick for me anyway!
EDIT: If you are using IDataErrorInfo, you could try (though I haven't personally tested it):
<Binding Path="MyField" ValidatesOnExceptions="True">
<Binding.ValidationRules>
<DataErrorValidationRule ValidationStep="ConvertedProposedValue" />
</Binding.ValidationRules>
</Binding>
i.e. remove ValidatesOnDataErrors=True, which is just a shortcut for including a single <DataErrorValidationRule />
i would check the following:
check datacontext and binding for your combobox, if it works and IDataErrorInfo is called (i assume you do IDataErrorInfo validation) - next step
copy your validation template as a local resource to your ComboBox.Resources and check if it works
nevertheless it would be good if you post your validation template, your combobox xaml and your datacontext

WPF Validation Result Popup

I'm trying to implement WPF validation on a few text boxes. I want to be able to display the validation errors to the user with something more obvious than a tool-tip, but more subtle than a dialog or messagebox. I settled on using the Popup class, rather than some built in textbox for displaying errors, since there are many different fields that need to be validated this way, and I want the feedback to be "attached" to the field in question.
The problem I'm experiencing is that the binding of the Popup's child TextBox to the attached TextBox's (Validation.Errors) property is not updated aggressively enough. As soon as there is an Error object, the text is updated and displayed (i.e. "Please enter a name." for an empty field) but if the error changes (i.e. the user enters invalid text) the message in the popup stays the same... until/unless they enter valid input, at which point the popup disappears as desired.
I've done some debugging, and I've found that while the validation rule is being called correctly and returning the proper results, the converter for the Popup is only called at the creation of the initial error. I guess what I am confused about is why the Popup gets updated only when the validation state goes from "no errors" to "some error(s)", so to speak. Does anyone know a way to force the changes to Validation.Errors to be reflected in my Popup's TextBox.Text?
Here's a xaml example of what I've written.
<TextBox Name="MyTextBox">
<TextBox.Text>
<Binding Path="MyText" UpdateSourceTrigger="PropertyChanged" >
<Binding.ValidationRules>
<local:MyTextBoxValidationRule />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<Popup Name="MyPopup" IsOpen="{Binding ElementName=MyTextBox,
Path=(Validation.HasError), Mode=OneWay}">
<Border BorderThickness="1" BorderBrush="Red" Background="White">
<TextBlock Foreground="Red" Margin="5 5 5 5"
Text="{Binding ElementName=MyTextBox, Path=(Validation.Errors),
Converter={StaticResource errorsToMessageConverter}}"/>
</Border>
</Popup>
I was able to find a compromise. I changed the binding on the Popup's TextBlock like so:
<TextBlock Name="MyPopupTextBox" Foreground="Red" Margin="5 5 5 5"
Text="{Binding ElementName=MyTextBox, Path=(Validation.Errors)[0].ErrorContent,
UpdateSourceTrigger=PropertyChanged, Mode=OneWay, NotifyOnValidationError=True,
NotifyOnSourceUpdated=True, ValidatesOnExceptions=True}"/>
Your original problem is a typical problem when binding to a collection: As long as your collection does not change structurally (insert/remove/clear), the binding will not see any reason to update. So if only an item in the collection changes some property, this does not cause your binding to update.
You have already solved this by binding to the element in this collection, this way the full path to the changed property (the ErrorContent) will be observed in the binding.
The problem with binding (Validation.Errors)[0] is that it raises IndexOutOfRange-Exceptions (not thrown, but they are there) if the Error-Collection is empty.
This thread discusses that exception and also contains a workaround that works fine for me:
http://social.msdn.microsoft.com/forums/en/wpf/thread/1c6cd214-abce-4a8b-a919-0726dd81461a/
Just replace (Validation.Errors)[0].ErrorContent by (Validation.Errors).CurrentItem.ErrorContent and you are done.

Dependency propery for ValueConverter or rebind control?

I have custom control with some text in content template:
<ControlTemplate TargetType="{x:Type local:TouchScreenKey}">
<TextBlock><ContentPresenter Content="{TemplateBinding Title, Converter={StaticResource CaseConverter}}" /></TextBlock>
</ControlTemplate>
and custom IValueConverter CaseConverter - with property UpperCase. So, when UpperCase property of converter set to true it converts text to upper case on binding. Everything goes fine if I change UpperCase in markup. But if i change property in runtime - nothing happens - because changing converter property not force my control to rebind.
How can I rebind control which uses converter on converter's property change?
As far as I know there is no way to tell converter to update all targets. Converter knows nothing about targets. It's just a stateless function, F(x), takes one value and returns another.
To update property you should ask WPF to do so. For example, if property is bound to some source property, you can implement INotifyPropertyChanged, and trigger PropertyChanged event. Or you can ask BindingOperations to get binding expression, and invoke UpdateTarget() manually.
Maybe converter isn't the best choice here? You may also want to consider using Attached Properties to change capitalization.
It may help someone - I found solution - using multibinding
<TextBlock HorizontalAlignment="Center" VerticalAlignment="Center">
<ContentPresenter>
<ContentPresenter.Content>
<MultiBinding Converter="{StaticResource MultiCaseConverter}">
<Binding RelativeSource="{RelativeSource TemplatedParent}" Path="Title" />
<Binding ElementName="TouchKeyboard" Path="UpperCase" />
</MultiBinding>
</ContentPresenter.Content>
</ContentPresenter>
and wrote MultiCaseConverter - which convert first parameter depending from second (UpperCase)

Resources