如何绑定到MVVM中的PasswordBox

mar*_*ith 243 c# passwords wpf mvvm wpf-controls

我遇到了绑定到PasswordBox的问题.这似乎是一个安全风险,但我正在使用MVVM模式,所以我希望绕过这个.我在这里找到了一些有趣的代码(有没有人用过这个或类似的东西?)

http://www.wpftutorial.net/PasswordBox.html

它在技术上看起来很棒,但我不确定如何检索密码.

我基本上有我的LoginViewModelfor Username和属性Password.Username很好,正在工作,因为它是一个TextBox.

我按照说明使用了上面的代码并输入了这个

<PasswordBox ff:PasswordHelper.Attach="True"
    ff:PasswordHelper.Password="{Binding Path=Password}" Width="130"/>
Run Code Online (Sandbox Code Playgroud)

当我有了PasswordBoxa TextBox,Binding Path=Password然后我的房产LoginViewModel被更新了.

我的代码很简单,基本上我有一个Commandfor my Button.当我按下它时会CanLogin被调用,如果它返回true,则调用它Login.
你可以看到我检查我的房产在Username这里工作得很好.

Login我一起发送到我的服务UsernamePassword,Username包含数据从我ViewPasswordNull|Empty

private DelegateCommand loginCommand;

    public string Username { get; set; }
    public string Password { get; set; }


    public ICommand LoginCommand
    {
        get
        {
            if (loginCommand == null)
            {
                loginCommand = new DelegateCommand(
                    Login, CanLogin );
            }
            return loginCommand;
        }
    }

    private bool CanLogin()
    {
        return !string.IsNullOrEmpty(Username);
    }

    private void Login()
    {
        bool result = securityService.IsValidLogin(Username, Password);

        if (result) { }
        else { }
    }
Run Code Online (Sandbox Code Playgroud)

这就是我在做的事情

<TextBox Text="{Binding Path=Username, UpdateSourceTrigger=PropertyChanged}"
         MinWidth="180" />

<PasswordBox ff:PasswordHelper.Attach="True" 
             ff:PasswordHelper.Password="{Binding Path=Password}" Width="130"/>
Run Code Online (Sandbox Code Playgroud)

我有我的TextBox,这没问题,但我ViewModelPassword是空的.

我做错了什么或错过了一步?

我放了一个断点,确实代码进入了静态助手类,但它永远不会更新我PasswordViewModel.

Kon*_*man 187

我的2美分:

我使用WPF和MVVM开发了一个典型的登录对话框(用户和密码框,加上"确定"按钮).我通过简单地将PasswordBox控件本身作为参数传递给附加到"确定"按钮的命令来解决了密码绑定问题.所以我认为:

<PasswordBox Name="txtPassword" VerticalAlignment="Top" Width="120" />
<Button Content="Ok" Command="{Binding Path=OkCommand}"
   CommandParameter="{Binding ElementName=txtPassword}"/>
Run Code Online (Sandbox Code Playgroud)

在ViewModel中,Execute附加命令的方法如下:

void Execute(object parameter)
{
    var passwordBox = parameter as PasswordBox;
    var password = passwordBox.Password;
    //Now go ahead and check the user name and password
}
Run Code Online (Sandbox Code Playgroud)

这略微违反了MVVM模式,因为现在ViewModel知道View是如何实现的,但在那个特定的项目中我能负担得起.希望它对某人也有用.

  • 这是一个很好的解决方案,但失败的密码+密码确认组合 (4认同)
  • 非常有帮助,谢谢.fyi,有人可能会习惯看到_loginCommand = new RelayCommand(param => Login(UserName,(PasswordBox)param),param => CanLogIn); (3认同)
  • 谢谢你!这解决了将数据从UI线程移动到主程序线程时遇到的一个巨大问题.确保实现SecureString方法,并尽快〜尽快删除密码〜.转发它.处理它.清除它.做你需要做的事.此外,请确保您实现IDisposable. (2认同)

Ste*_* CO 174

也许我错过了一些东西,但似乎大多数这些解决方案都过于复杂化并取消了安全实践.

此方法不违反MVVM模式并保持完整的安全性.是的,从技术上来说它是代码背后,但它只不过是一个"特殊情况"绑定.ViewModel仍然不了解View实现,如果您尝试将PasswordBox传递给ViewModel,我认为这样做.

Code Behind!=自动MVVM违规.这一切都取决于你用它做什么.在这种情况下,我们只是手动编码绑定,因此它都被认为是UI实现的一部分,因此是可以的.

在ViewModel中,只是一个简单的属性.我把它设为"只写",因为不应该出于任何原因从ViewModel外部检索它,但它不一定是这样.请注意,它是一个SecureString,而不仅仅是一个字符串.

public SecureString SecurePassword { private get; set; }
Run Code Online (Sandbox Code Playgroud)

在xaml中,您设置了一个PasswordChanged事件处理程序.

<PasswordBox PasswordChanged="PasswordBox_PasswordChanged"/>
Run Code Online (Sandbox Code Playgroud)

在后面的代码中:

private void PasswordBox_PasswordChanged(object sender, RoutedEventArgs e)
{
    if (this.DataContext != null)
    { ((dynamic)this.DataContext).SecurePassword = ((PasswordBox)sender).SecurePassword; }
}
Run Code Online (Sandbox Code Playgroud)

使用此方法,您的密码始终保留在SecureString中,因此可提供最大的安全性.如果您真的不关心安全性,或者需要明文密码来获取需要它的下游方法(注意:大多数需要密码的.NET方法也支持SecureString选项,因此您可能不需要明文密码即使你认为你这样做,你也可以使用Password属性.像这样:

(ViewModel属性)

public string Password { private get; set; }
Run Code Online (Sandbox Code Playgroud)

(代码背后)

private void PasswordBox_PasswordChanged(object sender, RoutedEventArgs e)
{
    if (this.DataContext != null)
    { ((dynamic)this.DataContext).Password = ((PasswordBox)sender).Password; }
}
Run Code Online (Sandbox Code Playgroud)

如果你想保持强类型,你可以用ViewModel的界面替换(动态)强制转换.但实际上,"正常"的数据绑定也不是强类型的,所以它并不是那么重要.

private void PasswordBox_PasswordChanged(object sender, RoutedEventArgs e)
{
    if (this.DataContext != null)
    { ((IMyViewModel)this.DataContext).Password = ((PasswordBox)sender).Password; }
}
Run Code Online (Sandbox Code Playgroud)

所以世界上最好的 - 你的密码是安全的,你的ViewModel只有一个属性,就像任何其他属性一样,你的View是自包含的,不需要外部引用.

  • 感谢关于MVVM和偏执狂的严格教条的实用性.工作得很好,谢谢. (3认同)
  • 这个扩展名的SecureString示例很棒http://blogs.msdn.com/b/fpintos/archive/2009/06/12/how-to-properly-convert-securestring-to-string.aspx (2认同)
  • 确实不错 我希望 MS 刚刚将 SecureString 类型的 Password DP 添加到此控件中。 (2认同)

Jus*_*gel 162

对不起,但你做错了.

人们应该在眼睑内侧纹上以下安全指南:
切勿将纯文本密码保存在内存中.

WPF/Silverlight PasswordBox不为Password属性公开DP的原因与安全性有关.
如果WPF/Silverlight要保留DP for Password,则需要框架将密码本身保持在内存中未加密.这被认为是一个非常麻烦的安全攻击媒介.PasswordBox使用加密内存(各种),访问密码的唯一方法是通过CLR属性.

我建议在访问PasswordBox.Password CLR属性时,不要将其放在任何变量或任何属性的值中.
在客户机RAM上以明文形式保存密码是一种安全禁忌.
所以摆脱那个"公共字符串密码{get; set;}"你已经到了那里.

访问PasswordBox.Password时,只需将其取出并尽快将其发送到服务器.不要保留密码的值,也不要像对待任何其他客户端机器文本那样对待它.不要在内存中保留明文密码.

我知道这打破了MVVM模式,但你不应该绑定到PasswordBox.Password附加DP,将你的密码存储在ViewModel或任何其他类似的恶作剧中.

如果您正在寻找过度架构的解决方案,请
参阅以下内容:1.使用一种返回密码明文的方法创建IHavePassword接口.
2.让UserControl实现IHavePassword接口.
3.在您的IoC中注册UserControl实例,以实现IHavePassword接口.
4.当需要密码的服务器请求发生时,请调用IoC以获取IHavePassword实现,而不是获得令人垂涎的密码.

只是我对它的看法.

- 贾斯汀

  • 如果坏人可以访问您机器的RAM,那么您遇到的问题比他们窃取您的密码要大. (323认同)
  • 对于大多数情况,您*不需要*级别的安全性.当有太多其他方法来窃取密码时,有什么意义呢?Atleast WPF应该允许像@Bryant这样使用SecureString. (179认同)
  • 我同意您的意图和您传达的信息,但您的回答意味着如果您遵循此方法,密码字符串永远不会在内存中.从用户输入密码开始,密码的值将在内存中.消除保存密码短语的属性是一个好主意,并且会限制密码的副本,这些副本会留给垃圾收集器收集,或者可能由作为程序一部分运行的其他托管和非托管代码找到,但是不要完全隐瞒它. (33认同)
  • 难道你不能在VM中使用SecureString for WPF来解决这个问题吗?似乎Silverlight没有什么东西. (19认同)
  • 多年来,我一直在使用自定义用户控件,其行为与PasswordBox类似,但仅将文本值作为SecureString返回.是的,这可以防止Snoop以纯文本显示密码.但是,SecureString的纯文本值仍然可以很容易地提取,并且只能阻止新手攻击.如果您的系统存在隐蔽地使用像Snoop这样的关键记录器和嗅探器的风险,那么您应该重新评估系统安全性. (13认同)
  • 我同意,使用像Snoop这样的工具获取`PasswordBox.Password`属性的价值非常简单(比从ViewModel中取回它容易得多),因此不值得在ViewModel中隐藏该字符串. (9认同)
  • 有时我们只需要一个简单的PasswordBox控件来隐藏字符!在表单登录中没有密码的含义.. (6认同)
  • 出于好奇,还没有办法绑定到SecureString版本?完全可以理解的是,您可能不希望直接提供对Password的绑定,但完全没有绑定只会使整个控件更难以使用.显然,有一些解决方法,但不应该有. (4认同)
  • 名称PasswordBox或属性密码并不一定意味着在所有情况下或在典型的假设情况下的问题都与实施的关注相匹配.在这种情况下,这个人"做错了"是一个合理的猜测.但有些情况下,唯一的问题是直接可见性和预先填充,但需要密码样式的隐藏文本,尽管不需要基础数据安全性.这存在于想要使用/滥用类似PasswordBox的控件的可想象的原因之中. (2认同)

小智 20

你可以使用这个XAML:

<PasswordBox Name="PasswordBox">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="PasswordChanged">
            <i:InvokeCommandAction Command="{Binding PasswordChangedCommand}" CommandParameter="{Binding ElementName=PasswordBox}"/>
        </i:EventTrigger>
    </i:Interaction.Triggers>
</PasswordBox>
Run Code Online (Sandbox Code Playgroud)

而这个命令执行方法:

private void ExecutePasswordChangedCommand(PasswordBox obj)
{ 
   if (obj != null)
     Password = obj.Password;
}
Run Code Online (Sandbox Code Playgroud)

  • 仅供参考`xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"` (3认同)

Vla*_*kov 14

这对我来说很好.

<Button Command="{Binding Connect}" 
        CommandParameter="{Binding ElementName=MyPasswordBox}"/>
Run Code Online (Sandbox Code Playgroud)

  • CommandParameter ="{Binding ElementName = MyPasswordBox,Path = SecurePassword"}怎么样? (3认同)
  • LukeN,这不起作用(至少对我而言).可能出于同样的原因 - SecurePassword不是依赖属性. (2认同)

Jan*_*m B 10

不违反MVVM模式的简单解决方案是在ViewModel中引入一个收集密码的事件(或委托).

ViewModel中:

public event EventHandler<HarvestPasswordEventArgs> HarvestPassword;

使用这些EventArgs:

class HarvestPasswordEventArgs : EventArgs
{
    public string Password;
}
Run Code Online (Sandbox Code Playgroud)

View中,订阅创建ViewModel的事件并填写密码值.

_viewModel.HarvestPassword += (sender, args) => 
    args.Password = passwordBox1.Password;
Run Code Online (Sandbox Code Playgroud)

ViewModel中,当您需要密码时,您可以触发事件并从那里获取密码:

if (HarvestPassword == null)
  //bah 
  return;

var pwargs = new HarvestPasswordEventArgs();
HarvestPassword(this, pwargs);

LoginHelpers.Login(Username, pwargs.Password);
Run Code Online (Sandbox Code Playgroud)


Tay*_*ese 8

我在这里发布了一个GIST ,它是一个可绑定的密码框.

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

namespace CustomControl
{
    public class BindablePasswordBox : Decorator
    {
        /// <summary>
        /// The password dependency property.
        /// </summary>
        public static readonly DependencyProperty PasswordProperty;

        private bool isPreventCallback;
        private RoutedEventHandler savedCallback;

        /// <summary>
        /// Static constructor to initialize the dependency properties.
        /// </summary>
        static BindablePasswordBox()
        {
            PasswordProperty = DependencyProperty.Register(
                "Password",
                typeof(string),
                typeof(BindablePasswordBox),
                new FrameworkPropertyMetadata("", FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, new PropertyChangedCallback(OnPasswordPropertyChanged))
            );
        }

        /// <summary>
        /// Saves the password changed callback and sets the child element to the password box.
        /// </summary>
        public BindablePasswordBox()
        {
            savedCallback = HandlePasswordChanged;

            PasswordBox passwordBox = new PasswordBox();
            passwordBox.PasswordChanged += savedCallback;
            Child = passwordBox;
        }

        /// <summary>
        /// The password dependency property.
        /// </summary>
        public string Password
        {
            get { return GetValue(PasswordProperty) as string; }
            set { SetValue(PasswordProperty, value); }
        }

        /// <summary>
        /// Handles changes to the password dependency property.
        /// </summary>
        /// <param name="d">the dependency object</param>
        /// <param name="eventArgs">the event args</param>
        private static void OnPasswordPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs eventArgs)
        {
            BindablePasswordBox bindablePasswordBox = (BindablePasswordBox) d;
            PasswordBox passwordBox = (PasswordBox) bindablePasswordBox.Child;

            if (bindablePasswordBox.isPreventCallback)
            {
                return;
            }

            passwordBox.PasswordChanged -= bindablePasswordBox.savedCallback;
            passwordBox.Password = (eventArgs.NewValue != null) ? eventArgs.NewValue.ToString() : "";
            passwordBox.PasswordChanged += bindablePasswordBox.savedCallback;
        }

        /// <summary>
        /// Handles the password changed event.
        /// </summary>
        /// <param name="sender">the sender</param>
        /// <param name="eventArgs">the event args</param>
        private void HandlePasswordChanged(object sender, RoutedEventArgs eventArgs)
        {
            PasswordBox passwordBox = (PasswordBox) sender;

            isPreventCallback = true;
            Password = passwordBox.Password;
            isPreventCallback = false;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)


Aoi*_*asu 7

为了在不破坏MVVM的情况下解决OP问题,我将使用自定义值转换器和必须从密码框中检索的值(密码)的包装器.

public interface IWrappedParameter<T>
{
    T Value { get; }
}

public class PasswordBoxWrapper : IWrappedParameter<string>
{
    private readonly PasswordBox _source;

    public string Value
    {
        get { return _source != null ? _source.Password : string.Empty; }
    }

    public PasswordBoxWrapper(PasswordBox source)
    {
        _source = source;
    }
}

public class PasswordBoxConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        // Implement type and value check here...
        return new PasswordBoxWrapper((PasswordBox)value);
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new InvalidOperationException("No conversion.");
    }
}
Run Code Online (Sandbox Code Playgroud)

在视图模型中:

public string Username { get; set; }

public ICommand LoginCommand
{
    get
    {
        return new RelayCommand<IWrappedParameter<string>>(password => { Login(Username, password); });
    }
}

private void Login(string username, string password)
{
    // Perform login here...
}
Run Code Online (Sandbox Code Playgroud)

因为视图模型使用IWrappedParameter<T>,所以不需要任何关于PasswordBoxWrappernor的知识PasswordBoxConverter.这样,您可以将PasswordBox对象与视图模型隔离,而不会破坏MVVM模式.

在视图中:

<Window.Resources>
    <h:PasswordBoxConverter x:Key="PwdConverter" />
</Window.Resources>
...
<PasswordBox Name="PwdBox" />
<Button Content="Login" Command="{Binding LoginCommand}"
        CommandParameter="{Binding ElementName=PwdBox, Converter={StaticResource PwdConverter}}" />
Run Code Online (Sandbox Code Playgroud)


小智 6

这种实现略有不同.您将密码框传递给View通过ViewModel中的属性绑定,它不使用任何命令参数.ViewModel保持对视图的无知.我有一个VB vs 2010项目,可以从SkyDrive下载.Wpf MvvM PassWordBox Example.zip https://skydrive.live.com/redir.aspx?cid=e95997d33a9f8d73&resid=E95997D33A9F8D73!511

我在Wpf MvvM应用程序中使用PasswordBox的方式非常简单,对我来说效果很好.这并不意味着我认为这是正确的方式或最好的方式.它只是使用PasswordBox和MvvM模式的一个实现.

基本上您创建一个公共只读属性,View可以将其绑定为PasswordBox(实际控件)示例:

Private _thePassWordBox As PasswordBox
Public ReadOnly Property ThePassWordBox As PasswordBox
    Get
        If IsNothing(_thePassWordBox) Then _thePassWordBox = New PasswordBox
        Return _thePassWordBox
    End Get
End Property
Run Code Online (Sandbox Code Playgroud)

我使用后备字段来执行属性的自我初始化.

然后从Xaml绑定ContentControl的内容或控件容器示例:

 <ContentControl Grid.Column="1" Grid.Row="1" Height="23" Width="120" Content="{Binding Path=ThePassWordBox}" HorizontalAlignment="Center" VerticalAlignment="Center" />
Run Code Online (Sandbox Code Playgroud)

从那里你可以完全控制密码箱我还使用PasswordAccessor(只是字符串函数)在登录时或其他任何你想要密码的地方返回密码值.在示例中,我在通用用户对象模型中有一个公共属性.例:

Public Property PasswordAccessor() As Func(Of String)
Run Code Online (Sandbox Code Playgroud)

在用户对象中,密码字符串属性是只读的,没有任何后备存储,它只是从PasswordBox返回密码.例:

Public ReadOnly Property PassWord As String
    Get
        Return If((PasswordAccessor Is Nothing), String.Empty, PasswordAccessor.Invoke())
    End Get
End Property
Run Code Online (Sandbox Code Playgroud)

然后在ViewModel中,我确保创建了Accessor并将其设置为PasswordBox.Password属性'示例:

Public Sub New()
    'Sets the Accessor for the Password Property
    SetPasswordAccessor(Function() ThePassWordBox.Password)
End Sub

Friend Sub SetPasswordAccessor(ByVal accessor As Func(Of String))
    If Not IsNothing(VMUser) Then VMUser.PasswordAccessor = accessor
End Sub
Run Code Online (Sandbox Code Playgroud)

当我需要密码字符串说登录时,我只是获得真正调用函数来获取密码并将其返回的用户对象密码属性,然后用户对象不存储实际密码.示例:将在ViewModel中

Private Function LogIn() as Boolean
    'Make call to your Authentication methods and or functions. I usally place that code in the Model
    Return AuthenticationManager.Login(New UserIdentity(User.UserName, User.Password)
End Function
Run Code Online (Sandbox Code Playgroud)

应该这样做.ViewModel不需要任何View的控件知识.视图仅绑定到ViewModel中的属性,与视图绑定到图像或其他资源没有任何区别.在这种情况下,资源(Property)恰好是用户控件.它允许在ViewModel创建和拥有Property时进行测试,并且Property独立于View.至于安全性,我不知道这个实现有多好.但是通过使用Function,Value不会存储在Property所访问的Property本身中.


Moo*_*tom 6

我花了大量时间来研究各种解决方案。我不喜欢装饰器的想法,行为使验证UI混乱,背后的代码...真的吗?

最好的方法是坚持使用自定义附加属性并SecureString在视图模型中绑定到您的属性。尽可能将其保留在那里。每当您需要快速访问纯密码时,请使用以下代码将其临时转换为不安全的字符串:

namespace Namespace.Extensions
{
    using System;
    using System.Runtime.InteropServices;
    using System.Security;

    /// <summary>
    /// Provides unsafe temporary operations on secured strings.
    /// </summary>
    [SuppressUnmanagedCodeSecurity]
    public static class SecureStringExtensions
    {
        /// <summary>
        /// Converts a secured string to an unsecured string.
        /// </summary>
        public static string ToUnsecuredString(this SecureString secureString)
        {
            // copy&paste from the internal System.Net.UnsafeNclNativeMethods
            IntPtr bstrPtr = IntPtr.Zero;
            if (secureString != null)
            {
                if (secureString.Length != 0)
                {
                    try
                    {
                        bstrPtr = Marshal.SecureStringToBSTR(secureString);
                        return Marshal.PtrToStringBSTR(bstrPtr);
                    }
                    finally
                    {
                        if (bstrPtr != IntPtr.Zero)
                            Marshal.ZeroFreeBSTR(bstrPtr);
                    }
                }
            }
            return string.Empty;
        }

        /// <summary>
        /// Copies the existing instance of a secure string into the destination, clearing the destination beforehand.
        /// </summary>
        public static void CopyInto(this SecureString source, SecureString destination)
        {
            destination.Clear();
            foreach (var chr in source.ToUnsecuredString())
            {
                destination.AppendChar(chr);
            }
        }

        /// <summary>
        /// Converts an unsecured string to a secured string.
        /// </summary>
        public static SecureString ToSecuredString(this string plainString)
        {
            if (string.IsNullOrEmpty(plainString))
            {
                return new SecureString();
            }

            SecureString secure = new SecureString();
            foreach (char c in plainString)
            {
                secure.AppendChar(c);
            }
            return secure;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

确保您允许GC收集您的UI元素,因此不要再对上的PasswordChanged事件使用静态事件处理程序PasswordBox。我还发现了一个异常,其中使用SecurePassword属性进行设置时控件未更新UI,这就是我将密码复制到其中的原因Password

namespace Namespace.Controls
{
    using System.Security;
    using System.Windows;
    using System.Windows.Controls;
    using Namespace.Extensions;

    /// <summary>
    /// Creates a bindable attached property for the <see cref="PasswordBox.SecurePassword"/> property.
    /// </summary>
    public static class PasswordBoxHelper
    {
        // an attached behavior won't work due to view model validation not picking up the right control to adorn
        public static readonly DependencyProperty SecurePasswordBindingProperty = DependencyProperty.RegisterAttached(
            "SecurePassword",
            typeof(SecureString),
            typeof(PasswordBoxHelper),
            new FrameworkPropertyMetadata(new SecureString(),FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, AttachedPropertyValueChanged)
        );

        private static readonly DependencyProperty _passwordBindingMarshallerProperty = DependencyProperty.RegisterAttached(
            "PasswordBindingMarshaller",
            typeof(PasswordBindingMarshaller),
            typeof(PasswordBoxHelper),
            new PropertyMetadata()
        );

        public static void SetSecurePassword(PasswordBox element, SecureString secureString)
        {
            element.SetValue(SecurePasswordBindingProperty, secureString);
        }

        public static SecureString GetSecurePassword(PasswordBox element)
        {
            return element.GetValue(SecurePasswordBindingProperty) as SecureString;
        }

        private static void AttachedPropertyValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            // we'll need to hook up to one of the element's events
            // in order to allow the GC to collect the control, we'll wrap the event handler inside an object living in an attached property
            // don't be tempted to use the Unloaded event as that will be fired  even when the control is still alive and well (e.g. switching tabs in a tab control) 
            var passwordBox = (PasswordBox)d;
            var bindingMarshaller = passwordBox.GetValue(_passwordBindingMarshallerProperty) as PasswordBindingMarshaller;
            if (bindingMarshaller == null)
            {
                bindingMarshaller = new PasswordBindingMarshaller(passwordBox);
                passwordBox.SetValue(_passwordBindingMarshallerProperty, bindingMarshaller);
            }

            bindingMarshaller.UpdatePasswordBox(e.NewValue as SecureString);
        }

        /// <summary>
        /// Encapsulated event logic
        /// </summary>
        private class PasswordBindingMarshaller
        {
            private readonly PasswordBox _passwordBox;
            private bool _isMarshalling;

            public PasswordBindingMarshaller(PasswordBox passwordBox)
            {
                _passwordBox = passwordBox;
                _passwordBox.PasswordChanged += this.PasswordBoxPasswordChanged;
            }

            public void UpdatePasswordBox(SecureString newPassword)
            {
                if (_isMarshalling)
                {
                    return;
                }

                _isMarshalling = true;
                try
                {
                    // setting up the SecuredPassword won't trigger a visual update so we'll have to use the Password property
                    _passwordBox.Password = newPassword.ToUnsecuredString();

                    // you may try the statement below, however the benefits are minimal security wise (you still have to extract the unsecured password for copying)
                    //newPassword.CopyInto(_passwordBox.SecurePassword);
                }
                finally
                {
                    _isMarshalling = false;
                }
            }

            private void PasswordBoxPasswordChanged(object sender, RoutedEventArgs e)
            {
                // copy the password into the attached property
                if (_isMarshalling)
                {
                    return;
                }

                _isMarshalling = true;
                try
                {
                    SetSecurePassword(_passwordBox, _passwordBox.SecurePassword.Copy());
                }
                finally
                {
                    _isMarshalling = false;
                }
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

和XAML用法:

<PasswordBox controls:PasswordBoxHelper.SecurePassword="{Binding LogonPassword, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}">
Run Code Online (Sandbox Code Playgroud)

我在视图模型中的属性如下所示:

[RequiredSecureString]
public SecureString LogonPassword
{
   get
   {
       return _logonPassword;
   }
   set
   {
       _logonPassword = value;
       NotifyPropertyChanged(nameof(LogonPassword));
   }
}
Run Code Online (Sandbox Code Playgroud)

RequiredSecureString仅仅是一个拥有以下逻辑简单的自定义的验证:

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = true)]    
public class RequiredSecureStringAttribute:ValidationAttribute
{
    public RequiredSecureStringAttribute()
        :base("Field is required")
    {            
    }

    public override bool IsValid(object value)
    {
        return (value as SecureString)?.Length > 0;
    }
}
Run Code Online (Sandbox Code Playgroud)

这边有 完整且经过测试的纯MVVM解决方案。


mik*_*nie 5

虽然我同意避免将密码存储在任何地方很重要,但我仍然需要能够在没有视图的情况下实例化视图模型并执行我的测试.

对我有用的解决方案是将PasswordBox.Password函数注册到视图模型,并让视图模型在执行登录代码时调用它.

确实意味着视图的代码隐藏中的一行代码.

所以,在我的Login.xaml中,我有

<PasswordBox x:Name="PasswordBox"/>
Run Code Online (Sandbox Code Playgroud)

在Login.xaml.cs我有

LoginViewModel.PasswordHandler = () => PasswordBox.Password;
Run Code Online (Sandbox Code Playgroud)

然后在LoginViewModel.cs中我定义了PasswordHandler

public Func<string> PasswordHandler { get; set; }
Run Code Online (Sandbox Code Playgroud)

当登录需要发生时,代码调用处理程序从视图中获取密码......

bool loginResult = Login(Username, PasswordHandler());
Run Code Online (Sandbox Code Playgroud)

这样,当我想测试viewmodel时,我可以简单地将PasswordHandler设置为匿名方法,该方法允许我提供我想在测试中使用的任何密码.