如何绑定到MVVM中的PasswordBox

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何绑定到MVVM中的PasswordBox相关的知识,希望对你有一定的参考价值。

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

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

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

我基本上在LoginViewModelUsernamePassword中有属性。 Username很好,正在工作,因为它是一个TextBox

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

<PasswordBox ff:PasswordHelper.Attach="True"
    ff:PasswordHelper.Password="{Binding Path=Password}" Width="130"/>

当我把PasswordBox作为TextBoxBinding Path=Password时,我的LoginViewModel中的属性被更新了。

我的代码非常简单,基本上我有Command为我的Button。当我按下它时,调用CanLogin,如果它返回true,则调用Login。 你可以看到我检查我的物业Username这里工作得很好。

Login我发送给我的服务UsernamePasswordUsername包含我的View的数据,但PasswordNull|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 { }
    }

这就是我在做的事情

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

<PasswordBox ff:PasswordHelper.Attach="True" 
             ff:PasswordHelper.Password="{Binding Path=Password}" Width="130"/>

我有我的TextBox,这没问题,但在我的ViewModel中,Password是空的。

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

我放了一个断点,确定代码进入静态助手类但它永远不会在我的Password中更新我的ViewModel

答案

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

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

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

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

<PasswordBox x:Name="PasswordBox"/>

在Login.xaml.cs我有

LoginViewModel.PasswordHandler = () => PasswordBox.Password;

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

public Func<string> PasswordHandler { get; set; }

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

bool loginResult = Login(Username, PasswordHandler());

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

另一答案

我花了很多时间研究各种解决方案。我不喜欢装饰者的想法,行为搞砸了验证用户界面,代码背后......真的吗?

最好的方法是坚持自定义附加属性并绑定到视图模型中的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;
        }
    }
}

确保您允许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;
                }
            }
        }
    }
}

和XAML用法:

<PasswordBox controls:PasswordBoxHelper.SecurePassword="{Binding LogonPassword, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}">
以上是关于如何绑定到MVVM中的PasswordBox的主要内容,如果未能解决你的问题,请参考以下文章

WPF PasswordBox password数据绑定

WPF MVVM从入门到精通3:数据绑定

WPF中PasswordBox控件的Password属性的数据绑定

PasswordBox和MVVM

WPF MVVM从入门到精通6:RadioButton等一对多控件的绑定

WPF MVVM从入门到精通8:数据验证