How to suppress validation when nothing is entered
Asked Answered
C

8

38

I use WPF data binding with entities that implement IDataErrorInfo interface. In general my code looks like this:

Business entity:

public class Person : IDataErrorInfo 
{
  public string Name { get; set;}

  string IDataErrorInfo.this[string columnName]
  {
    if (columnName=="Name" && string.IsNullOrEmpty(Name))
      return "Name is not entered";
    return string.Empty;
  }  
}

Xaml file:

<TextBox Text="{Binding Path=Name, Mode=TwoWay, ValidatesOnDataErrors=true}" />

When user clicks on "Create new person" following code is executed:

DataContext = new Person();

The problem is that when person is just created its name is empty and WPF immediately draws red frame and shows error message. I want it to show error only when name was already edited and focus is lost. Does anybody know the way to do this?

Camilacamile answered 1/10, 2009 at 6:42 Comment(3)
I'm putting a bounty on this question in hope for a non-hacky solution, if one exists.Zealotry
Can't you just create the Person before InitializeComponent() is called?Floriculture
Added bounty to get a good non-hacky solution..Wozniak
A
16

You can change your person class to fire validation error only if Name property was ever changed:

public class Person : IDataErrorInfo {

    private bool nameChanged = false;
    private string name;
    public string Name {
        get { return name; }
        set { 
            name = value;
            nameChanged = true;
        }
    }

//... skipped some code

    string IDataErrorInfo.this[string columnName] {
        get {
            if(nameChanged && columnName == "Name" && string.IsNullOrEmpty(Name)) 
                return "Name is not entered"; 
            return string.Empty;
        }
    }
}
Antimissile answered 1/10, 2009 at 10:2 Comment(4)
Yes, I can, but I'd like to tune up WPF binding rather then change my business entities. Since I am not WPF expert, a hope there is relatively easy solution of such problem. It seems to be typical behavior - not to show alerts for all field when form is just open.Camilacamile
I don't think this can be controlled at XAML or Binding settings level. The data is either correct or it isn't, so it's up to IDataErrorInfo to validate it. Alternatively you can check "if(columnName == "Name" && Name == "")" thus treating initial "null" as valid which will turn into invalid empty string upon editing. Can't think of any other way.Antimissile
@AlexKofman I know it is an old post but this is something I am tackling right now. Your point about changing the business entities is irrelevant when you have already had to implement the IDataErrorInfo interface. You are better of putting your object behind a ViewModel as Uri suggested and putting your presentation logic in there.Reflexive
I don't like this, if you have 50, 100 properties this begins to be clunky and way to much code than should be needed. BTW WPF Validation is HORRIBLE, any way that is approached. There is no clean way to handle it.Acherman
C
6

There is another solution which i found but i don't like it a lot. You have to clear validation on page load.

What i mean is you have to do this :

Validation.ClearInvalid(...) for instance if you have a textbox you dont want to be validated should call

Validation.ClearInvalid(txtSomething.GetBindingExpression(TextBox.TextProperty)) or something like that.

You should do this for every control you want to be cleared of validation.

I didn't like the solution but that was the best i found. I hoped wpf had something "out of the box" that worked but didn't find it.

Catalepsy answered 15/9, 2010 at 10:22 Comment(0)
T
4

I think @Stanislav Kniazev approach is the correct one. Your comment about not adding logic to the business object is also valid. To have a clean separation of concern, how about keeping the Person in the business layer (or the data model layer), and introduce a new class PersonVm with view logic. For the VM layer I like the containment pattern more than inheritence, and at this layer I also implement INotifyPropertyChanged, which is also a property of the VM and not the data model.

public class PersonVm : IDataErrorInfo, INotifyPropertyChanged
{
    private Person _person;

    public PersonVm( ) {
        // default constructor
        _person = new Person( );
        _dirty = false;
    }

    public PersonVm( Person p ) {
        // User this constructor when you get a Person from database or network
        _person = p;
        _dirty = false;
    }

    void fire( string prop ) {
        PropertyChanged( this, new PropertyChangedEventArgs( prop ) );
    }

    public string name {
        get { return _person.name; }
        set { _person.name = value; fire( "name" ); dirty = true; }
    }

    ...

    string IDataErrorInfo.this[string columnName] { 
        get {
            if( dirty ) return _person[columnName];
        }
    }

}

The idea is to put the logic of each layer at the appropriate class. At the data model layer, you do validation that only concern the pure data. The the View Model layer, you add logic that concerns the View Model (as well as notificaton and other View Model logic).

Trellis answered 6/2, 2012 at 16:7 Comment(1)
I agree, you should probably not be exposing your entities directly to the view. And the point about changing the Business Entity is a moot point when you have already had to dirty it with the IDataErrorInfo interfaceReflexive
J
3

Damn this took a while to think out but as always,...attached behaviours to the rescue.

What you're looking at in essence is dirty state tracking. There are many ways to do this using your ViewModel but since you didn't want to change your entities the best way is using behaviours.

First off remove the ValidatesOnDataErrors from your Xaml binding. Create a behaviour for the control you're working on ( as shown below for TextBox ) and in the TextChanged event (or whatever event you want) reset the binding to one that does validate on data errors. Simple really.

This way, your entities don't have to change, your Xaml is kept reasonably clean and you get your behaviour.

Here's the behaviour code-

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

    namespace IDataErrorInfoSample
    {
        public static class DirtyStateBehaviours
        {


            public static string GetDirtyBindingProperty(DependencyObject obj)
            {
                return (string)obj.GetValue(DirtyBindingPropertyProperty);
            }

            public static void SetDirtyBindingProperty(DependencyObject obj, string value)
            {
                obj.SetValue(DirtyBindingPropertyProperty, value);
            }

            // Using a DependencyProperty as the backing store for DirtyBindingProperty.  This enables animation, styling, binding, etc...
            public static readonly DependencyProperty DirtyBindingPropertyProperty =
                DependencyProperty.RegisterAttached("DirtyBindingProperty", typeof(string), typeof(DirtyStateBehaviours),
                new PropertyMetadata(new PropertyChangedCallback(Callback)));


            public static void Callback(DependencyObject obj,
                DependencyPropertyChangedEventArgs args)
            {
                var textbox = obj as TextBox;
                textbox.TextChanged += (o, s) =>
                {
                    Binding b = new Binding(GetDirtyBindingProperty(textbox));
                    b.ValidatesOnDataErrors = true;
                    textbox.SetBinding(TextBox.TextProperty, b);
                };

            }
        }
    }

And the Xaml is pretty straight forward too.

<Window x:Class="IDataErrorInfoSample.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:IDataErrorInfoSample"
    xmlns:sys="clr-namespace:System;assembly=mscorlib"
    Title="MainWindow"
    Height="350"
    Width="525">

<Window.DataContext>
    <local:Person />
</Window.DataContext>
<StackPanel Margin="20">
    <TextBox Height="20"
             Margin="0,0,0,10"
             local:DirtyStateBehaviours.DirtyBindingProperty="Name"
             Text="{Binding Path=Name}">
    </TextBox>
    <Button Content="Go" />
</StackPanel>

HTH, Stimul8d.

Jannette answered 16/11, 2010 at 11:17 Comment(1)
You would have to duplicate the name of the binding target and don't forget to remove the eventhandler for the TextChanged event or there will be a new binding every time you type in your textbox.Araxes
T
3

Maybe it's an option for you, to shift your validation to the View: Instead of implementing IDataErrorInfo, you can enable NotifyOnValidationError in your binding and add a ValidationRule that does the check. For ValidationRules there is a standard way to control, if the rule should be applied when the object changes (not the property value directly)

This will already provide visual feedback to the user (ErrorTemplate will be applied). If you need more, e.g. disable some buttons etc, you can wire up the Validation.Error-Event of your view to your ViewModel or BusinessEntity, s.th. you can identify there, if any error is present.

Tendon answered 5/2, 2012 at 22:38 Comment(0)
E
3

I've implemented the following solution:

 public class SkipValidationOnFirstLoadBehavior : Behavior<TextBox>
 {
        protected override void OnAttached()
        {
            AssociatedObject.LostFocus += AssociatedObjectOnLostFocus;
        }

        private void AssociatedObjectOnLostFocus(object sender, RoutedEventArgs routedEventArgs)
        {
            //Execute only once
            AssociatedObject.LostFocus -= AssociatedObjectOnLostFocus;

            //Get the current binding
            BindingExpression  expression = AssociatedObject.GetBindingExpression(TextBox.TextProperty);
            if (expression == null) return;
            Binding parentBinding = expression.ParentBinding;

            //Create a new one and trigger the validation 
            Binding updated = new Binding(parentBinding.Path.Path);
            updated.ValidatesOnDataErrors = true;
            updated.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;  
            AssociatedObject.SetBinding(TextBox.TextProperty, updated);
        }
 }

Example of usage:

    <TextBox Text="{Binding Email}">
        <i:Interaction.Behaviors>
            <local:SkipValidationOnFirstLoadBehavior/>
        </i:Interaction.Behaviors>
    </TextBox>
Eremite answered 18/7, 2013 at 8:57 Comment(0)
M
1

I'm just a junior developer who doesn't have many knowledge but I fixed it this way.

In my validationresult class I've made a constructor with no parameters to return a valid validationresult.

public  class NotEmptyValidation : ValidationRule
{
  public override ValidationResult Validate(object value, CultureInfo cultureInfo)
  {
      if (string.IsNullOrEmpty(value as string))
      {
          return new ValidationResult(false,"Veld kan niet leeg zijn");
      }

      return new ValidationResult(true,null);

 }
  public NotEmptyValidation() : base()
  {
      Validate();
  }


  public ValidationResult Validate()
  {
      return new ValidationResult(true,null);
  }
}

My xaml code looks like this

<!--TEXTBOXES-->
                <TextBox Grid.Column="1" Grid.ColumnSpan="2" Margin="5">
                    <TextBox.Text>
                        <Binding Path="SelectedEntity.Code" UpdateSourceTrigger="PropertyChanged">
                            <Binding.ValidationRules>
                                <val:NotEmptyValidation  />
                            </Binding.ValidationRules>
                        </Binding>
                    </TextBox.Text>
                </TextBox>
                <TextBox Grid.Column="1" Grid.ColumnSpan="3" Grid.Row="1" Margin="5">
                    <TextBox.Text>
                        <Binding Path="SelectedEntity.Name" UpdateSourceTrigger="PropertyChanged">
                            <Binding.ValidationRules>
                                <val:NotEmptyValidation />
                            </Binding.ValidationRules>
                        </Binding>
                    </TextBox.Text>
                </TextBox>

When my form loads, the validation doesnt fire when the window loads, but if I clear a textbox, it does fire.

There is a downside to this, if I load an invalid Entity wich has an emty name or code, the validation doesnt fire at loading the window, it does however when u fill in the textbox and clear it. But this doesn't really happen since I validate all my fields when I create the entity..

Its not a perfect solution but it works for me.

Mauretta answered 16/9, 2014 at 17:5 Comment(0)
P
0

I believe this behavior to also be a good solution. It removes ErrorTemplate on TextBox when needed and also supports multiple "valid" invalid values (you can also improve it by making ValidInputs a dependency property).

public class NotValidateWhenSpecified : Behavior<TextBox>
{
    private ControlTemplate _errorTemplate;

    public string[] ValidInputs { get; set; } = { string.Empty };

    protected override void OnAttached()
    {
        AssociatedObject.TextChanged += HideValiationIfNecessary;

        _errorTemplate = Validation.GetErrorTemplate(AssociatedObject);
        Validation.SetErrorTemplate(AssociatedObject, null);
    }        

    protected override void OnDetaching()
    {
        AssociatedObject.TextChanged -= HideValiationIfNecessary;
    }

    private void HideValiationIfNecessary(object sender, TextChangedEventArgs e)
    {
        if (ValidInputs.Contains(AssociatedObject.Text))
        {                
            if (_errorTemplate != null)
            {
                _errorTemplate = Validation.GetErrorTemplate(AssociatedObject);
                Validation.SetErrorTemplate(AssociatedObject, null);
            }                
        }
        else
        {
            if (Validation.GetErrorTemplate(AssociatedObject) != _errorTemplate)
            {
                Validation.SetErrorTemplate(AssociatedObject, _errorTemplate);
            }                
        }
    }
}
Pumpkin answered 23/3, 2019 at 9:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.