Home > front end >  WPF - How to validate a form after clicking on a button?
WPF - How to validate a form after clicking on a button?

Time:07-13

I found this simple example for validating user inputs in WPF. It works fine, but the validation happens after every input change. I would like the validation to happen only when the button is clicked (after submiting a form). In other words - I want the same behavior as HTTP5 inputs with the required property have.

I assume, that the problem is in the UpdateSourceTrigger="PropertyChanged", but I dont know, how to bind it on a button.

MainWindow.xaml

<Window x:Class="B_Validation_ByDataErrorInfo.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:B_Validation_ByDataErrorInfo"
    mc:Ignorable="d"
    Title="Validation By IDataErrorInfo" Height="450" Width="800">
<Window.Resources>
    <local:Person x:Key="data"/>

    <!--The tool tip for the TextBox to display the validation error message.-->
    <Style x:Key="textBoxInError" TargetType="TextBox">
        <Style.Triggers>
            <Trigger Property="Validation.HasError" Value="true">
                <Setter Property="ToolTip"
                        Value="{Binding RelativeSource={x:Static RelativeSource.Self},
                    Path=(Validation.Errors)[0].ErrorContent}"/>
            </Trigger>
        </Style.Triggers>
    </Style>
</Window.Resources>

<StackPanel Margin="20">
    <TextBlock>Enter your age:</TextBlock>

    <TextBox Style="{StaticResource textBoxInError}">
        <TextBox.Text>
            <Binding Path="Age" Source="{StaticResource data}"
                     ValidatesOnExceptions="True"
                     UpdateSourceTrigger="PropertyChanged">
                <Binding.ValidationRules>
                    <DataErrorValidationRule/>
                </Binding.ValidationRules>
            </Binding>
        </TextBox.Text>
    </TextBox>

    <Button>Submit</Button>
</StackPanel>

Person.cs

public class Person : IDataErrorInfo
{
    private int age;

    public int Age
    {
        get { return age; }
        set { age = value; }
    }

    public string Error => null;

    public string this[string name]
    {
        get
        {
            string result = null;

            if (name == "Age")
            {
                if (this.age < 0 || this.age > 150)
                {
                    result = "Age must not be less than 0 or greater than 150.";
                }
            }
            return result;
        }
    }
}

CodePudding user response:

DataErrorInfo is the old interface. Since .NET 4.5 we should implement the modern INotifyDataErrorInfo interface (How to add validation to view model properties or how to implement INotifyDataErrorInfo).

You find details on how to implement this interface in the above link. It will also allow you to clean up your code by moving the actual validation logic (constraints) to a dedicated class that extends ValidationRule.

The following example shows how to control property validation outside the property setter.

Following the implementation of the above link, we will have a IsPropertyValid method, that we usually would invoke from the property setter:

// Example property, which validates its value before applying it
private string userInput;
public string UserInput
{ 
  get => this.userInput; 
  set 
  { 
    // Validate the value
    bool isValueValid = IsPropertyValid(value);

    // Optionally reject value if validation has failed
    if (isValueValid)
    {
      this.userInput = value; 
      OnPropertyChanged();
    }
  }
}

The above property validates on every set() call. To validate the entire type on the click of a Button, you can call the IsPropertyValid method from a command handler.

For this reason you should introduce an IComannd (Commanding Overview) to your Person class. You bind this command to the Button.Command property.

The following example adds a SaveCommand to the Person class. ExecuteSaveCommand is the command handler that will execute the property validation for the complete Person.
To avoid referencing each property explicitly, we can use reflection to obtain all public property names of the properties we need to validate.

You can find an example for a reusable command implementation (RelayCommand) by visiting Microsoft Docs: Relaying Command Logic.

The following example extends the example from How to add validation to view model properties or how to implement INotifyDataErrorInfo. It basically adds the member TryValidateAllProperties, which calls the existing IsPropertyValid method from the mentioned example (from the above link).
The below example also shows how to extract your property rule to a custom ValidationRule that you can use with the example from the link (IsPropertyValid method currently depends on a Dictionary<string, IList<ValidationRule>> of defined ValidationRule instances (a property name maps to a collection of validation rules).

AgeValidationRule.cs

public class AgeValidationRule : ValidationRule
{
  public override ValidationResult Validate(object value, CultureInfo cultureInfo)
    => value is int age && age > 0 && age <= 150 
      ? ValidationResult.ValidResult 
      : new ValidationResult(false, "Age must not be less than 0 or greater than 150.");
}

Person.cs

public class Person : INotifyDataErrorInfo
{
  // Maps a property name to a list of ValidationRules that belong to this property
  private Dictionary<string, IList<ValidationRule>> ValidationRules { get; }
  public ICommand SavePersonCommand => new RelayCommand(ExecuteSavePersonCommand);

  private int age;
  public int Age
  {
    get { return age; }
    set { age = value; }
  }       

  public Person()
  {
    this.ValidationRules = new Dictionary<string, IList<ValidationRule>>
    {
      { nameof(this.Age), new List<ValidationRule>() { new AgeValidationRule() } }
    };
  }

  private void ExecuteSavePersonCommand(object obj)
  {
    if (TryValidateAllProperties())
    {
      // TODO::Save this Person
    }
  }

  private bool TryValidateAllProperties()
  {
    PropertyInfo[] publicPropertyInfos = GetType().GetProperties(BindingFlags.Public);
    foreach (PropertyInfo propertyInfo in publicPropertyInfos)
    {
      string propertyName = propertyInfo.Name;
      object propertyValue = propertyInfo.GetValue(this);
      _ = IsPropertyValid(propertyValue, propertyName);
    }

    return this.HasErrors;
  }
}

CodePudding user response:

You need to change so `UpdateSourceTrigger="Explicit", Next you need to add a handler to your button click event, and from within you can invoke updatesource() on the binding.

So, change your xaml as below:

<Window.Resources>
    <local:Person x:Key="data"/>

    <!--The tool tip for the TextBox to display the validation error message.-->
    <Style x:Key="textBoxInError" TargetType="TextBox">
        <Style.Triggers>
            <Trigger Property="Validation.HasError" Value="true">
                <Setter Property="ToolTip"
                    Value="{Binding RelativeSource={x:Static RelativeSource.Self},
                Path=(Validation.Errors)[0].ErrorContent}"/>
            </Trigger>
        </Style.Triggers>
    </Style>
</Window.Resources>

<StackPanel Margin="20">
    <TextBlock>Enter your age:</TextBlock>

    <TextBox x:Name="txtbxValidator" Style="{StaticResource textBoxInError}">
        <TextBox.Text>
            <Binding Path="Age" Source="{StaticResource data}"
                 ValidatesOnExceptions="True"
                 UpdateSourceTrigger="Explicit">
                <Binding.ValidationRules>
                    <DataErrorValidationRule/>
                </Binding.ValidationRules>
            </Binding>
        </TextBox.Text>
    </TextBox>

    <Button Click="Button_Click">Submit</Button>
</StackPanel>

And add to your form's code-behind this method:

 private void Button_Click(object sender, RoutedEventArgs e)
 {
    txtbxValidator.GetBindingExpression(TextBox.TextProperty).UpdateSource();
 }
  • Related