diff --git a/src/Wpf.Ui/Controls/PasswordBox/PasswordBox.cs b/src/Wpf.Ui/Controls/PasswordBox/PasswordBox.cs index e802d3796..780ace8a6 100644 --- a/src/Wpf.Ui/Controls/PasswordBox/PasswordBox.cs +++ b/src/Wpf.Ui/Controls/PasswordBox/PasswordBox.cs @@ -3,11 +3,6 @@ // Copyright (C) Leszek Pomianowski and WPF UI Contributors. // All Rights Reserved. -// 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 */ - using System.Windows.Controls; // ReSharper disable once CheckNamespace @@ -16,12 +11,14 @@ namespace Wpf.Ui.Controls; /// /// The modified password control. /// -public partial class PasswordBox : Wpf.Ui.Controls.TextBox +public partial class PasswordBox : TextBox { private readonly PasswordHelper _passwordHelper; - private bool _lockUpdatingContents; + private bool _isUpdating; - /// Identifies the dependency property. + /// + /// Identifies the dependency property. + /// public static readonly DependencyProperty PasswordProperty = DependencyProperty.Register( nameof(Password), typeof(string), @@ -29,7 +26,9 @@ public partial class PasswordBox : Wpf.Ui.Controls.TextBox new PropertyMetadata(string.Empty, OnPasswordChanged) ); - /// Identifies the dependency property. + /// + /// Identifies the dependency property. + /// public static readonly DependencyProperty PasswordCharProperty = DependencyProperty.Register( nameof(PasswordChar), typeof(char), @@ -37,7 +36,9 @@ public partial class PasswordBox : Wpf.Ui.Controls.TextBox new PropertyMetadata('*', OnPasswordCharChanged) ); - /// Identifies the dependency property. + /// + /// Identifies the dependency property. + /// public static readonly DependencyProperty IsPasswordRevealedProperty = DependencyProperty.Register( nameof(IsPasswordRevealed), typeof(bool), @@ -45,7 +46,9 @@ public partial class PasswordBox : Wpf.Ui.Controls.TextBox new PropertyMetadata(false, OnIsPasswordRevealedChanged) ); - /// Identifies the dependency property. + /// + /// Identifies the dependency property. + /// public static readonly DependencyProperty RevealButtonEnabledProperty = DependencyProperty.Register( nameof(RevealButtonEnabled), typeof(bool), @@ -53,7 +56,9 @@ public partial class PasswordBox : Wpf.Ui.Controls.TextBox new PropertyMetadata(true) ); - /// Identifies the routed event. + /// + /// Identifies the routed event. + /// public static readonly RoutedEvent PasswordChangedEvent = EventManager.RegisterRoutedEvent( nameof(PasswordChanged), RoutingStrategy.Bubble, @@ -62,7 +67,15 @@ public partial class PasswordBox : Wpf.Ui.Controls.TextBox ); /// - /// Gets or sets currently typed text represented by asterisks. + /// Initializes a new instance of the class. + /// + public PasswordBox() + { + _passwordHelper = new PasswordHelper(this); + } + + /// + /// Gets or sets the actual password (not asterisks). /// public string Password { @@ -71,7 +84,7 @@ public string Password } /// - /// Gets or sets character used to mask the password. + /// Gets or sets the character used to mask the password. /// public char PasswordChar { @@ -80,7 +93,7 @@ public char PasswordChar } /// - /// Gets a value indicating whether the password is revealed. + /// Gets a value indicating whether the password is currently revealed. /// public bool IsPasswordRevealed { @@ -89,7 +102,7 @@ public bool IsPasswordRevealed } /// - /// Gets or sets a value indicating whether to display the password reveal button. + /// Gets or sets whether the password reveal button is enabled. /// public bool RevealButtonEnabled { @@ -98,120 +111,101 @@ public bool RevealButtonEnabled } /// - /// Event fired from this text box when its inner content - /// has been changed. + /// Occurs when the password content changes. /// - /// - /// It is redirected from inner TextContainer.Changed event. - /// public event RoutedEventHandler PasswordChanged { add => AddHandler(PasswordChangedEvent, value); remove => RemoveHandler(PasswordChangedEvent, value); } - public PasswordBox() - { - _lockUpdatingContents = false; - _passwordHelper = new PasswordHelper(this); - } - - /// + /// protected override void OnTextChanged(TextChangedEventArgs e) { - UpdateTextContents(true); + UpdateTextContents(isTriggeredByTextInput: true); - if (_lockUpdatingContents) + if (!_isUpdating) { base.OnTextChanged(e); - } - else - { SetPlaceholderTextVisibility(); - RevealClearButton(); } } /// - /// Is called when property is changing. + /// Called when the property changes. /// - protected virtual void OnPasswordChanged() - { - UpdateTextContents(false); - } + protected virtual void OnPasswordChanged() => UpdateTextContents(isTriggeredByTextInput: false); /// - /// Is called when property is changing. + /// Called when the property changes. /// protected virtual void OnPasswordCharChanged() { - // If password is currently revealed, - // do not replace displayed text with asterisks if (IsPasswordRevealed) { return; } - _lockUpdatingContents = true; - - SetCurrentValue(TextProperty, new string(PasswordChar, Password.Length)); - - _lockUpdatingContents = false; + UpdateWithLock(() => SetCurrentValue(TextProperty, new string(PasswordChar, Password.Length))); } - protected virtual void OnPasswordRevealModeChanged() + /// + /// Called when the property changes. + /// + protected virtual void OnIsPasswordRevealedChanged() { - _lockUpdatingContents = true; - - SetCurrentValue( - TextProperty, - IsPasswordRevealed ? Password : new string(PasswordChar, Password.Length) - ); - - _lockUpdatingContents = false; + UpdateWithLock(() => SetCurrentValue(TextProperty, IsPasswordRevealed ? Password : new string(PasswordChar, Password.Length))); } - /// - /// Triggered by clicking a button in the control template. - /// - /// Additional parameters. + /// protected override void OnTemplateButtonClick(string? parameter) { - System.Diagnostics.Debug.WriteLine( - $"INFO: {typeof(PasswordBox)} button clicked with param: {parameter}", - "Wpf.Ui.PasswordBox" - ); - - switch (parameter) + if (parameter == "reveal") { - case "reveal": - SetCurrentValue(IsPasswordRevealedProperty, !IsPasswordRevealed); - _ = Focus(); - CaretIndex = Text.Length; - break; - default: - base.OnTemplateButtonClick(parameter); - break; + SetCurrentValue(IsPasswordRevealedProperty, !IsPasswordRevealed); + _ = Focus(); + CaretIndex = Text.Length; + } + else + { + base.OnTemplateButtonClick(parameter); } } + /// + /// Updates the text contents based on the current state. + /// + /// True if triggered by user text input; false if triggered by property change. private void UpdateTextContents(bool isTriggeredByTextInput) { - if (_lockUpdatingContents) + if (_isUpdating) { return; } if (IsPasswordRevealed) { - if (Password == Text) - { - return; - } + HandleRevealedModeUpdate(isTriggeredByTextInput); + return; + } - _lockUpdatingContents = true; + HandleHiddenModeUpdate(isTriggeredByTextInput); + } + /// + /// Handles updates when password is in revealed mode. + /// + /// True if triggered by user text input. + private void HandleRevealedModeUpdate(bool isTriggeredByTextInput) + { + if (Password == Text) + { + return; + } + + UpdateWithLock(() => + { if (isTriggeredByTextInput) { SetCurrentValue(PasswordProperty, Text); @@ -222,68 +216,80 @@ private void UpdateTextContents(bool isTriggeredByTextInput) CaretIndex = Text.Length; } - RaiseEvent(new RoutedEventArgs(PasswordChangedEvent)); - - _lockUpdatingContents = false; - - return; - } + RaisePasswordChangedEvent(); + }); + } + /// + /// Handles updates when password is in hidden mode. + /// + /// True if triggered by user text input. + private void HandleHiddenModeUpdate(bool isTriggeredByTextInput) + { var caretIndex = CaretIndex; - var newPasswordValue = _passwordHelper.GetPassword(); + var newPassword = isTriggeredByTextInput ? _passwordHelper.GetNewPassword() : Password; - if (isTriggeredByTextInput) + UpdateWithLock(() => { - newPasswordValue = _passwordHelper.GetNewPassword(); - } - - _lockUpdatingContents = true; - - SetCurrentValue(TextProperty, new string(PasswordChar, newPasswordValue.Length)); - SetCurrentValue(PasswordProperty, newPasswordValue); - CaretIndex = caretIndex; - - RaiseEvent(new RoutedEventArgs(PasswordChangedEvent)); + SetCurrentValue(TextProperty, new string(PasswordChar, newPassword.Length)); + SetCurrentValue(PasswordProperty, newPassword); + CaretIndex = caretIndex; + RaisePasswordChangedEvent(); + }); + } - _lockUpdatingContents = false; + /// + /// Executes an action while preventing recursive updates. + /// + /// The action to execute. + private void UpdateWithLock(Action updateAction) + { + _isUpdating = true; + updateAction(); + _isUpdating = false; } /// - /// Called when is changed. + /// Raises the . /// - private static void OnPasswordChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + private void RaisePasswordChangedEvent() => RaiseEvent(new RoutedEventArgs(PasswordChangedEvent)); + + /// + /// Handles changes to the dependency property. + /// + /// The that raised the event. + /// The containing the event data. + private static void OnPasswordChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e) { - if (d is not PasswordBox control) + if (dependencyObject is PasswordBox passwodBox) { - return; + passwodBox.OnPasswordChanged(); } - - control.OnPasswordChanged(); } /// - /// Called if the character is changed in the during the run. + /// Handles changes to the dependency property. /// - private static void OnPasswordCharChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + /// The instance where the change occurred. + /// Event data that contains information about the property change. + private static void OnPasswordCharChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e) { - if (d is not PasswordBox control) + if (dependencyObject is PasswordBox passwodBox) { - return; + passwodBox.OnPasswordCharChanged(); } - - control.OnPasswordCharChanged(); } /// - /// Called if the reveal mode is changed in the during the run. + /// Handles changes to the dependency property. /// - private static void OnIsPasswordRevealedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + /// The instance where the property changed. + /// The containing the old and new values. + private static void OnIsPasswordRevealedChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e) { - if (d is not PasswordBox control) + if (dependencyObject is PasswordBox passwodBox) { - return; + passwodBox.OnIsPasswordRevealedChanged(); } - - control.OnPasswordRevealModeChanged(); } -} +} \ No newline at end of file diff --git a/src/Wpf.Ui/Controls/PasswordBox/PasswordBox.xaml b/src/Wpf.Ui/Controls/PasswordBox/PasswordBox.xaml index 5e4b52517..b4a266e27 100644 --- a/src/Wpf.Ui/Controls/PasswordBox/PasswordBox.xaml +++ b/src/Wpf.Ui/Controls/PasswordBox/PasswordBox.xaml @@ -14,6 +14,21 @@ xmlns:controls="clr-namespace:Wpf.Ui.Controls" xmlns:system="clr-namespace:System;assembly=mscorlib"> + + + + + + 1,1,1,1 0,0,0,1 10,8,0,0 @@ -205,11 +220,8 @@ CommandParameter="clear" Cursor="Arrow" Foreground="{DynamicResource TextControlButtonForeground}" - IsTabStop="False"> - - - - + Icon="{StaticResource PasswordBoxClearIcon}" + IsTabStop="False" /> - - - - + Icon="{StaticResource PasswordBoxRevealIcon}" + IsTabStop="False" /> - - - - - + + diff --git a/src/Wpf.Ui/Controls/PasswordBox/PasswordHelper.cs b/src/Wpf.Ui/Controls/PasswordBox/PasswordHelper.cs index 64ed3fa0d..87340821a 100644 --- a/src/Wpf.Ui/Controls/PasswordBox/PasswordHelper.cs +++ b/src/Wpf.Ui/Controls/PasswordBox/PasswordHelper.cs @@ -12,107 +12,116 @@ namespace Wpf.Ui.Controls; public partial class PasswordBox { + /// + /// Helper class for managing password operations in . + /// private class PasswordHelper { private readonly PasswordBox _passwordBox; private string _currentText; - private string _newPasswordValue; + private string _newPassword; private string _currentPassword; + /// + /// Initializes a new instance of the class. + /// + /// The parent control. public PasswordHelper(PasswordBox passwordBox) { _passwordBox = passwordBox; _currentText = string.Empty; - _newPasswordValue = string.Empty; + _newPassword = string.Empty; _currentPassword = string.Empty; } + /// + /// Calculates and returns the new password value based on current input. + /// + /// The updated password string. + /// + /// Handles three scenarios: + /// 1. When text is being deleted + /// 2. When password is revealed (plain text mode) + /// 3. When password is hidden (masked character mode) + /// public string GetNewPassword() { - _currentPassword = GetPassword(); - _newPasswordValue = _currentPassword; + _currentPassword = _passwordBox.Password; + _newPassword = _currentPassword; _currentText = _passwordBox.Text; - var selectionIndex = _passwordBox.SelectionStart; - var passwordChar = _passwordBox.PasswordChar; - var newCharacters = _currentText.Replace(passwordChar.ToString(), string.Empty); - bool isDeleted = false; + int selectionIndex = _passwordBox.SelectionStart; - if (IsDeleteOption()) + if (IsDeletingText()) { - _newPasswordValue = _currentPassword.Remove( - selectionIndex, - _currentPassword.Length - _currentText.Length - ); - isDeleted = true; + int charsToRemove = _currentPassword.Length - _currentText.Length; + _newPassword = _currentPassword.Remove(selectionIndex, charsToRemove); + return _newPassword; } - switch (newCharacters.Length) + if (_passwordBox.IsPasswordRevealed) { - case > 1: - { - var index = _currentText.IndexOf(newCharacters[0]); - - _newPasswordValue = - index > _newPasswordValue.Length - 1 - ? _newPasswordValue + newCharacters - : _newPasswordValue.Insert(index, newCharacters); - break; - } - - case 1: - { - for (int i = 0; i < _currentText.Length; i++) - { - if (_currentText[i] == passwordChar) - { - continue; - } - - UpdatePasswordWithInputCharacter(i, _currentText[i].ToString()); - break; - } - - break; - } - - case 0 when !isDeleted: - { - // The input is a PasswordChar, which is to be inserted at the designated position. - int insertIndex = selectionIndex - 1; - UpdatePasswordWithInputCharacter(insertIndex, passwordChar.ToString()); - break; - } + _newPassword = _currentText; + return _newPassword; } - return _newPasswordValue; + return HandleHiddenModeChanges(selectionIndex); } - private void UpdatePasswordWithInputCharacter(int insertIndex, string insertValue) + /// + /// Handles password changes when in hidden (masked) mode. + /// + /// Current caret position in the text box. + /// The updated password string. + /// + /// Manages three cases: + /// 1. Characters were inserted + /// 2. Character was replaced (overwrite) + /// 3. Characters were removed + /// + private string HandleHiddenModeChanges(int selectionIndex) { - Debug.Assert(_currentText == _passwordBox.Text, "_currentText == _passwordBox.Text"); + char passwordChar = _passwordBox.PasswordChar; + int currentLength = _currentPassword.Length; - if (_currentText.Length == _newPasswordValue.Length) + if (_currentText.Length > currentLength) { - // If it's a direct character replacement, remove the existing one before inserting the new one. - _newPasswordValue = _newPasswordValue.Remove(insertIndex, 1).Insert(insertIndex, insertValue); + // Characters were inserted + int insertedCount = _currentText.Length - currentLength; + string insertedText = _currentText.Substring(selectionIndex - insertedCount, insertedCount); + _newPassword = _currentPassword.Insert(selectionIndex - insertedCount, insertedText); } - else + else if (_currentText.Length == currentLength) { - _newPasswordValue = _newPasswordValue.Insert(insertIndex, insertValue); + // Character was replaced (overwrite) + for (int i = 0; i < _currentText.Length; i++) + { + if (_currentText[i] != passwordChar && i < _newPassword.Length) + { + _newPassword = _newPassword.Remove(i, 1).Insert(i, _currentText[i].ToString()); + break; + } + } + } + else if (_currentText.Length < currentLength) + { + // Characters were removed (fallback) + int removedCount = currentLength - _currentText.Length; + _newPassword = _currentPassword.Remove(selectionIndex, removedCount); } + + return _newPassword; } - private bool IsDeleteOption() + /// + /// Determines if the current operation is deleting text. + /// + /// True if text is being deleted; otherwise, false. + private bool IsDeletingText() { - Debug.Assert(_currentText == _passwordBox.Text, "_currentText == _passwordBox.Text"); - Debug.Assert( - _currentPassword == _passwordBox.Password, - "_currentPassword == _passwordBox.Password" - ); + Debug.Assert(_currentText == _passwordBox.Text, "Text mismatch"); + Debug.Assert(_currentPassword == _passwordBox.Password, "Password mismatch"); return _currentText.Length < _currentPassword.Length; } - - public string GetPassword() => _passwordBox.Password ?? string.Empty; } }