-1

I have WPF MVVM application, and one the ViewModel - View pair is a creation page. When u r providing an incorrect input in the textbox which is binded to the value it is turning red, I want to also disable the button "Ok" (which is submitting the entered data, and starting the try to create an object). How can I disable the button, if, let's say, user typed in the text in the text box binded to the int?

Palamar66
  • 212
  • 1
  • 9
  • The main way is to pass the dependency through a command parameter. In this case, the command's CanExecute method will be able to check it. If the value is not valid, then it should return false and the button will become IsEnabled=false. – EldHasp Nov 05 '22 at 21:52

1 Answers1

1

You should use data validation in the first place in order to validate the text property that binds to the TextBox.

For this purpose you would implement the INotifyDataErrorInfo interface.
You can find a complete example following this post: How to add validation to view model properties or how to implement INotifyDataErrorInfo

Once you have implemented the interface, you can use the INotifyDataErrorInfo.GetErrors method to let the ICommand that is bound to the Button check whether it can execute or not. If the command can't execute then the Button (which implements ICommandSource) will automatically disable itself.

The code snippets are taken from the referenced example above:

MainWindow.xaml

<Window>
    <Window.DataContext>
        <MainViewModel />       
    </Window.DataContext>
    
    <StackPanel>
        <Button Command="{Binding OkCommand}"
                Content="Ok" />

        <!-- Important: set ValidatesOnNotifyDataErrors to true to enable visual feedback -->
        <TextBox Text="{Binding UserInput, ValidatesOnNotifyDataErrors=True}" />  
</Window>

ViewModel.cs

// Example uses System.ValueTuple
public class ViewModel : INotifyPropertyChanged, INotifyDataErrorInfo
{  
  // Alternative usage example property which validates its value 
  // before applying it using a Method Group.
  // Example uses System.ValueTuple.
  private string userInput;
  public string UserInput
  { 
    get => this.userInput; 
    set 
    { 
      // Use Method group
      if (IsPropertyValid(value, IsUserInputValid))
      {
        this.userInput = value; 
        OnPropertyChanged();
      }
    }
  }

  public ICommand OkCommand => new RelayCommand(ExecuteOkCommand, CanExecuteOkCommand);

  // Constructor
  public ViewModel()
  {
    this.Errors = new Dictionary<string, IList<object>>();
  }

  private void ExecuteOkCommand(object commandParameter)
  { ... }

  // Control whether the Button (or a ICommandSource in general) is enabled or not
  private bool CanExecuteOkCommand(object commandParameter)
  {
    // Use INotifyDataErrorInfo.GetErrors to check for errors of a particular property.
    // This example uses a custom method (defined below) to achieve this.
    // Opposed to GetErrors, this custom method does not return the actual errors.
    return PropertyHasErrors(nameof(this.UserInput));
  }


  // The validation handler for the 'UserInput' property
  private (bool IsValid, IEnumerable<object> ErrorMessages) IsUserInputValid(string value)
  {
    return value.StartsWith("@") 
      ? (true, Enumerable.Empty<object>()) 
      : (false, new[] { "Value must start with '@'." });
  }

  // Example uses System.ValueTuple
  public bool IsPropertyValid<TValue>(
    TValue value, 
    Func<TValue, (bool IsValid, IEnumerable<object> ErrorMessages)> validationDelegate, 
    [CallerMemberName] string propertyName = null)  
  {  
    // Clear previous errors of the current property to be validated 
    _ = ClearErrors(propertyName); 

    // Validate using the delegate
    (bool IsValid, IEnumerable<object> ErrorMessages) validationResult = validationDelegate?.Invoke(value) ?? (true, Enumerable.Empty<object>());

    if (!validationResult.IsValid)
    {
      AddErrorRange(propertyName, validationResult.ErrorMessages);
    } 

    return validationResult.IsValid;
  }  

  // Adds the specified errors to the errors collection if it is not 
  // already present, inserting it in the first position if 'isWarning' is 
  // false. Raises the ErrorsChanged event if the Errors collection changes. 
  // A property can have multiple errors.
  private void AddErrorRange(string propertyName, IEnumerable<object> newErrors, bool isWarning = false)
  {
    if (!newErrors.Any())
    {
      return;
    }

    if (!this.Errors.TryGetValue(propertyName, out IList<object> propertyErrors))
    {
      propertyErrors = new List<object>();
      this.Errors.Add(propertyName, propertyErrors);
    }

    if (isWarning)
    {
      foreach (object error in newErrors)
      {
        propertyErrors.Add(error);
      }
    }
    else
    {
      foreach (object error in newErrors)
      {
        propertyErrors.Insert(0, error);
      }
    }

    OnErrorsChanged(propertyName);
  }

  // Removes all errors of the specified property. 
  // Raises the ErrorsChanged event if the Errors collection changes. 
  public bool ClearErrors(string propertyName)
  {
    this.ValidatedAttributedProperties.Remove(propertyName);
    if (this.Errors.Remove(propertyName))
    { 
      OnErrorsChanged(propertyName); 
      return true;
    }
    return false;
  }

  // Optional method to check if a certain property has validation errors
  public bool PropertyHasErrors(string propertyName) => this.Errors.TryGetValue(propertyName, out IList<object> propertyErrors) && propertyErrors.Any();

  #region INotifyDataErrorInfo implementation

  // The WPF binding engine will listen to this event
  public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

  // This implementation of GetErrors returns all errors of the specified property. 
  // If the argument is 'null' instead of the property's name, 
  // then the method will return all errors of all properties.
  // This method is called by the WPF binding engine when ErrorsChanged event was raised and HasErrors return true
  public System.Collections.IEnumerable GetErrors(string propertyName) 
    => string.IsNullOrWhiteSpace(propertyName) 
      ? this.Errors.SelectMany(entry => entry.Value) 
      : this.Errors.TryGetValue(propertyName, out IList<object> errors) 
        ? (IEnumerable<object>)errors 
        : new List<object>();

  // Returns 'true' if the view model has any invalid property
  public bool HasErrors => this.Errors.Any(); 

  #endregion

  #region INotifyPropertyChanged implementation

  public event PropertyChangedEventHandler PropertyChanged;

  #endregion

  protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)        
  {
    this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
  }

  protected virtual void OnErrorsChanged(string propertyName)
  {
    this.ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
  }

  // Maps a property name to a list of errors that belong to this property
  private Dictionary<string, IList<object>> Errors { get; }
}
BionicCode
  • 1
  • 4
  • 28
  • 44