'WPF BindingGroup Validation
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>
Solution 1:[1]
OK, So with a lot of help from other questions & responses on stackoverflow, I figured it out:
XAML Changes: I added a Style for my StatusBar:TextBlock, It implements a DataTrigger on the StackPanel binding that has the BindingGroup. The Trigger takes the current Validation RuleInError which is a ValidationRule and converts it to a String via an IValueConverter.
TextBlock Style:
<Style x:Key="TextBlockStyle" TargetType="TextBlock">
<Setter Property="Foreground" Value="#FF000000"/>
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=TextBoxStack, Path=(Validation.Errors)[0].RuleInError,
Converter={StaticResource RuleConverterClass}}" Value="True" >
<Setter Property="Foreground" Value="Red" />
</DataTrigger>
</Style.Triggers>
</Style>
For this solution, Validation.Error is not raised by the StackPanel, The handler:ItemError is not used. The StatusBar TextBlock has been updated to use the new Style:
<StatusBar Margin="4,0,0,1" VerticalAlignment="Bottom" VerticalContentAlignment="Bottom" Padding="0,3" >
<StatusBarItem>
<TextBlock Name="StatusTextBlock" Style="{StaticResource TextBlockStyle}" />
</StatusBarItem>
</StatusBar>
Code Changes: Updated the Button Click Event to call the BindingGroup.UpdateSources() function to complete the validation, (ValidateAll validation rule):
private void ButtonClick(object sender, RoutedEventArgs e)
{
//this.TextBoxStack.BindingGroup.UpdateSources();
if (!this.TextBoxStack.BindingGroup.UpdateSources())
StatusTextBlock.Text = (string)this.TextBoxStack.BindingGroup.ValidationErrors[0].ErrorContent;
else
StatusTextBlock.Text = "Calculation Successful";
}
Added the Conversion Class to convert the Validation Rule object into a Text String:
[ValueConversion(typeof(ValidationRule), typeof(Boolean))]
public class ValidationRuleConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
bool returnValue = false;
ValidationRule rule = (ValidationRule)value;
string name = rule.ToString();
if (name == "WpfGroupValidationDemo4.ValidateAll")
returnValue = true;
return returnValue;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return value;
}
}
That basically is what I was trying to do. This will give me window level validation and allow me to display the validation message on my StatusBar. Using this technique, I can display all my window level messages and change text color to reflect the severity of the message and at the same time, keeping in the spirit of MVVM.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | Henry Malinowski |
