0
votes

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>

1
This article might help you how-to-implement-binding-validation - neelesh bodgal
Have added the code to help illustrate the problem I am experiencing. - user13013304
I would like to implement all the ItemError logic in xaml instead of in code, that way I can use the TextBlock for all messages, not just error messages. I can turn the foreground Red for ValidateAll messages and switch back for any other messages. - user13013304

1 Answers

0
votes

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)
        {
            Boolean 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.