PasswordBox.cs 9.2 KB
Newer Older
P
pomianowski 已提交
1 2 3 4 5 6 7 8 9
// 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;

10
namespace Wpf.Ui.Controls;
P
pomianowski 已提交
11 12 13

// TODO: This is an initial implementation and requires the necessary corrections, tests and adjustments.

14 15 16 17 18
/**
 * TextProperty contains asterisks OR raw password if IsPasswordRevealed is set to true
 * PasswordProperty always contains raw password
 */

P
pomianowski 已提交
19 20 21
/// <summary>
/// The modified password control.
/// </summary>
22
public class PasswordBox : Wpf.Ui.Controls.TextBox
P
pomianowski 已提交
23
{
P
pomianowski 已提交
24
    private bool _lockUpdatingContents;
P
pomianowski 已提交
25 26 27 28 29

    /// <summary>
    /// Property for <see cref="Password"/>.
    /// </summary>
    public static readonly DependencyProperty PasswordProperty = DependencyProperty.Register(nameof(Password),
30
        typeof(string), typeof(PasswordBox), new PropertyMetadata(String.Empty, OnPasswordPropertyChanged));
P
pomianowski 已提交
31 32 33 34 35

    /// <summary>
    /// Property for <see cref="PasswordChar"/>.
    /// </summary>
    public static readonly DependencyProperty PasswordCharProperty = DependencyProperty.Register(nameof(PasswordChar),
36
        typeof(char), typeof(PasswordBox), new PropertyMetadata('*', OnPasswordCharPropertyChanged));
P
pomianowski 已提交
37 38

    /// <summary>
39
    /// Property for <see cref="IsPasswordRevealed"/>.
P
pomianowski 已提交
40
    /// </summary>
41
    public static readonly DependencyProperty IsPasswordRevealedProperty = DependencyProperty.Register(nameof(IsPasswordRevealed),
42
        typeof(bool), typeof(PasswordBox), new PropertyMetadata(false, OnPasswordRevealModePropertyChanged));
P
pomianowski 已提交
43 44

    /// <summary>
45
    /// Property for <see cref="RevealButtonEnabled"/>.
P
pomianowski 已提交
46
    /// </summary>
47
    public static readonly DependencyProperty RevealButtonEnabledProperty = DependencyProperty.Register(nameof(RevealButtonEnabled),
P
pomianowski 已提交
48 49
        typeof(bool), typeof(PasswordBox), new PropertyMetadata(true));

50 51 52 53 54 55 56 57 58
    /// <summary>
    /// Event for "Password has changed"
    /// </summary>
    public static readonly RoutedEvent PasswordChangedEvent = EventManager.RegisterRoutedEvent(
        nameof(PasswordChanged),
        RoutingStrategy.Bubble,
        typeof(RoutedEventHandler),
        typeof(PasswordBox));

P
pomianowski 已提交
59 60 61 62 63 64
    /// <summary>
    /// Gets or sets currently typed text represented by asterisks.
    /// </summary>
    public string Password
    {
        get => (string)GetValue(PasswordProperty);
65
        set => SetValue(PasswordProperty, value);
P
pomianowski 已提交
66 67 68 69 70 71 72 73 74 75 76 77
    }

    /// <summary>
    /// Gets or sets character used to mask the password.
    /// </summary>
    public char PasswordChar
    {
        get => (char)GetValue(PasswordCharProperty);
        set => SetValue(PasswordCharProperty, value);
    }

    /// <summary>
78
    /// Gets a value indicating whether the password is revealed.
P
pomianowski 已提交
79
    /// </summary>
80
    public bool IsPasswordRevealed
P
pomianowski 已提交
81
    {
82 83
        get => (bool)GetValue(IsPasswordRevealedProperty);
        private set => SetValue(IsPasswordRevealedProperty, value);
P
pomianowski 已提交
84 85 86 87 88
    }

    /// <summary>
    /// Gets or sets a value deciding whether to display the reveal password button.
    /// </summary>
89
    public bool RevealButtonEnabled
P
pomianowski 已提交
90
    {
91 92
        get => (bool)GetValue(RevealButtonEnabledProperty);
        set => SetValue(RevealButtonEnabledProperty, value);
P
pomianowski 已提交
93 94 95
    }

    /// <summary>
96 97
    /// Event fired from this text box when its inner content
    /// has been changed.
P
pomianowski 已提交
98
    /// </summary>
99 100 101 102
    /// <remarks>
    /// It is redirected from inner TextContainer.Changed event.
    /// </remarks>
    public event RoutedEventHandler PasswordChanged
P
pomianowski 已提交
103
    {
104 105
        add => AddHandler(PasswordChangedEvent, value);
        remove => RemoveHandler(PasswordChangedEvent, value);
P
pomianowski 已提交
106 107
    }

P
pomianowski 已提交
108
    public PasswordBox()
P
pomianowski 已提交
109
    {
P
pomianowski 已提交
110
        _lockUpdatingContents = false;
111
    }
P
pomianowski 已提交
112

113 114 115 116
    /// <inheritdoc />
    protected override void OnTextChanged(TextChangedEventArgs e)
    {
        UpdateTextContents(true);
P
pomianowski 已提交
117

118
        if (_lockUpdatingContents)
P
pomianowski 已提交
119
        {
P
pomianowski 已提交
120
            base.OnTextChanged(e);
P
pomianowski 已提交
121 122 123 124 125 126 127 128 129 130 131
        }
        else
        {
            if (PlaceholderEnabled && Text.Length > 0)
                PlaceholderEnabled = false;

            if (!PlaceholderEnabled && Text.Length < 1)
                PlaceholderEnabled = true;

            RevealClearButton();
        }
132
    }
P
pomianowski 已提交
133

134 135 136 137 138 139
    /// <summary>
    /// Is called when <see cref="Password"/> property is changing.
    /// </summary>
    protected virtual void OnPasswordChanged()
    {
        UpdateTextContents(false);
P
pomianowski 已提交
140 141 142
    }

    /// <summary>
143
    /// Is called when <see cref="PasswordChar"/> property is changing.
P
pomianowski 已提交
144
    /// </summary>
145
    protected virtual void OnPasswordCharChanged()
P
pomianowski 已提交
146
    {
147 148
        // If password is currently revealed,
        // do not replace displayed text with asterisks
149
        if (IsPasswordRevealed)
P
pomianowski 已提交
150
            return;
151

152 153
        _lockUpdatingContents = true;

154
        Text = new String(PasswordChar, Password.Length);
155 156

        _lockUpdatingContents = false;
P
pomianowski 已提交
157 158
    }

159
    protected virtual void OnPasswordRevealModeChanged()
P
pomianowski 已提交
160
    {
161 162
        _lockUpdatingContents = true;

163
        Text = IsPasswordRevealed ? Password : new String(PasswordChar, Password.Length);
164 165

        _lockUpdatingContents = false;
P
pomianowski 已提交
166 167
    }

168 169 170 171 172
    /// <summary>
    /// Triggered by clicking a button in the control template.
    /// </summary>
    /// <param name="sender">Sender of the click event.</param>
    /// <param name="parameter">Additional parameters.</param>
P
pomianowski 已提交
173
    protected override void OnTemplateButtonClick(string parameter)
P
pomianowski 已提交
174
    {
P
pomianowski 已提交
175
        base.OnTemplateButtonClick(parameter);
P
pomianowski 已提交
176 177

#if DEBUG
P
pomianowski 已提交
178
        System.Diagnostics.Debug.WriteLine($"INFO: {typeof(PasswordBox)} button clicked with param: {parameter}", "Wpf.Ui.PasswordBox");
P
pomianowski 已提交
179 180
#endif

P
pomianowski 已提交
181
        switch (parameter)
P
pomianowski 已提交
182 183
        {
            case "reveal":
184
                IsPasswordRevealed = !IsPasswordRevealed;
185 186 187
                Focus();
                CaretIndex = Text.Length;

P
pomianowski 已提交
188 189 190 191
                break;
        }
    }

192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269
    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;
    }

P
pomianowski 已提交
270
    /// <summary>
271
    /// Called when <see cref="Password"/> is changed.
P
pomianowski 已提交
272
    /// </summary>
273
    private static void OnPasswordPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
P
pomianowski 已提交
274 275 276
    {
        if (d is not PasswordBox control)
            return;
277 278 279 280 281 282 283 284 285 286 287 288 289

        control.OnPasswordChanged();
    }

    /// <summary>
    /// Called if the character is changed in the during the run.
    /// </summary>
    private static void OnPasswordCharPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is not PasswordBox control)
            return;

        control.OnPasswordCharChanged();
P
pomianowski 已提交
290 291 292
    }

    /// <summary>
293
    /// Called if the reveal mode is changed in the during the run.
P
pomianowski 已提交
294
    /// </summary>
295
    private static void OnPasswordRevealModePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
P
pomianowski 已提交
296 297 298
    {
        if (d is not PasswordBox control)
            return;
299

300
        control.OnPasswordRevealModeChanged();
P
pomianowski 已提交
301 302
    }
}