How to disable a button if textbox and passwordbox is blank in wpf?

500 Views Asked by At

I basically used a Model's (UserAccount) Property from my ViewModel(CreateAccountViewModel) to bind to my View, and call to my Command (CreateAccountCommand).


My Model(UserAccount):

public class UserAccount : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;


    private int _id;
    private string _username;
    private string _password;
    private DateTime _dateTime;

    public int Id
    {
        get { return _id; }
        set { _id = value; OnPropertyChanged(nameof(Id)); }
    }


    public string Username
    {
        get { return _username; }
        set { _username = value; OnPropertyChanged(nameof(Username)); }
    }



    public string Password
    {
        get { return _password; }
        set { _password = value; OnPropertyChanged(nameof(Password)); }
    }


    public DateTime DateCreated
    {
        get { return _dateTime; }
        set { _dateTime = value; OnPropertyChanged(nameof(DateCreated)); }
    }

    public virtual void OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

My ViewModel(CreateAccountViewModel):

public class CreateAccountViewModel: ViewModelBase
{
    private UserAccount _userAccount;

    public UserAccount CurrentUserAccount
    {
        get { return _userAccount; }
        set { _userAccount = value; OnPropertyChanged(nameof(CurrentUserAccount)); }
    }

    public ICommand CreateAccountCommand{ get; }

    public CreateAccountViewModel()
    {
        CreateAccountCommand= new CreateAccountCommand(this, Test);
        CurrentUserAccount = new UserAccount();
    }

    public void Test()
    {
        MessageBox.Show("Random Message");
        //I'm going to put my Create functionality here
    }
}

My View (CreateAccountView):

<!--The TextBox for username-->
<TextBox Grid.Column="1"
         Margin="10,0,0,0"
         Text="{Binding Path=CurrentUserAccount.Username, UpdateSourceTrigger=PropertyChanged}" />

<!--The PasswordBox for password-->
<components:BindablePasswordBox Grid.Column="1"
                                Margin="10,0,0,0"
                                Password="{Binding Path=CurrentUserAccount.Password, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"
                                Grid.ColumnSpan="2" />

<!--The Create user button-->
<Button Grid.Row="2"
        Margin="0,20,0,0"
        HorizontalAlignment="Center"
        Command="{Binding CreateAccountCommand}"
        Content="Create Account" />

My Command(CreateAccountCommand):

public class CreateAccountCommand: ICommand
{
    private readonly CreateAccountViewModel _viewModel;
    private readonly Action RunCommand;

    public CreateAccountCommand(CreateAccountViewModel viewModel , Action runCommand)
    {
        _viewModel = viewModel;
        _viewModel.PropertyChanged += ViewModel_PropertyChanged;

        RunCommand = runCommand;
    }

    public event EventHandler CanExecuteChanged;

    public bool CanExecute(object parameter)
    {        
        //This is supposed to check whether the Username textbox and Password passwordbox is blank (if both of them are blank, the button should be disabled, else disabled
        return !string.IsNullOrEmpty(_viewModel.CurrentUserAccount.Username) && !string.IsNullOrEmpty(_viewModel.CurrentUserAccount.Password);
    }

    public void Execute(object parameter)
    {
        RunCommand();
    }

    private void ViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        CanExecuteChanged?.Invoke(this, new EventArgs());
    }
}

My PasswordBox is bindable because I created a custom PasswordBox with DependencyProperty:

public partial class BindablePasswordBox : UserControl
{

    public static readonly DependencyProperty PasswordProperty =
        DependencyProperty.Register("Password", typeof(string), typeof(BindablePasswordBox),
            new PropertyMetadata(string.Empty));

    public string Password
    {
        get { return (string)GetValue(PasswordProperty); }
        set { SetValue(PasswordProperty, value); }
    }

    public BindablePasswordBox()
    {
        InitializeComponent();
    }

    //This method will notify us, whenever a password in our passwordBox changes
    private void PasswordBox_PasswordChanged(object sender, RoutedEventArgs e)
    {
        Password = passwordBox.Password; //sets the value of the DependencyProperty (PasswordProperty)
    }
}

My problem here, is that, the button in my View does not change enable/disable even if I set my command's CanExecute to do so. Am I missing something obvious here? I really have to ask because I've been stuck here since yesterday. (My Main goal here is to disable the Create Account button if the Textbox and PasswordBox have no input. Any solutions are okay)

2

There are 2 best solutions below

8
On

The CreateAccountCommand hooks up en event handler to the view model's PropertyChanged but there is no such event raised when you set the Username and Password properties of the UserAccount object.

Either implement INotifyPropertyChanged in UserAccount or bind to wrapper properties of the CreateAccountViewModel:

public string Username
{
    get { return _userAccount?.Username; }
    set
    {
        if (_userAccount != null)
            _userAccount.Username = value;
        OnPropertyChanged();
    }
}

If you decide to implement INotifyPropertyChanged in UserAccount, you still need to notify the command when the properties have been updated.

Since your CurrentUserAccount property may be set to a new value dynamically, you should remove and add the event handler dynamically:

private UserAccount _userAccount;
public UserAccount CurrentUserAccount
{
    get { return _userAccount; }
    set
    {
        if (_userAccount != null)
            _userAccount.PropertyChanged -= OnUserAccountPropertyChanged;
        _userAccount = value;
        if (_userAccount != null)
            _userAccount.PropertyChanged += OnUserAccountPropertyChanged;
        OnPropertyChanged(nameof(CurrentUserAccount));
    }
}

private void OnUserAccountPropertyChanged(object sender, PropertyChangedEventArgs e) =>
    OnPropertyChanged(null);
0
On

Lets do a small refactoring.

  • use CallerMemberNameAttribute (see here how) to have shorter property setters in vm;
  • write once reusable ICommand implementation and use it for all commands, see DelegateCommand;
  • rise command CanExecuteChanged in vm when you change one of command canExecuted condition;
  • UserAccount needs notifications (you have done it in the edit), if it's a model, then you need an extra vm to act as a wrapper, otherwise you wouldn't be able to catch changes done by the bound controls;
  • Since the properties of UserAccount are part of command canExecuted, you need to monitor for them.

With all changes your button using the command should be property enabled/disabled.

Below is pseudo-code (can contain mistakes):


public class CreateAccountViewModel: ViewModelBase
{
    UserAccount _userAccount;
    public UserAccount CurrentUserAccount
    {
        get => _userAccount;
        set
        {
            // unsubscribe
            if(_userAccount != null)
                _userAccount -= UserAccount_PropertyChanged;
            _userAccount = value;
            // subscribe
            if(_userAccount != null)
                _userAccount += UserAccount_PropertyChanged;
            // notifications
            OnPropertyChanged(); // shorter syntax with CallerMemberNameAttribute
            CreateAccountCommand.RaiseCanExecuteChanged();
        }
    }

    public ICommand CreateAccountCommand { get; }

    public CreateAccountViewModel()
    {
        CurrentUserAccount = new UserAccount();
        CreateAccountCommand = new DelegateCommand(Test,
            o => !string.IsNullOrEmpty(CurrentUserAccount.Username) && !string.IsNullOrEmpty(CurrentUserAccount.Password));
    }

    void Test(object parameter)
    {
        MessageBox.Show("Random Message");
        //I'm going to put my Create functionality here
    }

    void UserAccount_PropertyChanged(object sender, NotifyPropertyChangedEventArgs e) =>
        CreateAccountCommand.RaiseCanExecuteChanged(); // rise always of check for specific properties changes
}