tags:

views:

754

answers:

1

I have a WPF window that uses validation. I created an error template that puts a red border around an element that fails validation and displays the error message below. This works fine, but the error message is rendered on top of any controls beneath the control with the error. The best I can tell, this happens because the error template renders on the Adorner Layer, which is on top of everything else. What I'd like to have happen is for everything else to move down to make room for the error message. Is there a way to do this? All of the examples on the web seem to use a tool tip and use a simple indicator like an asterisk or exclamation point that doesn't use much room.

Here is the template:

<ControlTemplate x:Key="ValidationErrorTemplate">
    <StackPanel>
        <Border BorderBrush="Red" BorderThickness="2" CornerRadius="2">
            <AdornedElementPlaceholder x:Name="placeholder"/>
        </Border>
        <TextBlock Foreground="Red" FontSize="10" Text="{Binding ElementName=placeholder, Path=AdornedElement.(Validation.Errors)[0].ErrorContent, FallbackValue=Error!}"></TextBlock>
    </StackPanel>
</ControlTemplate>

Here are the controls using the template (I typed some of this out, so ignore any syntax errors):

<Grid>
  <Grid.RowDefinitions>
    <RowDefinition Height="Auto"/>
    <RowDefinition Height="Auto"/>
  </Grid.RowDefinitions>
  <TextBox Name="Account" Grid.Row="0" Validation.ErrorTemplate="{StaticResource ValidationErrorTemplate}" Width="200">
    <TextBox.Text>
      <Binding Path="AccountNumber">
        <Binding.ValidationRules>
          <validators:RequiredValueValidationRule/>
          <validators:NumericValidationRule/>
        </Binding.ValidationRules>
      </Binding>
    </TextBox.Text>
  </TextBox>
  <TextBox Name="Expiration" Grid.Row="1" Validation.ErrorTemplate="{StaticResource ValidationErrorTemplate}" Width="100"  Margin="0,2,5,2">
    <TextBox.Text>
      <Binding Path="ExpirationDate">
        <Binding.ValidationRules>
          <validators:ExpirationDateValidationRule/>
        </Binding.ValidationRules>
      </Binding>
    </TextBox.Text>
  </TextBox>
</Grid>
+2  A: 

EDIT: Alright, I'm not positive that this is the best solution (I sure hope someone can provide a better one), but here it goes:

Instead of using the Validation.ErrorTeplate, which will present all of the visuals in the AdornerLayer, you can add some TextBlocks and bind them to Validation.HasError and (Validation.Errors)[0].ErrorContent, using a customer IValueConverter to convert the Validation.HasError bool to a Visibility value. It would look something like the following:

Window1.cs:

<Window x:Class="WpfApplicationTest.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:sys="clr-namespace:System;assembly=mscorlib"
    xmlns:local="clr-namespace:WpfApplicationTest"
    Title="Window1" Height="300" Width="300">
    <Grid Margin="10">
        <Grid.Resources>
            <!-- The person we are binding to -->
            <local:Person x:Key="charles" Name="Charles" Age="20" />
            <!-- The convert to use-->
            <local:HasErrorToVisibilityConverter x:Key="visibilityConverter" />
        </Grid.Resources>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <!-- The name -->
        <TextBox Name="NameTextBox" Grid.Row="0" Text="{Binding Source={StaticResource charles}, Path=Name, ValidatesOnDataErrors=true}" />
        <TextBlock Grid.Row="1" 
                   Foreground="Red" 
                   Text="{Binding ElementName=NameTextBox, Path=(Validation.Errors)[0].ErrorContent}" 
                   Visibility="{Binding ElementName=NameTextBox, Path=(Validation.HasError), Converter={StaticResource visibilityConverter}}" />

        <!-- The age -->
        <TextBox Name="AgeTextBox" Grid.Row="2" Text="{Binding Source={StaticResource charles}, Path=Age, ValidatesOnExceptions=true}" />
        <TextBlock Grid.Row="3" 
                   Foreground="Red" 
                   Text="{Binding ElementName=AgeTextBox, Path=(Validation.Errors)[0].ErrorContent}" 
                   Visibility="{Binding ElementName=AgeTextBox, Path=(Validation.HasError), Converter={StaticResource visibilityConverter}}" />
    </Grid>
</Window>

Person.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.Text.RegularExpressions;

namespace WpfApplicationTest
{
    public class Person : IDataErrorInfo
    {
        public string Name { get; set; }
        public int Age { get; set; }

        #region IDataErrorInfo Members

        string IDataErrorInfo.Error
        {
            get { throw new NotImplementedException(); }
        }

        string IDataErrorInfo.this[string columnName]
        {
            get
            {
                switch (columnName)
                {
                    case ("Name"):
                        if (Regex.IsMatch(this.Name, "[^a-zA-Z ]"))
                        {
                            return "Name may contain only letters and spaces.";
                        }
                        else
                        {
                            return null;
                        }
                    default:
                        return null;
                }
            }
        }

        #endregion
    }
}

HasErrorToVisibilityConverter.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Data;
using System.Windows;

namespace WpfApplicationTest
{
    [ValueConversion(typeof(bool), typeof(Visibility))]
    public class HasErrorToVisibilityConverter : IValueConverter
    {
        #region IValueConverter Members

        object IValueConverter.Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            bool hasError = (bool)value;
            return hasError ? Visibility.Visible : Visibility.Collapsed;
        }

        object IValueConverter.ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            throw new NotImplementedException();
        }

        #endregion
    }
}

It doesn't scale as well as having a single ControlTemplate that you can reference in all of your controls, but it's the only solution I've found. I feel your pain - just about every example I can find on the topic of WPF validation is very simple, and almost always uses '!' or '*' preceding the control, with a tooltip bound to (Validation.Errors)[0].ErrorContent...

Best of luck to ya! If I find a better solution, I'll update this ;)

Charles
Very cool! I wish I could give you multiple up votes. ;)
Eddie Deyo
Thanks, I hope that helped :) ! If you find a better way, let me know. I tried creating a solution using attached Dependency Properties, but I can't remember what ever came of that...
Charles