mercredi 6 octobre 2021

Adding custom Input Validation on TextBox and PasswordBox [UWP][XAML]

We are going to go over how to create a custom TextBox & PasswordBox with a new visual state.

I needed this kind on input box for my account creation view, where the user inputs his email and his password, the default controls don't allow you to set these controls in a invalide state when you check for example if the email is valid and if the password is strong enough. 
What I wanted to do is set the border of the control to red when the user had not meet the necessary steps to go to the next step in the account creation process.

While we wait for WinUI 3.0 to support input validation here is my very simple version of how I implemented one on a TextBox and PasswordBox.

The idea was to add a property called HasError to my new control and bind it to my ViewModel, when the HasError property is changed we can use VisualStateManager to change the visual of our control. What I learned was that you can create a custom TextBox but not a PasswordBox as the PasswordBox is a sealed class.

TextBox custom control

Here is my simple control with HasError property added to a TextBox my controler is called  LoginValidatingTextBox.cs

 public class LoginValidatingTextBox : TextBox
    {
        public LoginValidatingTextBox()
        {
             this.DefaultStyleKey = typeof(LoginValidatingTextBox);
        }

        public bool HasError
        {
            get { return (bool)GetValue(HasErrorProperty); }
            set { SetValue(HasErrorProperty, value); }
        }

        public static readonly DependencyProperty HasErrorProperty =
            DependencyProperty.Register("HasError", typeof(bool), typeof(LoginValidatingTextBox), new PropertyMetadata(false, HasErrorUpdated));


        // This method will update the Validation visual state which will be defined later in the Style
        private static void HasErrorUpdated(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            LoginValidatingTextBox textBox = d as LoginValidatingTextBox;

            if (textBox != null)
            {
                if (textBox.HasError)
                    VisualStateManager.GoToState(textBox, "InvalidState", false);
                else
                    VisualStateManager.GoToState(textBox, "ValidState", false);
            }
        }
    }


Next, we need to add this VisualStateGroup to the default style of my TextBox so that when we have the InvalideState activated we can update our TextBox UI as we wish.

<VisualStateGroup x:Name="ValidationState">
 <VisualState x:Name="InvalidState">
  <Storyboard>
   <ObjectAnimationUsingKeyFrames Storyboard.TargetName="BorderElement" Storyboard.TargetProperty="BorderBrush">
    <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource DmRedBrush}" />
   </ObjectAnimationUsingKeyFrames>
  </Storyboard>
 </VisualState>
 <VisualState x:Name="ValidState">
  <Storyboard />
 </VisualState>
</VisualStateGroup>

Here is how we can implement this control in our UWP app

<controls:LoginValidatingTextBox
 x:Name="AddressTextbox"
 HasError="{Binding InvalidEmailErrorVisible}"
 PlaceholderText="Email address"
 Text="{Binding UserEmailAddress}" />

And, now when InvalidEmailErrorVisible is true we will update our TextBox as needed.

PasswordBox custom control

Next we will try to and do the same thing for the password box is not as pretty as you cant inherit from the base control you have to do it in CS of your view.

First off you need to add the same VisualStateGroup to the style of your PasswordBox same as the TextBox, int he code behind of your view you will need to listen to when your PasswordErrorVisible property has changed and call a method that will call:

VisualStateManager.GoToState(UI ELEMENT, STATE YOU WISH, false)

Here is the full code:

private void ViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
 Debug.WriteLine(e.PropertyName);

 switch (e.PropertyName)
 {
  case "PasswordErrorVisible":
   PasswordErrorChanged();
   break;
 }
}

private void PasswordErrorChanged()
{
 if (ViewModel.PasswordErrorVisible)
  VisualStateManager.GoToState(password, "InvalidState", false);
 else
  VisualStateManager.GoToState(password, "ValidState", false);
}

and there you have it fr both TextBox and PasswordBox we now have an invalid state.

Hope this helps!
Happy coding.