I try to bind the SecurePassword
property of a PasswordBox
to my ViewModel
with a custom Behavior
. Sadly it doesn't work properly.
Basically I added a property to the Behavior
which contains the target property of my ViewModel
.
Any ideas why it doesn't work?
PS: I am currently on the way home without my laptop, I gonna update the question with my code in about 15 minutes. But would be nice if someone would post ideas or sth.
EDIT
As I promised, here is some code :)
The Behavior
first:
using System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using System.Windows.Interactivity;
using System.Security;
namespace Knerd.Behaviors {
public class PasswordChangedBehavior : Behavior<PasswordBox> {
protected override void OnAttached() {
AssociatedObject.PasswordChanged += AssociatedObject_PasswordChanged;
base.OnAttached();
}
private void AssociatedObject_PasswordChanged(object sender, RoutedEventArgs e) {
if (AssociatedObject.Password != null)
TargetPassword = AssociatedObject.SecurePassword;
}
protected override void OnDetaching() {
AssociatedObject.PasswordChanged -= AssociatedObject_PasswordChanged;
base.OnDetaching();
}
public SecureString TargetPassword {
get { return (SecureString)GetValue(TargetPasswordProperty); }
set { SetValue(TargetPasswordProperty, value); }
}
// Using a DependencyProperty as the backing store for TargetPassword. This enables animation, styling, binding, etc...
public static readonly DependencyProperty TargetPasswordProperty = DependencyProperty.Register("TargetPassword", typeof(SecureString), typeof(PasswordChangedBehavior), new PropertyMetadata(default(SecureString)));
}
}
The PasswordBox
:
<PasswordBox Grid.Column="1" Grid.Row="1" Margin="5" Width="300" MinWidth="200">
<i:Interaction.Behaviors>
<behaviors:PasswordChangedBehavior TargetPassword="{Binding Password}" />
</i:Interaction.Behaviors>
</PasswordBox>
And last, the part of my ViewModel
.
private SecureString password;
public SecureString Password {
get { return password; }
set {
if (password != value) {
password = value;
OnPropertyChanged("Password");
}
}
}
I hope anyone can help, atm I use the codebehind version but I rather wouldn't.
EDIT 2
What actually doesn't work is, that the TargetPassword
property doesn't update the property of my ViewModel
Create an attach property
public static class PasswordBoxAssistant
{
public static readonly DependencyProperty BoundPassword =
DependencyProperty.RegisterAttached("BoundPassword", typeof(string), typeof(PasswordBoxAssistant), new PropertyMetadata(string.Empty, OnBoundPasswordChanged));
public static readonly DependencyProperty BindPassword = DependencyProperty.RegisterAttached(
"BindPassword", typeof (bool), typeof (PasswordBoxAssistant), new PropertyMetadata(false, OnBindPasswordChanged));
private static readonly DependencyProperty UpdatingPassword =
DependencyProperty.RegisterAttached("UpdatingPassword", typeof(bool), typeof(PasswordBoxAssistant), new PropertyMetadata(false));
private static void OnBoundPasswordChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
PasswordBox box = d as PasswordBox;
// only handle this event when the property is attached to a PasswordBox
// and when the BindPassword attached property has been set to true
if (d == null || !GetBindPassword(d))
{
return;
}
// avoid recursive updating by ignoring the box's changed event
box.PasswordChanged -= HandlePasswordChanged;
string newPassword = (string)e.NewValue;
if (!GetUpdatingPassword(box))
{
box.Password = newPassword;
}
box.PasswordChanged += HandlePasswordChanged;
}
private static void OnBindPasswordChanged(DependencyObject dp, DependencyPropertyChangedEventArgs e)
{
// when the BindPassword attached property is set on a PasswordBox,
// start listening to its PasswordChanged event
PasswordBox box = dp as PasswordBox;
if (box == null)
{
return;
}
bool wasBound = (bool)(e.OldValue);
bool needToBind = (bool)(e.NewValue);
if (wasBound)
{
box.PasswordChanged -= HandlePasswordChanged;
}
if (needToBind)
{
box.PasswordChanged += HandlePasswordChanged;
}
}
private static void HandlePasswordChanged(object sender, RoutedEventArgs e)
{
PasswordBox box = sender as PasswordBox;
// set a flag to indicate that we're updating the password
SetUpdatingPassword(box, true);
// push the new password into the BoundPassword property
SetBoundPassword(box, box.Password);
SetUpdatingPassword(box, false);
}
public static void SetBindPassword(DependencyObject dp, bool value)
{
dp.SetValue(BindPassword, value);
}
public static bool GetBindPassword(DependencyObject dp)
{
return (bool)dp.GetValue(BindPassword);
}
public static string GetBoundPassword(DependencyObject dp)
{
return (string)dp.GetValue(BoundPassword);
}
public static void SetBoundPassword(DependencyObject dp, string value)
{
dp.SetValue(BoundPassword, value);
}
private static bool GetUpdatingPassword(DependencyObject dp)
{
return (bool)dp.GetValue(UpdatingPassword);
}
private static void SetUpdatingPassword(DependencyObject dp, bool value)
{
dp.SetValue(UpdatingPassword, value);
}
}
And in your XAML
<Page xmlns:ff="clr-namespace:FunctionalFun.UI">
<!-- [Snip] -->
<PasswordBox x:Name="PasswordBox"
ff:PasswordBoxAssistant.BindPassword="true" ff:PasswordBoxAssistant.BoundPassword="{Binding Path=Password, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
</Page>
You probably don't want to do this anyway but if you really want to go ahead.
The reason the WPF/Silverlight PasswordBox doesn't expose a DP for the Password property is security related.
If WPF/Silverlight were to keep a DP for Password it would require the framework to keep the password itself unencrypted in memory. Which is considered quite a troublesome security attack vector. The PasswordBox uses encrypted memory (of sorts) and the only way to access the password is through the CLR property.
I would suggest that when accessing the PasswordBox.Password CLR property you'd refrain from placing it in any variable or as a value for any property.
Keeping your password in plain text on the client machine RAM is a security no-no.
SecurePassword
cannot be done with bindings.
.NET documentation explains why the PasswordBox was not made bindable in the first place.
An alternative solution is to put the PasswordBox
in your ViewModel
public class LoginViewModel
public class LoginViewModel
{
// other properties here
public PasswordBox Password
{
get { return m_passwordBox; }
}
// Executed when the Login button is clicked.
private void LoginExecute()
{
var password = Password.SecurePassword;
// do more stuff...
}
}
Yes, you are violating ViewModel best practices here, but
- best practices are "recommendations that work well in most cases"
rather than strict rules and
- writing simple, easy-to-read, maintainable code and avoiding
unnecessary complexity is also one of those "best practice" rules
(which might be violated slightly by the "attached property"
workaround).
I think I found a kinda weird solution. Please improve if there is sth to improve :)
I just changed it like so:
The Behavior
:
using System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using System.Windows.Interactivity;
using System.Security;
namespace Knerd.Behaviors {
public class PasswordChangedBehavior : Behavior<PasswordBox> {
protected override void OnAttached() {
AssociatedObject.PasswordChanged += AssociatedObject_PasswordChanged;
base.OnAttached();
}
private void AssociatedObject_PasswordChanged(object sender, RoutedEventArgs e) {
if (AssociatedObject.SecurePassword != null)
AssociatedObject.DataContext = AssociatedObject.SecurePassword.Copy();
}
protected override void OnDetaching() {
AssociatedObject.PasswordChanged -= AssociatedObject_PasswordChanged;
base.OnDetaching();
}
// Using a DependencyProperty as the backing store for TargetPassword. This enables animation, styling, binding, etc...
public static readonly DependencyProperty TargetPasswordProperty = DependencyProperty.Register("TargetPassword", typeof(SecureString), typeof(PasswordChangedBehavior), new PropertyMetadata(default(SecureString)));
}
}
The ViewModel
didn't change at all, but here is my View
:
<PasswordBox Grid.Column="1" Grid.Row="1" Margin="5" Width="300" MinWidth="200" DataContext="{Binding Password, Mode=TwoWay}">
<i:Interaction.Behaviors>
<behaviors:PasswordChangedBehavior />
</i:Interaction.Behaviors>
</PasswordBox>
That works just perfect, without exposing the plaintext password.