Property Validation

Mar 24, 2011 at 12:42 PM

Hi, everyone... 

I'd like to show how i made property validation. For example i need my property value to be greater than 0 otherwise it must couse validation with some message.

Firstly we need to create an attribute class that indicates wich validation rule must be applied for current property...

 [AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
    public class Validator : Attribute
    {
        public ValidationRule ValidatorInstance { get; private set; }

        public Validator(Type validatorType)
        {
            this.ValidatorInstance = (ValidationRule)Activator.CreateInstance(validatorType);
        }
    }

And a class that derivered from ValidationRule:

public class MyValidationRule: ValidationRule
    {
         public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
        {
            if (value == null)
                return new ValidationResult(false, "Value can't be null");
            try
            {
                var curValue = Convert.ToInt32(value);
                if (curValue > 0)
                    return new ValidationResult(true, string.Empty);
                else
                    return new ValidationResult(false, "Value must be greater than 0");
            }
            catch (Exception)
            {
                return new ValidationResult(false, "Incorrect value");
            }
        }
    }

Now we need to apply this validation rule to textbox. In my case textbox focus staies while user's input is incorrect. To do that i added a new class WPGTextBox that derivered form standart TextBox class:

public class WPGTextBox : TextBox
    {
        /*Delegate for invoking Focus() method*/
        public delegate void SimpleDelegate();

        public WPGTextBox()
        {
            System.Windows.Controls.Validation.AddErrorHandler(this, OnValidationError);
        }

        private static void OnValidationError(object o, ValidationErrorEventArgs e)
        {
            /*Get tell propertygrid that there's some error*/
            if (e.Action == ValidationErrorEventAction.Added)
            {
                var error = e.Error;
                var parent = VisualTreeHelper.GetParent(o as WPGTextBox);
                while (!(parent is PropertyGrid))
                {
                    parent = VisualTreeHelper.GetParent(parent);
                    if (parent == null)
                        break;
                }
                if (parent != null)
                    (parent as PropertyGrid).ShowPropertyInputError(e.Error.ErrorContent.ToString());
                (o as WPGTextBox).SetFocus();
            }
        }

        public void SetFocus()
        {
            this.Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.ApplicationIdle, new SimpleDelegate(DoFocus));
        }

        public void DoFocus()
        {
            this.Focus();
        }
       
        /*Validation rule for current property*/
        public ValidationRule Validation
        {
            get { return (ValidationRule)GetValue(ValidationProperty); }
            set { SetValue(ValidationProperty, value); }
        }

        // Using a DependencyProperty as the backing store for Validation.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty ValidationProperty =
            DependencyProperty.Register("Validation", typeof(ValidationRule), typeof(WPGTextBox),
            new FrameworkPropertyMetadata(null, OnValidationChanged, CoerceValidation));

        private static void OnValidationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            Binding binding = BindingOperations.GetBinding(d as WPGTextBox, WPGTextBox.TextProperty);
            System.Windows.Controls.Validation.RemoveErrorHandler(d as WPGTextBox, OnValidationError);
            binding.ValidationRules.Clear();
            if (e.NewValue != null)
            {
                binding.ValidationRules.Add((ValidationRule)e.NewValue);
                System.Windows.Controls.Validation.AddErrorHandler(d as WPGTextBox, OnValidationError);
            }
        }

        private static object CoerceValidation(DependencyObject d, object value)
        {
            return value;
        }
               
    }
Also a property object need to know what validation rule it's uses (Property.cs):
public class Property : Item, IDisposable
    {
        #region Fields

        ...

        protected ValidationRule _validator = null;

        #endregion

        #region Initialization

        public Property(object instance, PropertyDescriptor property)
        {
            .....
/*Getting validationrule attribute and validationrule object*/
            var validationAttribute = this._property.Attributes.OfType<Validator>().SingleOrDefault();
            if (validationAttribute != null)
                this._validator = validationAttribute.ValidatorInstance;
        }
        #endregion

        #region Properties

       ....

        public ValidationRule Validator
        {
            get
            {
                if (this._validator == null)
                    return null;
                else
                {
                    this._validator.Validate(this._property.GetValue(this._instance), new System.Globalization.CultureInfo("en-US"));
                    return this._validator;
                }
            }
        }

        ....
    }
And "all magic" happens in WPGTemplates.xaml. For example for property with "default" type: 
....

<DataTemplate x:Key="{ComponentResourceKey TypeInTargetAssembly={x:Type local:PropertyGrid}, ResourceId=default}">
        <local:WPGTextBox Validation="{Binding Path=Validator}" Validation.ErrorTemplate="{StaticResource TextBoxErrorTemplate}" IsReadOnly="{Binding Path=IsReadOnly}" Style="{DynamicResource {ComponentResourceKey TypeInTargetAssembly={x:Type local:PropertyGrid}, ResourceId=TextBoxStyle}}">
                <TextBox.Text>
                    <Binding Mode="TwoWay" Path="Value" NotifyOnValidationError="True">
				</Binding>
			</TextBox.Text>
        </local:WPGTextBox>
    </DataTemplate>

...
To show validation message just add "Validation.HasError" trigger in textbox style:
	<Style x:Key="{ComponentResourceKey TypeInTargetAssembly={x:Type local:PropertyGrid}, ResourceId=TextBoxStyle}" TargetType="{x:Type TextBox}">
		<Setter Property="KeyboardNavigation.TabNavigation" Value="None" />
        <Setter Property="FocusVisualStyle" Value="{x:Null}" />
        <Setter Property="AllowDrop" Value="true" />
        <Setter Property="Foreground" Value="{DynamicResource TextBrush}" />
        <Setter Property="Background" Value="{DynamicResource ControlBackgroundBrush}"/>
        <Setter Property="BorderBrush" Value="#FF000000"/>
		<Setter Property="Template">
			<Setter.Value>
				<ControlTemplate TargetType="{x:Type TextBox}">
        <ControlTemplate.Resources>
            <Storyboard x:Key="HoverOn">
                <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="HoverBorder" Storyboard.TargetProperty="(UIElement.Opacity)">
                    <SplineDoubleKeyFrame KeyTime="00:00:00.1000000" Value="0.5" />
                </DoubleAnimationUsingKeyFrames>
            </Storyboard>
            <Storyboard x:Key="HoverOff">
                <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="HoverBorder" Storyboard.TargetProperty="(UIElement.Opacity)">
                    <SplineDoubleKeyFrame KeyTime="00:00:00.4000000" Value="0" />
                </DoubleAnimationUsingKeyFrames>
            </Storyboard>
            <Storyboard x:Key="FocusedOn">
                <DoubleAnimationUsingKeyFrames Storyboard.TargetName="FocusVisualElement" Storyboard.TargetProperty="Opacity">
                    <SplineDoubleKeyFrame KeyTime="00:00:00.1000000" Value="1" />
                </DoubleAnimationUsingKeyFrames>
            </Storyboard>
            <Storyboard x:Key="FocusedOff">
                <DoubleAnimationUsingKeyFrames Storyboard.TargetName="FocusVisualElement" Storyboard.TargetProperty="Opacity">
                    <SplineDoubleKeyFrame KeyTime="00:00:00.4000000" Value="0" />
                </DoubleAnimationUsingKeyFrames>
            </Storyboard>
        </ControlTemplate.Resources>
        <Grid>
            <Border x:Name="Border" Opacity="1" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="2,2,2,2" Background="{TemplateBinding Background}">
                <Grid>
                    <Border BorderThickness="1">
                        <ScrollViewer Margin="0" x:Name="PART_ContentHost" Style="{DynamicResource NuclearScrollViewer}" />
                    </Border>
                </Grid>
            </Border>
            <Border x:Name="HoverBorder" IsHitTestVisible="False" Opacity="0" BorderBrush="{StaticResource GlyphBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="2,2,2,2" />
            <Border x:Name="FocusVisualElement" IsHitTestVisible="False" Opacity="0" BorderBrush="{StaticResource HoverShineBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="2,2,2,2" />
        </Grid>
        <ControlTemplate.Triggers>
                <Trigger Property="Validation.HasError" Value="true">
                    <Setter Property="ToolTip"
                            Value="{Binding RelativeSource={x:Static RelativeSource.Self}, Path=(Validation.Errors)[0].ErrorContent}"/>
                            <Setter Property="BorderBrush" TargetName="Border" Value="{DynamicResource ErrorBrush}" />
                </Trigger>
                <Trigger Property="TextBox.IsFocused" Value="True">
                <Trigger.ExitActions>
                    <BeginStoryboard Storyboard="{StaticResource FocusedOff}" x:Name="FocusedOff_BeginStoryboard" />
                </Trigger.ExitActions>
                <Trigger.EnterActions>
                    <BeginStoryboard Storyboard="{StaticResource FocusedOn}" x:Name="FocusedOn_BeginStoryboard" />
                </Trigger.EnterActions>

            </Trigger>
            <MultiTrigger>
                <MultiTrigger.ExitActions>
                    <BeginStoryboard Storyboard="{StaticResource HoverOff}" x:Name="HoverOff_BeginStoryboard" />
                </MultiTrigger.ExitActions>
                <MultiTrigger.EnterActions>
                    <BeginStoryboard Storyboard="{StaticResource HoverOn}" />
                </MultiTrigger.EnterActions>
                <MultiTrigger.Conditions>
                    <Condition Property="IsMouseOver" Value="True" />
                    <Condition Property="IsFocused" Value="False" />
                </MultiTrigger.Conditions>

            </MultiTrigger>
            <Trigger Property="IsEnabled" Value="False">
                <Setter Property="Background" TargetName="Border" Value="{DynamicResource DisabledBackgroundBrush}" />
                <Setter Property="BorderBrush" TargetName="Border" Value="{DynamicResource DisabledBorderBrush}" />
                <Setter Property="Foreground" Value="{DynamicResource DisabledForegroundBrush}" />
            </Trigger>
        </ControlTemplate.Triggers>
    </ControlTemplate>
			</Setter.Value>
		</Setter>
	</Style>

That's all... After that i can add any validation to any property... 

Mar 31, 2011 at 6:53 AM

And if you updating value with enter (http://wpg.codeplex.com/discussions/248442) in "private void PropertyGrid_PreviewKeyDown(object sender, KeyEventArgs e)" method some changes must be done:

private void PropertyGrid_PreviewKeyDown(object sender, KeyEventArgs e)
        {
            if (e.Key == Key.Enter && e.OriginalSource is TextBox)
            {
                if (this.SelectedProperty != null)
                {
                    var value = ((TextBox)e.OriginalSource).Text;
                    if (this.SelectedProperty.Validator != null)
                    {
                        var result = this.SelectedProperty.Validator.Validate(value, new System.Globalization.CultureInfo("en-US"));
                        if (result.IsValid)
                        {
                            this.SelectedProperty.Value = value;
                        }
                        ((TextBox)e.OriginalSource).GetBindingExpression(TextBox.TextProperty).UpdateSource();
                    }
                }
            }
        }

Apr 18, 2011 at 11:59 AM

Thanx for your great example! Its been really helpful as I'm adding validation rules myself.

 

Unfortunately I get a compiler error in the method:

		private static void OnValidationError( object o, ValidationErrorEventArgs e )
		{
			/*Get tell propertygrid that there's some error*/
			if ( e.Action == ValidationErrorEventAction.Added )
			{
				var error = e.Error;
				var parent = VisualTreeHelper.GetParent( o as WPGTextBox );
				while ( !( parent is PropertyGrid ) )
				{
					parent = VisualTreeHelper.GetParent( parent );
					if ( parent == null )
						break;
				}
				if ( parent != null )
					( parent as PropertyGrid ).ShowPropertyInputError( e.Error.ErrorContent.ToString() );
				( o as WPGTextBox ).SetFocus();
			}
		}

The red line does not compile. The reason is that the method

ShowPropertyInputError



is not defined. Did you perhaps miss out a bit of code?

Apr 18, 2011 at 2:20 PM

I also cannot find TextBoxErrorTemplate