Choub890
Choub890

Reputation: 1163

Binding Validation.HasError property in MVVM

I am currently implementing a ValidationRule to check if some invalid character are in a TextBox. I am happy that setting the class I have implemented that inherits ValidationRule on my TextBox sets it in red when such characters are found, but I would also like to use the Validation.HasError property or the Validation.Errors property to pop a messagebox telling the user that there are errors in the various textboxes in the page.

Is there a way to bind a property in my ViewModel to the Validation.HasError and/or to the Validation.Errors properties in order for me to have access to them in my ViewModel?

Here is my error style for the TextBox:

<Style x:Key="ErrorValidationTextBox" TargetType="{x:Type pres:OneTextBox}">
    <Setter Property="Validation.ErrorTemplate">
        <Setter.Value>
            <ControlTemplate>
                <DockPanel LastChildFill="True">
                    <TextBlock DockPanel.Dock="Right"
                    Foreground="Red"
                    FontSize="12pt"
                    Text="{Binding ElementName=MyAdorner, 
                           Path=AdornedElement.(Validation.Errors)[0].ErrorContent}">
                    </TextBlock>
                    <AdornedElementPlaceholder x:Name="MyAdorner"/>
                </DockPanel>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

Here is how I declare my TextBox (OneTextBox encapsulates the regular WPF TextBox) in my XAML:

<pres:OneTextBox Watermark="Name..." Margin="85,12,0,0" Style="{StaticResource ErrorValidationTextBox}"
                 AcceptsReturn="False" MaxLines="1" Height="22" VerticalAlignment="Top"
                 HorizontalAlignment="Left" Width="300" >
    <pres:OneTextBox.Text>
        <Binding Path="InterfaceSpecification.Name" UpdateSourceTrigger="PropertyChanged">                    
            <Binding.ValidationRules>                       
                <interfaceSpecsModule:NoInvalidCharsRule/>                        
            </Binding.ValidationRules>                    
        </Binding>               
    </pres:OneTextBox.Text>        
</pres:OneTextBox>

Upvotes: 8

Views: 34198

Answers (3)

Anatoliy Nikolaev
Anatoliy Nikolaev

Reputation: 22702

The Validation.HasError is readonly property, therefore Binding will not work with this property. This can be seen in ILSpy:

public virtual bool HasError
{
    get
    {
        return this._validationError != null;
    }
}

As an alternative, you should see a great article which provides a solution in the form of use attached dependency properties, there you will see a detailed explanation of the example.

Below is a full example from this article, I just translated it under C#, the original language is VB.NET:

XAML

<Window x:Class="HasErrorTestValidation.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:HasErrorTestValidation"
        WindowStartupLocation="CenterScreen"
        Title="MainWindow" Height="350" Width="525">
    
    <Window.DataContext>
        <local:TestData />
    </Window.DataContext>

    <StackPanel>
        <TextBox x:Name="TestTextBox" 
                 local:ProtocolSettingsLayout.MVVMHasError="{Binding Path=HasError}">
            <TextBox.Text>
                <Binding Path="TestText" UpdateSourceTrigger="PropertyChanged">
                    <Binding.ValidationRules>
                        <local:OnlyNumbersValidationRule ValidatesOnTargetUpdated="True"/>
                    </Binding.ValidationRules>
                </Binding>
            </TextBox.Text>
        </TextBox>
    
        <TextBlock>
            <TextBlock.Text>
                <Binding Path="HasError" StringFormat="HasError is {0}"/>
            </TextBlock.Text>
        </TextBlock>
    
        <TextBlock>
            <TextBlock.Text>
                <Binding Path="(Validation.HasError)" ElementName="TestTextBox" StringFormat="Validation.HasError is {0}"/>
            </TextBlock.Text>
        </TextBlock>        
    </StackPanel>
</Window>

Code-behind

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }
}

#region Model

public class TestData : INotifyPropertyChanged
{
    private bool _hasError = false;

    public bool HasError
    {
        get
        {
            return _hasError;
        }

        set
        {
            _hasError = value;
            NotifyPropertyChanged("HasError");
        }
    }

    private string _testText = "0";

    public string TestText
    {
        get
        {
            return _testText;
        }

        set
        {
            _testText = value;
            NotifyPropertyChanged("TestText");
        }
    }

    #region PropertyChanged

    public event PropertyChangedEventHandler PropertyChanged;

    protected void NotifyPropertyChanged(string sProp)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(sProp));
        }
    }

    #endregion
}

#endregion

#region ValidationRule

public class OnlyNumbersValidationRule : ValidationRule
{
    public override ValidationResult Validate(object value, CultureInfo cultureInfo)
    {
        var result = new ValidationResult(true, null);

        string NumberPattern = @"^[0-9-]+$";
        Regex rgx = new Regex(NumberPattern);

        if (rgx.IsMatch(value.ToString()) == false)
        {
            result = new ValidationResult(false, "Must be only numbers");
        }

        return result;
    }
}

#endregion

public class ProtocolSettingsLayout
{       
    public static readonly DependencyProperty MVVMHasErrorProperty= DependencyProperty.RegisterAttached("MVVMHasError", 
                                                                    typeof(bool),
                                                                    typeof(ProtocolSettingsLayout),
                                                                    new FrameworkPropertyMetadata(false, 
                                                                                                  FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                                                                                                  null,
                                                                                                  CoerceMVVMHasError));

    public static bool GetMVVMHasError(DependencyObject d)
    {
        return (bool)d.GetValue(MVVMHasErrorProperty);
    }

    public static void SetMVVMHasError(DependencyObject d, bool value)
    {
        d.SetValue(MVVMHasErrorProperty, value);
    }

    private static object CoerceMVVMHasError(DependencyObject d,Object baseValue)
    {
        bool ret = (bool)baseValue;

        if (BindingOperations.IsDataBound(d,MVVMHasErrorProperty))
        {
            if (GetHasErrorDescriptor(d)==null)
            {
                DependencyPropertyDescriptor desc = DependencyPropertyDescriptor.FromProperty(Validation.HasErrorProperty, d.GetType());
                desc.AddValueChanged(d,OnHasErrorChanged);
                SetHasErrorDescriptor(d, desc);
                ret = System.Windows.Controls.Validation.GetHasError(d);
            }
        }
        else
        {
            if (GetHasErrorDescriptor(d)!=null)
            {
                DependencyPropertyDescriptor desc= GetHasErrorDescriptor(d);
                desc.RemoveValueChanged(d, OnHasErrorChanged);
                SetHasErrorDescriptor(d, null);
            }
        }

        return ret;
    }

    private static readonly DependencyProperty HasErrorDescriptorProperty = DependencyProperty.RegisterAttached("HasErrorDescriptor", 
                                                                            typeof(DependencyPropertyDescriptor),
                                                                            typeof(ProtocolSettingsLayout));

    private static DependencyPropertyDescriptor GetHasErrorDescriptor(DependencyObject d)
    {
        var ret = d.GetValue(HasErrorDescriptorProperty);
        return ret as DependencyPropertyDescriptor;
    }

    private static void OnHasErrorChanged(object sender, EventArgs e)
    {
        DependencyObject d = sender as DependencyObject;

        if (d != null)
        {
            d.SetValue(MVVMHasErrorProperty, d.GetValue(Validation.HasErrorProperty));
        }
    }

   private static void SetHasErrorDescriptor(DependencyObject d, DependencyPropertyDescriptor value)
   {
        var ret = d.GetValue(HasErrorDescriptorProperty);
        d.SetValue(HasErrorDescriptorProperty, value);
    }
}

As an alternative to the use of ValidationRule, in MVVM style you can try to implement IDataErrorInfo Interface. For more info see this:

Enforcing Complex Business Data Rules with WPF

Upvotes: 9

user1005462
user1005462

Reputation: 332

all perfect work set NotifyOnValidationError="True" on binding; (or maybe with binding group also possible)

then use

<Button IsEnabled="{Binding ElementName=tbPeriod, Path=(Validation.HasError)}"

sample with one textBox:

<val:RangeRulecan be changed to ms sample agerangerule etc

<TextBox MaxLength="5" x:Name="tbPeriod" HorizontalAlignment="Left" VerticalAlignment="Top" Width="162" Margin="10,10,0,0" Style="{StaticResource TextBoxInError}">
            <TextBox.Text>
                <Binding Path="ReportPeriod" UpdateSourceTrigger="PropertyChanged" NotifyOnValidationError="True">
                    <Binding.ValidationRules>
                        <val:RangeRule Min="70" Max="5000" />
                    </Binding.ValidationRules>
                </Binding>
            </TextBox.Text>
        </TextBox>

Upvotes: 2

Brian Stewart
Brian Stewart

Reputation: 9277

In response to Anatoliy's request for an example of a non-working project:

Generic.xaml

<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:TestAttachedPropertyValidationError">


<Style TargetType="{x:Type local:TextBoxCustomControl}">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type local:TextBoxCustomControl}">
                <Border Background="{TemplateBinding Background}"
                        BorderBrush="{TemplateBinding BorderBrush}"
                        BorderThickness="{TemplateBinding BorderThickness}">
                    <Grid>
                        <Grid.RowDefinitions>
                            <RowDefinition Height="Auto"/>
                            <RowDefinition Height="Auto"/>
                            <RowDefinition Height="Auto"/>
                        </Grid.RowDefinitions>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="Auto"/>
                            <ColumnDefinition Width="10"/>
                            <ColumnDefinition Width="50"/>
                        </Grid.ColumnDefinitions>
                        <Grid.Resources>
                            <BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
                        </Grid.Resources>
                        <Label 
                            Grid.Row ="0" 
                            Grid.Column="0" 
                            Content="Enter a numeric value:" />
                        <TextBox 
                            Grid.Row ="0" 
                            Grid.Column="2" 
                            local:HasErrorUtility.HasError="{Binding NumericPropHasError, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}"
                            Text="{Binding NumericProp, Mode=TwoWay, UpdateSourceTrigger=LostFocus, RelativeSource={RelativeSource TemplatedParent}}" />
                        <Label 
                            Grid.Row ="1" 
                            Grid.Column="0" 
                            Content="Value entered:" />
                        <Label 
                            Grid.Row ="1" 
                            Grid.Column="2" 
                            Content="{TemplateBinding NumericProp}" />
                        <Label 
                            Grid.Row ="2" 
                            Grid.Column="0" 
                            Grid.ColumnSpan="3" 
                            Visibility="{TemplateBinding NumericPropHasError, Converter={StaticResource BooleanToVisibilityConverter}}"
                            Foreground="Red" 
                            Content="Not a numeric value" />
                    </Grid>
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

TextBoxCustomControl.cs

using System.Windows;
using System.Windows.Controls;

namespace TestAttachedPropertyValidationError
{
    public class TextBoxCustomControl : Control
    {
        static TextBoxCustomControl()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(TextBoxCustomControl), new FrameworkPropertyMetadata(typeof(TextBoxCustomControl)));
        }

        public static readonly DependencyProperty NumericPropProperty =
            DependencyProperty.Register("NumericProp", typeof (int), typeof (TextBoxCustomControl), new PropertyMetadata(default(int)));

        public int NumericProp
        {
            get { return (int) GetValue(NumericPropProperty); }
            set { SetValue(NumericPropProperty, value); }
        }

        public static readonly DependencyProperty NumericPropHasErrorProperty =
            DependencyProperty.Register("NumericPropHasError", typeof (bool), typeof (TextBoxCustomControl), new PropertyMetadata(default(bool)));

        public bool NumericPropHasError
        {
            get { return (bool) GetValue(NumericPropHasErrorProperty); }
            set { SetValue(NumericPropHasErrorProperty, value); }
        }
    }
}

HasErrorUtility.cs

using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;

namespace TestAttachedPropertyValidationError
{
    class HasErrorUtility
    {
        public static readonly DependencyProperty HasErrorProperty = DependencyProperty.RegisterAttached("HasError",
                                                                        typeof(bool),
                                                                        typeof(HasErrorUtility),
                                                                        new FrameworkPropertyMetadata(false,
                                                                                                      FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                                                                                                      null,
                                                                                                      CoerceHasError));

        public static bool GetHasError(DependencyObject d)
        {
            return (bool)d.GetValue(HasErrorProperty);
        }

        public static void SetHasError(DependencyObject d, bool value)
        {
            d.SetValue(HasErrorProperty, value);
        }

        private static object CoerceHasError(DependencyObject d, Object baseValue)
        {
            var ret = (bool)baseValue;
            if (BindingOperations.IsDataBound(d, HasErrorProperty))
            {
                if (GetHasErrorDescriptor(d) == null)
                {
                    var desc = DependencyPropertyDescriptor.FromProperty(Validation.HasErrorProperty, d.GetType());
                    desc.AddValueChanged(d, OnHasErrorChanged);
                    SetHasErrorDescriptor(d, desc);
                    ret = Validation.GetHasError(d);
                }
            }
            else
            {
                if (GetHasErrorDescriptor(d) != null)
                {
                    var desc = GetHasErrorDescriptor(d);
                    desc.RemoveValueChanged(d, OnHasErrorChanged);
                    SetHasErrorDescriptor(d, null);
                }
            }

            return ret;
        }

        private static readonly DependencyProperty HasErrorDescriptorProperty = DependencyProperty.RegisterAttached("HasErrorDescriptor",
                                                                                typeof(DependencyPropertyDescriptor),
                                                                                typeof(HasErrorUtility));

        private static DependencyPropertyDescriptor GetHasErrorDescriptor(DependencyObject d)
        {
            var ret = d.GetValue(HasErrorDescriptorProperty);
            return ret as DependencyPropertyDescriptor;
        }

        private static void SetHasErrorDescriptor(DependencyObject d, DependencyPropertyDescriptor value)
        {
            d.SetValue(HasErrorDescriptorProperty, value);
        }

        private static void OnHasErrorChanged(object sender, EventArgs e)
        {
            var d = sender as DependencyObject;

            if (d != null)
            {
                d.SetValue(HasErrorProperty, d.GetValue(Validation.HasErrorProperty));
            }
        }

    }
}

ViewModel.cs

using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace TestAttachedPropertyValidationError
{
    public class ViewModel :INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        private int _vmNumericProp;
        private bool _vmNumericPropHasError;

        public int VmNumericProp
        {
            get { return _vmNumericProp; }
            set
            {
                _vmNumericProp = value;
                OnPropertyChanged();
            }
        }

        public bool VmNumericPropHasError
        {
            get { return _vmNumericPropHasError; }
            set
            {
                _vmNumericPropHasError = value;
                OnPropertyChanged();
            }
        }

        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            var handler = PropertyChanged;
            if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

MainWindow.xaml

<Window x:Class="TestAttachedPropertyValidationError.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:TestAttachedPropertyValidationError"
    Title="MainWindow" Height="350" Width="525">
<StackPanel Margin="10">
    <StackPanel.Resources>
        <local:ViewModel x:Key="VM1"/>
        <local:ViewModel x:Key="VM2"/>
    </StackPanel.Resources>
    <Label Content="Custom Control...}"></Label>
    <local:TextBoxCustomControl 
        Margin="10" 
        DataContext="{StaticResource VM1}"
        NumericProp="{Binding VmNumericProp}"
        NumericPropHasError="{Binding VmNumericPropHasError}"/>
    <Label Content="Regular XAML...}" Margin="0,20,0,0"/>
    <Grid 
        Margin="10"
        DataContext="{StaticResource VM2}"
        >
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="10"/>
            <ColumnDefinition Width="50"/>
        </Grid.ColumnDefinitions>
        <Grid.Resources>
            <BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
        </Grid.Resources>
        <Label 
                            Grid.Row ="0" 
                            Grid.Column="0" 
                            Content="Enter a numeric value:" />
        <TextBox 
                            Grid.Row ="0" 
                            Grid.Column="2" 
                            local:HasErrorUtility.HasError="{Binding VmNumericPropHasError, Mode=TwoWay}"
                            Text="{Binding VmNumericProp, Mode=TwoWay, UpdateSourceTrigger=LostFocus}" />
        <Label 
                            Grid.Row ="1" 
                            Grid.Column="0" 
                            Content="Value entered:" />
        <Label 
                            Grid.Row ="1" 
                            Grid.Column="2" 
                            Content="{Binding VmNumericProp}" />
        <Label 
                            Grid.Row ="2" 
                            Grid.Column="0" 
                            Grid.ColumnSpan="3" 
                            Visibility="{Binding VmNumericPropHasError, Converter={StaticResource BooleanToVisibilityConverter}}"
                            Foreground="Red" 
                            Content="Not a numeric value" />
    </Grid>

</StackPanel>

Upvotes: -1

Related Questions