Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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>
like image 704
Choub890 Avatar asked Apr 03 '14 03:04

Choub890


1 Answers

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

like image 84
Anatoliy Nikolaev Avatar answered Sep 22 '22 23:09

Anatoliy Nikolaev