Why WPF PasswordBox.Password Cannot Be Bound, and Two Ways to Pass It to a ViewModel

PasswordBox.Password is not a dependency property, so binding it throws. Attached property vs. CommandParameter, compared on button state and plaintext left.

Overview

When a sign-in screen is built with MVVM, the first attempt is often to bind PasswordBox.Password to a string on the ViewModel, the same way a TextBox is bound.
XAML written as Password="{Binding Password}", however, throws when it is loaded.
The attached-property workaround can fail too: depending on how it is written, the input never reaches the ViewModel, or the typed characters come out in reverse order.

This article confirms the cause from the exception and the type information, then implements two ways of handing the password to a ViewModel: syncing it with a string through an attached property, and passing the PasswordBox as a CommandParameter.
The two differ in whether the button’s enabled state can follow the input and whether the plaintext password stays in process memory.
The second point was checked by actually scanning the process memory.


Prerequisites / Environment


Problem

The following XAML is meant to show the ViewModel’s Password property in a PasswordBox and send the input back to the ViewModel.

<PasswordBox Password="{Binding Password}" />

Loading this XAML throws XamlParseException.
The message says that a Binding cannot be set on the Password property of PasswordBox, and that a Binding can only be set on a DependencyProperty of a DependencyObject.

case measured
XAML: Password="{Binding Password}" XamlParseException: A 'Binding' cannot be set on the 'Password' property of type 'PasswordBox'. A 'Binding' can only be set on a DependencyProperty of a DependencyObject.
PasswordBox is sealed True
has PasswordProperty / SecurePasswordProperty / implements INotifyPropertyChanged False / False / False

The exception raised by binding Password, and properties of the PasswordBox type. Measured on .NET 10 / Windows 11. The XAML was loaded with XamlReader.Parse, and the first line of the message was read with the UI culture set to en-US.


Cause / Background

PasswordBox has neither a PasswordProperty nor a SecurePasswordProperty, so Password and SecurePassword are not dependency properties (third row of the table; for Password, the exception in the first row shows the same).
PasswordBox does not implement INotifyPropertyChanged either.
Password therefore cannot be a binding target.
When it was used as a binding source, changes made by typing were not propagated either (the CommandParameter table below).
The mechanism provided for observing input changes is the PasswordChanged event (Examples in PasswordBox).

The official documentation does not say why Password was not made a dependency property.
What it does say is that reading Password exposes the password as plain text in memory, and that SecurePassword returns it as a SecureString to avoid that (PasswordBox.Password).
Internally, the password is held in a SecureString field rather than a string (confirmed in the same environment on the PasswordBox demo page).

PasswordBox is sealed (second row of the table), so a derived class cannot add a dependency property.
Passing the value to a ViewModel means adding a mechanism from outside the control.
This article compares two such mechanisms: an attached property and a CommandParameter.


Two Ways to Hand Over the Password

Option 1: Sync it with a string property through an attached property

Attach a BoundPassword property to the PasswordBox and bind it to a string on the ViewModel.
When the user types, PasswordChanged updates BoundPassword; when the ViewModel changes the value, the attached property updates Password.

Depending on how this approach is written, two failures appeared.

case measured
subscribe in the change callback, ViewModel starts with "": type "abc" ViewModel.Password ""
set Password back without comparing: type "abc" PasswordBox "cba" / ViewModel "cba"
no attached property: type "ab", then type "c" (Password not set) PasswordBox "abc"
no attached property: type "ab", set Password = "ab" in code, then type "c" PasswordBox "cab"
article's attached property: type "abc" PasswordBox "abc" / ViewModel "abc"
then the ViewModel sets Password = "" PasswordBox ""
button IsEnabled before / after typing False / True

How the attached property is written, and the result of typing. Measured on .NET 10 / Windows 11. “abc” was typed one character at a time.

The first one subscribes to PasswordChanged inside the change callback of BoundPassword.
A change callback runs when the effective value of the property changes (PropertyChangedCallback).
When the ViewModel starts with an empty string, the value equals the attached property’s default (also an empty string), so the effective value does not change and, by that rule, the callback is not called.
In the measurement, the ViewModel’s Password indeed stayed empty after typing.

The second one sets Password again in the change callback without comparing the values.
Every keystroke updates BoundPassword through PasswordChanged, and its change callback then sets the same value on Password again.
After the same value is set on Password again from code, the next typed character goes to the start (the two “no attached property” rows: without setting it the result is "abc"; setting it again after “ab” and then typing “c” gives "cab").
Because this happens on every keystroke, typing “abc” produced "cba".

The article’s implementation subscribes through a separate Attach property and does not set Password when the value is already the same.

using System.Windows;
using System.Windows.Controls;

public static class PasswordBoxAssist
{
    public static readonly DependencyProperty AttachProperty =
        DependencyProperty.RegisterAttached("Attach", typeof(bool), typeof(PasswordBoxAssist),
            new PropertyMetadata(false, OnAttachChanged));

    public static readonly DependencyProperty BoundPasswordProperty =
        DependencyProperty.RegisterAttached("BoundPassword", typeof(string), typeof(PasswordBoxAssist),
            new FrameworkPropertyMetadata(string.Empty,
                FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnBoundPasswordChanged));

    public static bool GetAttach(DependencyObject d) => (bool)d.GetValue(AttachProperty);
    public static void SetAttach(DependencyObject d, bool value) => d.SetValue(AttachProperty, value);
    public static string GetBoundPassword(DependencyObject d) => (string)d.GetValue(BoundPasswordProperty);
    public static void SetBoundPassword(DependencyObject d, string value) => d.SetValue(BoundPasswordProperty, value);

    private static void OnAttachChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is not PasswordBox box) return;
        box.PasswordChanged -= OnPasswordChanged;
        if ((bool)e.NewValue) box.PasswordChanged += OnPasswordChanged;
    }

    private static void OnBoundPasswordChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is not PasswordBox box) return;
        string value = (string?)e.NewValue ?? string.Empty;

        // Skip setting Password when it already equals the value written back by typing;
        // after Password is set again, the next typed character goes to the start.
        if (box.Password != value) box.Password = value;
    }

    private static void OnPasswordChanged(object sender, RoutedEventArgs e)
    {
        var box = (PasswordBox)sender;
        SetBoundPassword(box, box.Password);
    }
}

The change callback of Attach runs when the value goes from false to true, so the event is subscribed whatever the ViewModel’s initial value is.
BoundPassword is registered with BindsTwoWayByDefault, so the input flows back to the ViewModel without writing Mode=TwoWay in XAML.

The ViewModel holds the password as a string property.
It calls RaiseCanExecuteChanged on every change so that the button can be pressed only when the password is not empty.

public sealed class LoginViewModel : ObservableObject
{
    private string _password = string.Empty;

    public LoginViewModel() => SignInCommand = new RelayCommand(SignIn, () => Password.Length > 0);

    public RelayCommand SignInCommand { get; }

    public string Password
    {
        get => _password;
        set
        {
            if (SetProperty(ref _password, value)) SignInCommand.RaiseCanExecuteChanged();
        }
    }

    private void SignIn()
    {
        // Authenticate here, then clear the value.
        Password = string.Empty;
    }
}

In XAML, set both Attach and BoundPassword.
local maps to the namespace that defines PasswordBoxAssist.

<StackPanel>
  <PasswordBox local:PasswordBoxAssist.Attach="True"
               local:PasswordBoxAssist.BoundPassword="{Binding Password}" />
  <Button Content="Sign in" Command="{Binding SignInCommand}" />
</StackPanel>

With this implementation, “abc” reached the ViewModel unchanged as "abc", and setting Password = "" on the ViewModel also emptied the PasswordBox (the “article’s attached property” row and the row after it).
The button was disabled before typing and became enabled after it (last row of the table).

Option 2: Pass the PasswordBox as a CommandParameter and read SecurePassword when the command runs

This approach keeps the password out of the ViewModel and receives it only when the button is pressed.
Here too, binding SecurePassword or Password did not work as expected.

case measured
CommandParameter="{Binding SecurePassword, ElementName=PasswordInput}": type "abc", then click SecureString.Length 0
CommandParameter="{Binding Password, ElementName=PasswordInput}": type "abc", then click String.Length 0
article's SignInViewModel, CommandParameter="{Binding ElementName=PasswordInput}": type "abc", then click SecureString.Length 3
RelayCommand<PasswordBox> with CanExecute checking SecurePassword.Length > 0: button IsEnabled before / after typing, (CommandParameter type; after RaiseCanExecuteChanged) False / False (PasswordBox; True)

How CommandParameter is written, and what the command received. Measured on .NET 10 / Windows 11. “abc” was typed one character at a time.

With {Binding SecurePassword, ElementName=PasswordInput} or {Binding Password, ElementName=PasswordInput} as the CommandParameter, the value passed to the command had a length of 0 even after typing “abc”.
The value from before the input, an empty one, was passed as is.
PasswordBox does have a PasswordChanged event, but the binding did not read the value again after the input.

Passing the PasswordBox itself and reading SecurePassword when the command runs gave the typed length, 3.

<StackPanel>
  <PasswordBox x:Name="PasswordInput" />
  <Button Content="Sign in"
          Command="{Binding SignInCommand}"
          CommandParameter="{Binding ElementName=PasswordInput}" />
</StackPanel>

The ViewModel exposes a command that takes the PasswordBox and handles the SecureString only inside the execution.
SecurePassword returns a new copy on every read (confirmed on the demo page), so the copy is disposed with using.

using System.Security;
using System.Windows.Controls;

public sealed class SignInViewModel
{
    public SignInViewModel() => SignInCommand = new RelayCommand<PasswordBox>(SignIn);

    public RelayCommand<PasswordBox> SignInCommand { get; }

    private void SignIn(PasswordBox box)
    {
        using SecureString password = box.SecurePassword;

        // Pass password to the authentication code here.
    }
}

With this approach, the ViewModel does not know when the input changes.
Even when the same RelayCommand<PasswordBox> gets a CanExecute that checks SecurePassword.Length > 0, the button stayed disabled both before and after typing (last row of the table).
The CommandParameter had resolved to the PasswordBox, and the button became enabled once the ViewModel called RaiseCanExecuteChanged.
Nothing raises CanExecuteChanged when the user types, so the button keeps the state it had before the input.
For commands wired to CommandManager.RequerySuggested, the related article measured that typing into a TextBox causes a requery.
With such a command and a PasswordBox, the result may differ from the one in this article (the button not following the input); that combination was not measured here.


Decision Criteria


Results by Criterion

Whether the plaintext remains in memory was checked by reading all readable committed memory of the process and searching for the password’s UTF-16 sequence.
A new random password was generated for each case, and the scanning code held it only in masked form and compared it one character at a time, so that the scanner itself never built the sequence.

what the code did found in memory
typed into a PasswordBox only no
read SecurePassword (and disposed it) no
copied SecurePassword to unmanaged memory (before ZeroFreeGlobalAllocUnicode) yes
copied SecurePassword to unmanaged memory, then ZeroFreeGlobalAllocUnicode no
read Password once (string, reference dropped) yes
synced to a ViewModel string with the attached property yes
same, then PasswordBox.Clear() (the ViewModel becomes "") and GC.Collect yes

Whether the UTF-16 sequence of a 12-character password was found in process memory after each operation. Measured on .NET 10 / Windows 11 (64-bit). A new random password was generated for each case, and typed one character at a time with TextComposition. Each row confirmed that all 12 characters arrived (rows with a ViewModel by the length of its string before the operation, the others by the length of SecurePassword after the scan). The scan covered regions that VirtualQuery reported as committed and readable (excluding no-access and guard pages), and searched only for contiguous sequences aligned to 2-byte boundaries. Ranges that failed to read were skipped. ANSI and split fragments were not searched for.

Typing into a PasswordBox, or only reading SecurePassword, left no such sequence.
Copying the SecureString to unmanaged memory with Marshal.SecureStringToGlobalAllocUnicode left the sequence there right after the copy, and none after ZeroFreeGlobalAllocUnicode had cleared it.
Finding it right after the copy shows that the scan reads unmanaged memory as well.

Reading Password once, on the other hand, left the sequence in memory even after the reference was dropped.
It was also found after Option 1 synced the password to a ViewModel string.
It was still found after the synced box was emptied with PasswordBox.Clear() (which also sets the ViewModel to an empty string) and GC.Collect was run.

Criterion Option 1: sync with a string via an attached property Option 2: pass the PasswordBox as a CommandParameter
Button enabled state Followed the input (last row of the attached-property table) Did not follow with the article’s RelayCommand (last row of the CommandParameter table)
Clearing the value from the ViewModel Password = "" also emptied the PasswordBox The ViewModel holds no value
Plaintext sequence left in memory Left once synced, and still left after clearing and GC Not found when only SecurePassword is read
Type the ViewModel depends on string only PasswordBox (WPF)

Choose Option 1 when, with the article’s RelayCommand, the button must be enabled by the input, or when the ViewModel must be able to clear the value.
In exchange, a plaintext string stays in the process from the moment the user types.

Choose Option 2 to reduce how often the plaintext becomes a string.
With the article’s RelayCommand, the design then does not check the input in CanExecute and rejects an empty password inside the command.


Notes


Summary

PasswordBox.Password is not a dependency property, so binding it in XAML throws XamlParseException.
PasswordBox is sealed, so a mechanism has to be added from outside; this article compared an attached property and a CommandParameter for that purpose.

Use the Option 1 attached property on screens where the ViewModel must empty the field, or where the button’s enabled state must follow the input with a command whose CanExecuteChanged is raised by the ViewModel.
Subscribe through a dedicated Attach property, and set Password only when the value differs.
On screens that should reduce how often a plaintext string is created, use Option 2: pass the PasswordBox and read and dispose of SecurePassword inside the command.
With either option, the plaintext reaches memory once the authentication API requires a string, so when keeping the password out of memory is a hard requirement, consider handling it as credentials stored outside the process.