Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Hide WPF validation error when window is first loaded

Tags:

c#

validation

wpf

When i first load the window, the button is visible and there isn't an error in the validation (no red line round textbox).

what window looks like when first loaded

When typing values into the textbox, the validation rules work as they should.

I would like, if possible, to have the button hidden at the start, and for the validation rules to start when the I start typing text into the box.

Here is the code that i have so far. The xaml:

 <TextBox x:Name="txtName" HorizontalAlignment="Left" Height="23" Margin="156,119,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="120" Validation.ErrorTemplate="{StaticResource ValidationErrorTemplate}">
        <TextBox.Text>
            <Binding Path="Name" UpdateSourceTrigger="PropertyChanged" NotifyOnSourceUpdated="True">
                <Binding.ValidationRules>
                    <local:ValidationTest/>
                </Binding.ValidationRules>
            </Binding>
        </TextBox.Text>
    </TextBox>


    <Button x:Name="btn1" Content="Button" HorizontalAlignment="Left" Margin="85,221,0,0" VerticalAlignment="Top" Width="75" Click="Button_Click">
        <Button.Style>
            <Style TargetType="Button">
                <Setter Property="Visibility" Value="Hidden"/>
                <Style.Triggers>
                    <MultiDataTrigger>
                        <MultiDataTrigger.Conditions>
                            <Condition Binding="{Binding Path=(Validation.HasError), ElementName=txtName}" Value="False"/>
                        </MultiDataTrigger.Conditions>
                        <Setter Property="Visibility" Value="Visible"></Setter>
                    </MultiDataTrigger>
                </Style.Triggers>
            </Style>
        </Button.Style>
    </Button>

My validation logic:

class ValidationTest : ValidationRule
{
    private int result;
    public override ValidationResult Validate(object value, CultureInfo cultureInfo)
    {
        if (value == null || string.IsNullOrEmpty(value.ToString()))
        {
            return new ValidationResult(false, "Value cannot be empty.");
        }
        if (value.ToString().Length > 4)
        {
            return new ValidationResult(false, "Name cannot be more than 20 characters long.");
        }
        return ValidationResult.ValidResult;
    }
}

The error template i am using:

    <Window.Resources>
    <Style 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>
    <ControlTemplate x:Key="ValidationErrorTemplate">
        <DockPanel>
            <Border BorderBrush="Red" BorderThickness="1.5">
                <AdornedElementPlaceholder x:Name="ErrorAdorner"></AdornedElementPlaceholder>
            </Border>
        </DockPanel>
    </ControlTemplate>
</Window.Resources>

I have tried to update the binding when the window loads by using txtName.GetBindingExpression(TextBox.TextProperty).UpdateSource();, but that shows the validation error (red line around textbox). However, the button is hidden, so is there any way of hiding the validation error until the user types text into the textbox?

what i get when i update the binding when the windows loads

Am i approaching this in the right way?

like image 241
user2840120 Avatar asked May 14 '26 03:05

user2840120


1 Answers

I know I am a little late to the party but I came across this question while I was looking to do the same thing. The only thing that I didn't like about using a flag to control when the Validation is performed is that you needed to set the DoValidation flag at some point in your code and I wanted it to be a little more "Automated".

I found quite a few examples online but they all seemed to use the Boolean flag method. I found this MSDN Article and used it as a base then adjusted the code.

I came up with a solution that seems to work really well. Basically in a nutshell what I did was instead of having one variable to keep track of when the validation should be performed I created another Dictionary to keep track of:

  1. When the Validation should be performed.
  2. Store the state of the Validation (Valid, Invalid).

I only wanted the validation to be performed after the first update, so the first order of business is to decide if the Validation should be performed. The first run of the Validation stores the parameter in the Dictionary, then next time around if the parameter is present it performs the validation and stores a true/false (Invalid/Valid) result. This is also a handy way of telling both if the Model has been Validated and if it is valid, so I also added a parameter/flag to simply return if there are any results and the state of the validation. This is especially useful for binding the command enable/disable.

Here is how I accomplished this:

Base PropertyValidation Model:

public abstract class PropertyValidation : INotifyPropertyChanged, IDataErrorInfo
{
   #region Fields

   private readonly Dictionary<string, object> _values = new Dictionary<string, object>();

   /// <summary>
   /// This holds the list of validation results and controls when the validation should be 
   /// performed and if the validation is valid.
   /// </summary>
   private Dictionary<string, bool> _validationResults { get; set; } = new Dictionary<string, bool>();

   #endregion

   #region Protected

   /// <summary>
   /// Sets the value of a property.
   /// </summary>
   /// <typeparam name="T">The type of the property value.</typeparam>
   /// <param name="propertySelector">Expression tree contains the property definition.</param>
   /// <param name="value">The property value.</param>
   protected void SetValue<T>(Expression<Func<T>> propertySelector, T value)
   {
      string propertyName = GetPropertyName(propertySelector);

      SetValue<T>(propertyName, value);
   }

   /// <summary>
   /// Sets the value of a property.
   /// </summary>
   /// <typeparam name="T">The type of the property value.</typeparam>
   /// <param name="propertyName">The name of the property.</param>
   /// <param name="value">The property value.</param>
   protected void SetValue<T>(string propertyName, T value)
   {
      if (string.IsNullOrEmpty(propertyName))
      {
         throw new ArgumentException("Invalid property name", propertyName);
      }

      _values[propertyName] = value;
      OnPropertyChanged(propertyName);
   }

   /// <summary>
   /// Gets the value of a property.
   /// </summary>
   /// <typeparam name="T">The type of the property value.</typeparam>
   /// <param name="propertySelector">Expression tree contains the property definition.</param>
   /// <returns>The value of the property or default value if not exist.</returns>
   protected T GetValue<T>(Expression<Func<T>> propertySelector)
   {
      string propertyName = GetPropertyName(propertySelector);

      return GetValue<T>(propertyName);
   }

   /// <summary>
   /// Gets the value of a property.
   /// </summary>
   /// <typeparam name="T">The type of the property value.</typeparam>
   /// <param name="propertyName">The name of the property.</param>
   /// <returns>The value of the property or default value if not exist.</returns>
   protected T GetValue<T>(string propertyName)
   {
      if (string.IsNullOrEmpty(propertyName))
      {
         throw new ArgumentException("Invalid property name", propertyName);
      }

      object value;
      if (!_values.TryGetValue(propertyName, out value))
      {
         value = default(T);
         _values.Add(propertyName, value);
      }

      return (T)value;
   }

   /// <summary>
   /// Validates current instance properties using Data Annotations.
   /// </summary>
   /// <param name="propertyName">This instance property to validate.</param>
   /// <returns>Relevant error string on validation failure or <see cref="System.String.Empty"/> on validation success.</returns>
   protected virtual string OnValidate(string propertyName)
   {
      string error = string.Empty;

      if (string.IsNullOrEmpty(propertyName))
      {
         throw new ArgumentException("Invalid property name", propertyName);
      }

      //Check if the Field has been added, this keeps track of when the validation
      //is performed.
      if (_validationResults.Any(x => x.Key == propertyName))
      {
         var value = GetValue(propertyName);
         var results = new List<System.ComponentModel.DataAnnotations.ValidationResult>(1);
         var result = Validator.TryValidateProperty(
               value,
               new ValidationContext(this, null, null)
               {
                  MemberName = propertyName
               },
               results);

         if (!result)
         {
            var validationResult = results.First();
            error = validationResult.ErrorMessage;

            //Store a true result in the validation to set the error.
            _validationResults[propertyName] = true;
         }
         else
         {
            //If the Validation has been run and not invalid make sure the 
            //paramter in the list is cleared, otherwise validation would 
            //always return invalid once it is invalidated.
            _validationResults[propertyName] = false;
         }
      }
      else
      {
         //This is the first run of the Validation, simply store the paramter
         //in the validation list and wait until next time to validate.
         _validationResults.Add(propertyName, true);
      }

      //Notify that things have changed
      OnPropertyChanged("IsValid");

      //Return the actual result
      return error;
   }

   #endregion

   #region Public

   /// <summary>
   /// This returns if the Validation is Valid or not
   /// </summary>
   /// <returns>True if the Validation has been perfomed and if there are not 
   /// true values. Will return false until the validation has been done once.</returns>
   public bool IsValid {
      get { return (!_validationResults.Any(x => x.Value) && (_validationResults.Count > 0)); }
   }

   /// <summary>
   /// Clears/Reset the Validation
   /// </summary>
   public void ClearValidation()
   {
      _validationResults.Clear();
   }

   #endregion

   #region Change Notification

   /// <summary>
   /// Raised when a property on this object has a new value.
   /// </summary>
   public event PropertyChangedEventHandler PropertyChanged;

   /// <summary>
   /// Raises this object's PropertyChanged event.
   /// </summary>
   /// <param name="propertyName">The property that has a new value.</param>
   protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
   {
      this.VerifyPropertyName(propertyName);

      PropertyChangedEventHandler handler = this.PropertyChanged;
      if (handler != null)
      {
         var e = new PropertyChangedEventArgs(propertyName);
         handler(this, e);
      }
   }

   protected void OnPropertyChanged<T>(Expression<Func<T>> propertySelector)
   {
      var propertyChanged = PropertyChanged;
      if (propertyChanged != null)
      {
         string propertyName = GetPropertyName(propertySelector);
         propertyChanged(this, new PropertyChangedEventArgs(propertyName));
      }
   }

   #endregion // IOnPropertyChanged Members

   #region Data Validation

   string IDataErrorInfo.Error {
      get {
         throw new NotSupportedException("IDataErrorInfo.Error is not supported, use IDataErrorInfo.this[propertyName] instead.");
      }
   }

   string IDataErrorInfo.this[string propertyName] {
      get {
         return OnValidate(propertyName);
      }
   }

   #endregion

   #region Privates

   private string GetPropertyName(LambdaExpression expression)
   {
      var memberExpression = expression.Body as MemberExpression;
      if (memberExpression == null)
      {
         throw new InvalidOperationException();
      }

      return memberExpression.Member.Name;
   }

   private object GetValue(string propertyName)
   {
      object value;
      if (!_values.TryGetValue(propertyName, out value))
      {
         var propertyDescriptor = TypeDescriptor.GetProperties(GetType()).Find(propertyName, false);
         if (propertyDescriptor == null)
         {
            throw new ArgumentException("Invalid property name", propertyName);
         }

         value = propertyDescriptor.GetValue(this);
         _values.Add(propertyName, value);
      }

      return value;
   }

   #endregion

   #region Debugging

   /// <summary>
   /// Warns the developer if this object does not have
   /// a public property with the specified name. This 
   /// method does not exist in a Release build.
   /// </summary>
   [Conditional("DEBUG")]
   [DebuggerStepThrough]
   public void VerifyPropertyName(string propertyName)
   {
      // Verify that the property name matches a real,  
      // public, instance property on this object.
      if (TypeDescriptor.GetProperties(this)[propertyName] == null)
      {
         string msg = "Invalid property name: " + propertyName;

         if (this.ThrowOnInvalidPropertyName)
            throw new Exception(msg);
         else
            Debug.Fail(msg);
      }
   }

   /// <summary>
   /// Returns whether an exception is thrown, or if a Debug.Fail() is used
   /// when an invalid property name is passed to the VerifyPropertyName method.
   /// The default value is false, but subclasses used by unit tests might 
   /// override this property's getter to return true.
   /// </summary>
   protected virtual bool ThrowOnInvalidPropertyName { get; private set; }

   #endregion // Debugging Aides
}

Model:

public class MyModel : PropertyValidation
{
   [Required(ErrorMessage = "Name must be specified")]
   [MaxLength(50, ErrorMessage = "Name too long, Name cannot contain more than 50 characters")]
   public string Name {
      get { return GetValue(() => Name); }
      set { SetValue(() => Name, value); }
   }

   [Required(ErrorMessage = "Description must be specified")]
   [MaxLength(150, ErrorMessage = "Description too long, Description cannot contain more than 150 characters")]
   public string Description {
      get { return GetValue(() => Description); }
      set { SetValue(() => Description, value); }
   }
}

Error Template:

<ControlTemplate x:Key="ValidationErrorTemplate">
   <DockPanel LastChildFill="true">
      <Border Background="Red" DockPanel.Dock="right" 
                           Margin="-20,0,0,0" Width="10" Height="10" CornerRadius="10"
                           ToolTip="{Binding ElementName=customAdorner, Path=AdornedElement.(Validation.Errors)[0].ErrorContent}">
         <TextBlock Text="!" VerticalAlignment="center" HorizontalAlignment="center" FontWeight="Bold" Foreground="white"/>
      </Border>
      <AdornedElementPlaceholder Name="customAdorner" VerticalAlignment="Center" >
         <Border BorderBrush="red" BorderThickness="1" >
            <Border.Effect>
               <BlurEffect Radius="5" />
            </Border.Effect>
         </Border>
      </AdornedElementPlaceholder>

   </DockPanel>
</ControlTemplate>

Data Template/Form:

<DataTemplate x:Key="MyModelDetailsTemplate" DataType="{x:Type data:MyModel}" >

   <StackPanel Grid.IsSharedSizeScope="True">

      <Grid>
         <Grid.ColumnDefinitions>
            <ColumnDefinition Width="auto" SharedSizeGroup="Labels" />
            <ColumnDefinition Width="*" />
         </Grid.ColumnDefinitions>

         <Label Grid.Column="0">Name</Label>
         <TextBox x:Name="Name" 
                  Grid.Column="1" 
                  MinWidth="150"
                  Validation.ErrorTemplate="{StaticResource ValidationErrorTemplate}"
                  Text="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True, NotifyOnValidationError=True}" />
      </Grid>
      <Grid>
         <Grid.ColumnDefinitions>
            <ColumnDefinition Width="auto" SharedSizeGroup="Labels" />
            <ColumnDefinition Width="*" />
         </Grid.ColumnDefinitions>

         <Label Grid.Column="0">Description</Label>
         <TextBox Grid.Column="1" 
                   Validation.ErrorTemplate="{StaticResource ValidationErrorTemplate}"
                  Text="{Binding Description, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True, NotifyOnValidationError=True}" AcceptsReturn="True" VerticalAlignment="Stretch" />

      </Grid>
   </StackPanel>

</DataTemplate>

RelayCommand (For Completeness)

public class RelayCommand : ICommand
{
   private Action<object> execute;

   private Predicate<object> canExecute;

   private event EventHandler CanExecuteChangedInternal;

   public RelayCommand(Action<object> execute) : this(execute, DefaultCanExecute)
   {
   }

   public RelayCommand(Action<object> execute, Predicate<object> canExecute)
   {
      if (execute == null)
      {
         throw new ArgumentNullException("execute");
      }

      if (canExecute == null)
      {
         throw new ArgumentNullException("canExecute");
      }

      this.execute = execute;
      this.canExecute = canExecute;
   }

   public event EventHandler CanExecuteChanged {
      add {
         CommandManager.RequerySuggested += value;
         this.CanExecuteChangedInternal += value;
      }

      remove {
         CommandManager.RequerySuggested -= value;
         this.CanExecuteChangedInternal -= value;
      }
   }

   public bool CanExecute(object parameter)
   {
      return this.canExecute != null && this.canExecute(parameter);
   }

   public void Execute(object parameter)
   {
      this.execute(parameter);
   }

   public void OnCanExecuteChanged()
   {
      EventHandler handler = this.CanExecuteChangedInternal;
      if (handler != null)
      {
         //DispatcherHelper.BeginInvokeOnUIThread(() => handler.Invoke(this, EventArgs.Empty));
         handler.Invoke(this, EventArgs.Empty);
      }
   }

   public void Destroy()
   {
      this.canExecute = _ => false;
      this.execute = _ => { return; };
   }

   private static bool DefaultCanExecute(object parameter)
   {
      return true;
   }
}

ViewModel:

** Note the PropertyValidation here is not required, A separate INotifyPropertyChanged base could be used instead, I only used it for the OnPropertyChanged Notification and to keep things simple **

public class PageHomeVM : PropertyValidation
{
   private ICommand saveCommand;
   public ICommand SaveCommand {
      get {
         return saveCommand;
      }
      set {
         saveCommand = value;
         OnPropertyChanged();
      }
   }

   public MyModel MyModel { get; set; } = new MyModel();

   public PageHomeVM()
   {
      SaveCommand = new RelayCommand(SaveRecord, p => MyModel.IsValid);
      MyModel.ClearValidation();
   }

   public void SaveRecord(object p)
   {
      //Perform the save....
   }
}

View:

<pages:BasePage.DataContext>
   <ViewModels:PageHomeVM/>
</pages:BasePage.DataContext>

<StackPanel>
   <Label Content="MyModel Details"/>

   <ContentPresenter ContentTemplate="{StaticResource MyModelDetailsTemplate}" Content="{Binding MyModel}" />

   <Button x:Name="btnSave" 
            Command="{Binding SaveCommand}"
            Width="75"
            HorizontalAlignment="Right">Save</Button>

</StackPanel>

I hope this helps...

like image 183
Andy Braham Avatar answered May 15 '26 16:05

Andy Braham