I am using a StackPanel to host several TextBoxes that have validation rules attached. I also have a StackPanel.BindingGroup validation, see code below:
I have a BindingGroup validation rule called: ValidateAll from which I would like to display the error message in a TextBlock on my StatusBar. I only want to display the ValidateAll message as the TextBox validation messages are displayed below the TextBoxes.
I would like to setup a style for my TextBlock where I can display only the validation error message from my BindingGroup, (the ValidateAll rule).
I know I can do this in code by handling the ItemError event, where I can get the rule associated with an error message, through the ValidationError.RuleInError property, (see below).
I would like to be able to accomplish this in xaml, possibly by setting up a Style/Trigger/Setter combination to my StatusBar TextBlock. Any help would be much appreciated.
Code:
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Diagnostics;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Globalization;
namespace WpfGroupValidationDemo2
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new ViewModel();
}
private void PreviewTextBoxKeyUp(object sender, System.Windows.Input.KeyEventArgs e)
{
this.TextBoxStack.BindingGroup.UpdateSources();
}
// This event occurs when a ValidationRule in the BindingGroup or in a Binding fails.
private void ItemError(object sender, ValidationErrorEventArgs e)
{
if ((e.Action == ValidationErrorEventAction.Added) &&
(e.Error.RuleInError.ToString() == "WpfGroupValidationDemo2.ValidateAll"))
{
StatusTextBlock.Text = e.Error.ErrorContent.ToString();
}
else
StatusTextBlock.Text = String.Empty;
}
}
public class ViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public ViewModel()
{
this.name = "Allan";
this.age = 30;
}
#region Properties
private string name;
public string Name
{
get { return this.name; }
set
{
if (value != name)
{
this.name = value;
this.OnPropertyChanged(nameof(Name));
}
}
}
private int age;
public int Age
{
get { return this.age; }
set
{
if (value != this.age)
{
this.age = value;
this.OnPropertyChanged(nameof(Age));
}
}
}
#endregion Properties
private void OnPropertyChanged([CallerMemberName] String propertyName = "")
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
#region Validation Rules
public class ValidateAgeRule : ValidationRule
{
public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
{
if (!int.TryParse(value.ToString(), out int i))
return new ValidationResult(false, "Please enter a valid integer value.");
if (i < 30 || i > 70)
return new ValidationResult(false, "Age must be between 30 and 70");
return new ValidationResult(true, null);
}
}
public class ValidateNameRule : ValidationRule
{
public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
{
string name = (string)value;
if (name != "Allan" && name != "Jim")
return new ValidationResult(false, "Please enter the names: Allan or Jim");
return new ValidationResult(true, null);
}
}
public class ValidateAll : ValidationRule
{
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
if (value == null)
return ValidationResult.ValidResult;
BindingGroup bg = value as BindingGroup;
ViewModel viewModel = bg.Items[0] as ViewModel;
object ageValue;
object nameValue;
// Get the proposed values for age and name
bool ageResult = bg.TryGetValue(viewModel, "Age", out ageValue);
bool nameResult = bg.TryGetValue(viewModel, "Name", out nameValue);
if (!ageResult || !nameResult)
return new ValidationResult(false, "Properties not found");
int age = (int)ageValue;
string name = (string)nameValue;
if ((age == 30 ) && (name == "Jim"))
return new ValidationResult(false, "Jim cannot be Thirty!");
return ValidationResult.ValidResult;
}
}
#endregion Validation Rules
}
XAML:
Window x:Class="WpfGroupValidationDemo2.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:WpfGroupValidationDemo2"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.Resources>
<ControlTemplate x:Key="validationTemplate" >
<StackPanel>
<!--Placeholder for the TextBox itself-->
<AdornedElementPlaceholder/>
<TextBlock Text="{Binding [0].ErrorContent}" Foreground="Red" Background="{DynamicResource {x:Static SystemColors.ControlLightLightBrushKey}}"/>
</StackPanel>
</ControlTemplate>
<!-- Add a red border on validation error to a textbox control -->
<Style x:Key="TextBoxBorderStyle" TargetType="TextBox">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TextBox}">
<Border x:Name="bg" BorderBrush="#FFABADB3" BorderThickness="1">
<ScrollViewer x:Name="PART_ContentHost" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="Validation.HasError" Value="True" >
<Trigger.Setters>
<Setter Property="BorderBrush" TargetName="bg" Value="Red"/>
<Setter Property="BorderThickness" TargetName="bg" Value="1"/>
<Setter Property="SnapsToDevicePixels" TargetName="bg" Value="True"/>
</Trigger.Setters>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<Grid>
<StackPanel HorizontalAlignment="Left" Height="204" Margin="168,125,0,0" VerticalAlignment="Top" Width="409" RenderTransformOrigin="0.5,0.5" Orientation="Horizontal">
<StackPanel Width="184" HorizontalAlignment="Right">
<Label Content="Name:" HorizontalAlignment="Right" Margin="0,3"/>
<Label Content="Age:" HorizontalAlignment="Right"/>
</StackPanel>
<StackPanel Name="TextBoxStack" Width="200" Height="202" Validation.ErrorTemplate="{x:Null}" Validation.Error="ItemError">
<StackPanel.BindingGroup>
<BindingGroup Name="ValidateAllFields" NotifyOnValidationError="True">
<BindingGroup.ValidationRules>
<local:ValidateAll ValidationStep="ConvertedProposedValue"/>
</BindingGroup.ValidationRules>
</BindingGroup>
</StackPanel.BindingGroup>
<TextBox x:Name="NameTextBox" Style="{StaticResource TextBoxBorderStyle}" TextWrapping="Wrap" Height="26" VerticalContentAlignment="Center"
Margin="0,3,130,3" Validation.ErrorTemplate="{StaticResource validationTemplate}" PreviewKeyUp="PreviewTextBoxKeyUp">
<TextBox.Text>
<Binding Path="Name" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<local:ValidateNameRule ValidationStep="RawProposedValue" ValidatesOnTargetUpdated="True"/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<TextBox x:Name="AgeTextBox" Style="{StaticResource TextBoxBorderStyle}" Height="26" TextWrapping="Wrap" VerticalContentAlignment="Center"
Margin="0,0,130,3" Validation.ErrorTemplate="{StaticResource validationTemplate}" PreviewKeyUp="PreviewTextBoxKeyUp">
<TextBox.Text>
<Binding Path="Age" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<local:ValidateAgeRule ValidationStep="RawProposedValue" ValidatesOnTargetUpdated="True"/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
</StackPanel>
</StackPanel>
<Label Content="BindingGroup Demo" HorizontalAlignment="Left" Margin="204,78,0,0" VerticalAlignment="Top" Width="305"/>
<Label Content="Only Visible when All the textboxes pass validation!" HorizontalAlignment="Left" Margin="417,332,0,0" VerticalAlignment="Top" Width="286" >
<Label.Style>
<Style TargetType="{x:Type Label}">
<Setter Property="Visibility" Value="Hidden" />
<Style.Triggers>
<!-- Require the controls to be valid in order to be visible -->
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding ElementName=NameTextBox, Path=(Validation.HasError)}" Value="false" />
<Condition Binding="{Binding ElementName=AgeTextBox, Path=(Validation.HasError)}" Value="false" />
<Condition Binding="{Binding ElementName=TextBoxStack, Path=(Validation.HasError)}" Value="false" />
</MultiDataTrigger.Conditions>
<Setter Property="Visibility" Value="Visible" />
</MultiDataTrigger>
</Style.Triggers>
</Style>
</Label.Style>
</Label>
<StatusBar Margin="4,0,0,1" VerticalAlignment="Bottom" VerticalContentAlignment="Bottom" Padding="0,3" >
<StatusBarItem>
<TextBlock Name="StatusTextBlock" Foreground="Red" />
</StatusBarItem>
</StatusBar>
</Grid>