Alex F
Alex F

Reputation: 43331

Accessing WPF control validation rules from code

XAML:

  <TextBox Name="textboxMin">
      <TextBox.Text>
          <Binding Path="Max">
              <Binding.ValidationRules>
                  <local:IntValidator/>
              </Binding.ValidationRules>
          </Binding>
      </TextBox.Text>
  </TextBox>

Code:

void buttonOK_Click(object sender, RoutedEventArgs e)
{
    // I need to know here whether textboxMin validation is OK
    // textboxMin. ???

    // I need to write something like:
    // if ( textboxMin.Validation.HasErrors )
    //     return;
}

It would be nice also to know, how to disable OK button, if at least one of dialog controls doesn't pass validation - in XAML, using binding. Having this way, I don't need to check validation state in the code.

Upvotes: 21

Views: 23769

Answers (3)

bjhuffine
bjhuffine

Reputation: 934

Much thanks to Fredrik Hedblad for his solution. It helped me as well. I also agree with Lukáš Koten that it would best be used as a behavior. That way there's not a mix of application logic mixed in the view layer and the view-model doesn't have to worry about dubplicating validation just to simply have it there. Here's my version via behavior:

As Fredrik Hedblad stated, first make sure any control validating has binding attribute NotifyOnValidationError="True".

Here's the view logic... much simpler...

xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"

and then just under the Window start tag

    Height="Auto" Width="Auto">
<i:Interaction.Behaviors>
    <behavior:ValidationErrorMappingBehavior HasValidationError="{Binding IsInvalid, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
</i:Interaction.Behaviors

Then for the button, just bind the command like normal. We'll use basic view-model binding principles to disable it using the RelayCommand.

<Button x:Name="OKButton" Content="OK" Padding="5,0" MinWidth="70" Height="23"
                HorizontalAlignment="Right" VerticalAlignment="Bottom" Margin="5,5,0,0"
                Command="{Binding OKCommand}"/>

And now the view model with its basic property and command

    private bool _isInvalid = false;
    public bool IsInvalid
    {
        get { return _isInvalid; }
        set { SetProperty<bool>(value, ref _isInvalid); }
    }

    private ICommand _okCommand;
    public ICommand OKCommand
    {
        get
        {
            if (_okCommand == null)
            {
                _okCommand = new RelayCommand(param => OnOK(), canparam => CanOK());
            }

            return _okCommand;
        }
    }

    private void OnOK()
    {
        //  this.IsInvalid = false, so we're good... let's just close
        OnCloseRequested();
    }

    private bool CanOK()
    {
        return !this.IsInvalid;
    }

And now, the behavior

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

namespace UI.Behavior
{
public class ValidationErrorMappingBehavior : Behavior<Window>
{
    #region Properties

    public static readonly DependencyProperty ValidationErrorsProperty = DependencyProperty.Register("ValidationErrors", typeof(ObservableCollection<ValidationError>), typeof(ValidationErrorMappingBehavior), new PropertyMetadata(new ObservableCollection<ValidationError>()));

    public ObservableCollection<ValidationError> ValidationErrors
    {
        get { return (ObservableCollection<ValidationError>)this.GetValue(ValidationErrorsProperty); }
        set { this.SetValue(ValidationErrorsProperty, value); }
    }

    public static readonly DependencyProperty HasValidationErrorProperty = DependencyProperty.Register("HasValidationError", typeof(bool), typeof(ValidationErrorMappingBehavior), new PropertyMetadata(false));

    public bool HasValidationError
    {
        get { return (bool)this.GetValue(HasValidationErrorProperty); }
        set { this.SetValue(HasValidationErrorProperty, value); }
    }

    #endregion

    #region Constructors

    public ValidationErrorMappingBehavior()
        : base()
    { }

    #endregion

    #region Events & Event Methods

    private void Validation_Error(object sender, ValidationErrorEventArgs e)
    {
        if (e.Action == ValidationErrorEventAction.Added)
        {
            this.ValidationErrors.Add(e.Error);
        }
        else
        {
            this.ValidationErrors.Remove(e.Error);
        }

        this.HasValidationError = this.ValidationErrors.Count > 0;
    }

    #endregion

    #region Support Methods

    protected override void OnAttached()
    {
        base.OnAttached();
        Validation.AddErrorHandler(this.AssociatedObject, Validation_Error);
    }

    protected override void OnDetaching()
    {
        base.OnDetaching();
        Validation.RemoveErrorHandler(this.AssociatedObject, Validation_Error);
    }

    #endregion
  }
}

Upvotes: 1

TalentTuner
TalentTuner

Reputation: 17556

you need to get the Binding first before you get the rules

    Binding b=  BindingOperations.GetBinding(textboxMin,TextBox.TextProperty);
    b.ValidationRules

else you can have BindingExpression and check for HasError property

 BindingExpression be1 = BindingOperations.GetBindingExpression (textboxMin,TextBox.TextProperty);

be1.HasError

Upvotes: 8

Fredrik Hedblad
Fredrik Hedblad

Reputation: 84684

Validation.HasError is an attached property so you can check it for textboxMin like this

void buttonOK_Click(object sender, RoutedEventArgs e)
{
    if (Validation.GetHasError(textboxMin) == true)
         return;
}

To run all ValidationRules for the TextProperty in code behind you can get the BindingExpression and call UpdateSource

BindingExpression be = textboxMin.GetBindingExpression(TextBox.TextProperty);
be.UpdateSource();

Update

It will take some steps to achieve the binding to disable the button if any validation occurs.

First, make sure all bindings add NotifyOnValidationError="True". Example

<TextBox Name="textboxMin">
    <TextBox.Text>
        <Binding Path="Max" NotifyOnValidationError="True">
            <Binding.ValidationRules>
                <local:IntValidator/>
            </Binding.ValidationRules>
        </Binding>
    </TextBox.Text>
</TextBox>

Then we hook up an EventHandler to the Validation.Error event in the Window.

<Window ...
        Validation.Error="Window_Error">

And in code behind we add and remove the validation errors in an observablecollection as they come and go

public ObservableCollection<ValidationError> ValidationErrors { get; private set; } 
private void Window_Error(object sender, ValidationErrorEventArgs e)
{
    if (e.Action == ValidationErrorEventAction.Added)
    {
        ValidationErrors.Add(e.Error);
    }
    else
    {
        ValidationErrors.Remove(e.Error);
    }
}

And then we can bind IsEnabled of the Button to ValidationErrors.Count like this

<Button ...>
    <Button.Style>
        <Style TargetType="Button">
            <Setter Property="IsEnabled" Value="False"/>
            <Style.Triggers>
                <DataTrigger Binding="{Binding ValidationErrors.Count}" Value="0">
                    <Setter Property="IsEnabled" Value="True"/>
                </DataTrigger>
            </Style.Triggers>
        </Style>
    </Button.Style>
</Button>

Upvotes: 30

Related Questions