// This Source Code Form is subject to the terms of the MIT License. // If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT. // Copyright (C) Leszek Pomianowski and WPF UI Contributors. // All Rights Reserved. using System; using System.Windows; using System.Windows.Controls; namespace Wpf.Ui.Controls; // TODO: This is an initial implementation and requires the necessary corrections, tests and adjustments. /** * TextProperty contains asterisks OR raw password if IsPasswordRevealed is set to true * PasswordProperty always contains raw password */ /// /// The modified password control. /// public class PasswordBox : Wpf.Ui.Controls.TextBox { private bool _lockUpdatingContents; /// /// Property for . /// public static readonly DependencyProperty PasswordProperty = DependencyProperty.Register(nameof(Password), typeof(string), typeof(PasswordBox), new PropertyMetadata(String.Empty, OnPasswordPropertyChanged)); /// /// Property for . /// public static readonly DependencyProperty PasswordCharProperty = DependencyProperty.Register(nameof(PasswordChar), typeof(char), typeof(PasswordBox), new PropertyMetadata('*', OnPasswordCharPropertyChanged)); /// /// Property for . /// public static readonly DependencyProperty IsPasswordRevealedProperty = DependencyProperty.Register(nameof(IsPasswordRevealed), typeof(bool), typeof(PasswordBox), new PropertyMetadata(false, OnPasswordRevealModePropertyChanged)); /// /// Property for . /// public static readonly DependencyProperty RevealButtonEnabledProperty = DependencyProperty.Register(nameof(RevealButtonEnabled), typeof(bool), typeof(PasswordBox), new PropertyMetadata(true)); /// /// Event for "Password has changed" /// public static readonly RoutedEvent PasswordChangedEvent = EventManager.RegisterRoutedEvent( nameof(PasswordChanged), RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(PasswordBox)); /// /// Gets or sets currently typed text represented by asterisks. /// public string Password { get => (string)GetValue(PasswordProperty); set => SetValue(PasswordProperty, value); } /// /// Gets or sets character used to mask the password. /// public char PasswordChar { get => (char)GetValue(PasswordCharProperty); set => SetValue(PasswordCharProperty, value); } /// /// Gets a value indicating whether the password is revealed. /// public bool IsPasswordRevealed { get => (bool)GetValue(IsPasswordRevealedProperty); private set => SetValue(IsPasswordRevealedProperty, value); } /// /// Gets or sets a value deciding whether to display the reveal password button. /// public bool RevealButtonEnabled { get => (bool)GetValue(RevealButtonEnabledProperty); set => SetValue(RevealButtonEnabledProperty, value); } /// /// Event fired from this text box when its inner content /// has been changed. /// /// /// It is redirected from inner TextContainer.Changed event. /// public event RoutedEventHandler PasswordChanged { add => AddHandler(PasswordChangedEvent, value); remove => RemoveHandler(PasswordChangedEvent, value); } public PasswordBox() { _lockUpdatingContents = false; } /// protected override void OnTextChanged(TextChangedEventArgs e) { UpdateTextContents(true); if (_lockUpdatingContents) { base.OnTextChanged(e); } else { if (PlaceholderEnabled && Text.Length > 0) PlaceholderEnabled = false; if (!PlaceholderEnabled && Text.Length < 1) PlaceholderEnabled = true; RevealClearButton(); } } /// /// Is called when property is changing. /// protected virtual void OnPasswordChanged() { UpdateTextContents(false); } /// /// Is called when property is changing. /// protected virtual void OnPasswordCharChanged() { // If password is currently revealed, // do not replace displayed text with asterisks if (IsPasswordRevealed) return; _lockUpdatingContents = true; Text = new String(PasswordChar, Password.Length); _lockUpdatingContents = false; } protected virtual void OnPasswordRevealModeChanged() { _lockUpdatingContents = true; Text = IsPasswordRevealed ? Password : new String(PasswordChar, Password.Length); _lockUpdatingContents = false; } /// /// Triggered by clicking a button in the control template. /// /// Sender of the click event. /// Additional parameters. protected override void OnTemplateButtonClick(string parameter) { base.OnTemplateButtonClick(parameter); #if DEBUG System.Diagnostics.Debug.WriteLine($"INFO: {typeof(PasswordBox)} button clicked with param: {parameter}", "Wpf.Ui.PasswordBox"); #endif switch (parameter) { case "reveal": IsPasswordRevealed = !IsPasswordRevealed; Focus(); CaretIndex = Text.Length; break; } } private void UpdateTextContents(bool isTriggeredByTextInput) { if (_lockUpdatingContents) return; if (IsPasswordRevealed) { if (Password == Text) return; _lockUpdatingContents = true; if (isTriggeredByTextInput) { Password = Text; } else { Text = Password; CaretIndex = Text.Length; } RaiseEvent(new RoutedEventArgs(PasswordChangedEvent)); _lockUpdatingContents = false; return; } var caretIndex = CaretIndex; var selectionIndex = SelectionStart; var currentPassword = Password; var newPasswordValue = currentPassword; if (isTriggeredByTextInput) { var currentText = Text; var newCharacters = currentText.Replace(PasswordChar.ToString(), String.Empty); if (currentText.Length < currentPassword.Length) newPasswordValue = currentPassword.Remove(selectionIndex, currentPassword.Length - currentText.Length); if (newCharacters.Length > 1) { var index = currentText.IndexOf(newCharacters[0]); newPasswordValue = index > newPasswordValue.Length - 1 ? newPasswordValue + newCharacters : newPasswordValue.Insert(index, newCharacters); } else { for (int i = 0; i < currentText.Length; i++) { if (currentText[i] == PasswordChar) continue; newPasswordValue = currentText.Length == newPasswordValue.Length ? newPasswordValue .Remove(i, 1) .Insert(i, currentText[i].ToString()) : newPasswordValue .Insert(i, currentText[i].ToString()); } } } _lockUpdatingContents = true; Text = new String(PasswordChar, newPasswordValue.Length); Password = newPasswordValue; CaretIndex = caretIndex; RaiseEvent(new RoutedEventArgs(PasswordChangedEvent)); _lockUpdatingContents = false; } /// /// Called when is changed. /// private static void OnPasswordPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is not PasswordBox control) return; control.OnPasswordChanged(); } /// /// Called if the character is changed in the during the run. /// private static void OnPasswordCharPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is not PasswordBox control) return; control.OnPasswordCharChanged(); } /// /// Called if the reveal mode is changed in the during the run. /// private static void OnPasswordRevealModePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is not PasswordBox control) return; control.OnPasswordRevealModeChanged(); } }