如何绑定到MVVM中的PasswordBox


251

我遇到了绑定到P的问题asswordBox。似乎有安全隐患,但是我使用的是MVVM模式,因此我希望绕过这一点。我在这里找到了一些有趣的代码(有人使用过此代码还是类似的代码?)

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

从技术上来说看起来不错,但是我不确定如何找回密码。

我基本上有LoginViewModelfor Username和的属性PasswordUsername很好,并且可以正常工作TextBox

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

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

当我使用PasswordBoxas时TextBoxBinding Path=Password然后LoginViewModel更新了我的属性。

我的代码非常简单,基本上我有一个Commandfor Button。当我按下它时CanLogin被调用,如果返回true则调用Login
您可以看到我Username在这里检查我的房产,效果很好。

Login我一起发送到我的服务UsernamePasswordUsername从包含数据我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 { }
}

这就是我在做什么

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

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

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

我是在做错什么还是错过了一步?

我设置了一个断点,并确保代码进入了静态帮助器类,但是它永远不会更新my Password中的类ViewModel


3
好吧,事实证明代码没有用,但是我在这里尝试了另一种代码,它可以完美地工作。blog.functionalfun.net/2008/06/...
马克·史密斯

5
传递整个passwordbox控件是否不利于将视图与ViewModel分开?

Answers:


164

抱歉,您做错了。

人们的眼睑内侧应刻有以下安全准则:
切勿在内存中保留纯文本密码。

WPF / Silverlight PasswordBox不公开该Password属性的DP 的原因与安全性有关。
如果WPF / Silverlight保留DP,Password则它将要求框架将密码本身未加密在内存中。这被认为是相当麻烦的安全攻击媒介。该PasswordBox(的种类)使用加密存储和访问密码的唯一方法是通过CLR属性。

我建议访问PasswordBox.PasswordCLR属性时,不要将其放在任何变量中或作为任何属性的值。
将密码以纯文本格式保存在客户端计算机的RAM上是不可以的。
所以摆脱掉public string Password { get; set; }你已经站在那里。

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

我知道这会破坏MVVM模式,但您永远不应绑定到PasswordBox.PasswordAttached DP,将密码存储在ViewModel或任何其他类似的恶作剧中。

如果您正在寻找一种过度架构的解决方案,请执行以下操作:
1. IHavePassword使用一种返回密码明文的方法来创建接口。
2.让您UserControl实现一个IHavePassword接口。
3.在UserControl实现IHavePassword接口的同时向IoC 注册实例。
4.当服务器要求您输入密码时,请致电您的IoC进行IHavePassword实施,这不仅会获得梦co以求的密码。

只是我的看法。

-贾斯汀


19
您不能在VM for WPF中使用SecureString解决此问题吗?Silverlight似乎没有东西。
科比

35
我同意您的意图和您要传达的信息,但是您的回答暗示,如果您采用这种方法,则密码字符串永远不会在内存中。从用户键入密码之时起,该密码的值便会存储在内存中。消除持有密码短语的属性是个好主意,它将限制留给垃圾收集器使用的密码副本,或者可能由程序中运行的其他托管和非托管代码找到的副本,但是不要完全隐藏它。
伊恩·诺顿

182
在大多数情况下,你并不需要的安全等级。当有许多其他窃取密码的方式使一件事情变得困难时,有什么意义呢?Atleast WPF应该允许使用@@ Bryant所说的SecureString。
chakrit

335
如果坏人可以访问您计算机的RAM,那么与他们窃取密码相比,您面临的问题更大。
卡梅隆·麦克法兰

13
多年以来,我一直在使用自定义用户控件,其行为类似于PasswordBox,但仅将文本值返回为SecureString。是的,这可以防止Snoop以纯文本显示密码。但是,仍然可以很容易地提取SecureString的纯文本值,并且只能阻止新手破解。如果您的系统有秘密使用按键记录程序和Snoop等嗅探器的风险,则应重新评估系统安全性。
麦克·克里斯蒂安

199

我的2美分:

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

<PasswordBox Name="txtPassword" VerticalAlignment="Top" Width="120" />
<Button Content="Ok" Command="{Binding Path=OkCommand}"
   CommandParameter="{Binding ElementName=txtPassword}"/>

在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
}

因为现在ViewModel知道一些有关如何实现View的知识,所以这有点违反MVVM模式,但是在这个特定项目中,我可以负担得起。希望它对某人也有用。


您好,Konamiman,当调用Execute方法时。在我的视图模型中,我有一个类User(login,pass)和一个认证命令。如何在这种情况下使用Execute?

3
非常有帮助,谢谢。仅供参考,有人可能会看到类似_loginCommand = new RelayCommand(param => Login(UserName,(PasswordBox)param),param => CanLogIn)之类的东西;
Chuck Rostance

5
这是一个不错的解决方案,但由于密码+密码确认组合之类的问题而失败
Julien 2014年

您好Konamiman,我使用的是您的解决方案,但在Windows 8.1 Store应用程序上不起作用。我问过这样一个问题:stackoverflow.com/questions/26221594/...
VansFannel

2
谢谢你!这解决了我将数据从UI线程移到主程序线程时遇到的一个巨大问题。确保实施SecureString方法,并尽快删除密码。转储它。处理它。清除它。做您需要做的。另外,请确保您实现IDisposable。
史蒂文·布瑞顿

184

也许我缺少了一些东西,但是似乎这些解决方案中的大多数都使事情变得过于复杂,并且消除了安全做法。

此方法不违反MVVM模式,并保持完整的安全性。是的,从技术上讲,它是代码背后的东西,但这不过是“特殊情况”的绑定而已。ViewModel仍然不了解View实现,在我看来,如果您试图将PasswordBox传递给ViewModel,它就会知道。

后面的代码=自动违反MVVM。这完全取决于您对它的处理方式。在这种情况下,我们只是手动编码绑定,因此将其全部视为UI实现的一部分,因此可以。

在ViewModel中,只是一个简单的属性。我将其设置为“只写”,因为出于任何原因都无需从ViewModel外部检索它,但不必一定要这样做。请注意,它是一个SecureString,而不仅仅是一个字符串。

public SecureString SecurePassword { private get; set; }

在xaml中,设置一个PasswordChanged事件处理程序。

<PasswordBox PasswordChanged="PasswordBox_PasswordChanged"/>

在后面的代码中:

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

使用此方法,您的密码始终保留在SecureString中,因此可提供最大的安全性。如果您真的不在乎安全性,或者需要使用明文密码作为需要它的下游方法(请注意:大多数需要密码的.NET方法也支持SecureString选项,因此您可能实际上并不需要明文密码即使您认为这样做),也可以只使用Password属性。像这样:

(ViewModel属性)

public string Password { private get; set; }

(后面的代码)

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

如果要保持强类型输入,可以用ViewModel的接口替换(动态)强制类型转换。但是实际上,“正常”数据绑定也不是强类型的,因此没什么大不了的。

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

因此,万事俱备-您的密码是安全的,您的ViewModel具有与其他任何属性一样的属性,并且View是自包含的,不需要外部引用。


1
这对我来说看起来不错!如果您想在安全方面变得非常严格,我不确定这会削减它,但对我来说,这是一个完美的中间立场。谢谢!
jrich523 2014年

3
感谢您对有关MVVM和妄想症的严格教条的实用性。很好,谢谢。
布鲁斯·皮尔森


1
的确不错。我希望MS刚向该控件添加SecureString类型的Password DP。
基思·希尔

1
这是完美的答案,因为它保留了安全性和MVVM。
LoRdPMN '16

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>

而此命令的执行方法:

private void ExecutePasswordChangedCommand(PasswordBox obj)
{ 
   if (obj != null)
     Password = obj.Password;
}

3
FYIxmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
XAMlMAX

无需命名PasswordBox :(CommandParameter="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=PasswordBox}}"注意:not RelativeSource Self)。
温德拉

该解决方案违反了MVVM模式。
BionicCode

13

这对我来说很好。

<Button Command="{Binding Connect}" 
        CommandParameter="{Binding ElementName=MyPasswordBox}"/>

3
那么CommandParameter =“ {Binding ElementName = MyPasswordBox,Path = SecurePassword”}呢?
路加福音

2
卢克,这行不通(至少对我来说)。可能出于相同的原因-SecurePassword不是依赖项属性。
vkrzv 2011年

假设ICommand在视图模型中实现,则此解决方案将违反MVVM模式。
BionicCode

9

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

ViewModel中

public event EventHandler<HarvestPasswordEventArgs> HarvestPassword;

这些EventArgs:

class HarvestPasswordEventArgs : EventArgs
{
    public string Password;
}

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

_viewModel.HarvestPassword += (sender, args) => 
    args.Password = passwordBox1.Password;

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

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

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

LoginHelpers.Login(Username, pwargs.Password);

您缺少的一件事是,在将视图预订到视图模型事件时,应使用a WeakEventManager<TEventSource, TEventArgs>以避免内存泄漏。通常情况下,视图的寿命与视图模型的寿命不同。 WeakEventManager<IViewModel, EventArgs>.AddHandler(iViewModelInstance, nameof(IViewModel.Event), eventHandlerMethod);
Todd A. Stedel,

我更喜欢这种解决方案,因为它很简单,不会违反MVVM,背后的代码最少,可以正确使用密码箱(如果使用“ SecurePassword”,则可以)。现在,现在也很容易实现其他HarvestPassword方法(例如SmartCard ....)
Matt

8

我花了大量时间来研究各种解决方案。我不喜欢装饰器的想法,行为使验证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;
        }
    }
}

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

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

[RequiredSecureString]
public SecureString LogonPassword
{
   get
   {
       return _logonPassword;
   }
   set
   {
       _logonPassword = value;
       NotifyPropertyChanged(nameof(LogonPassword));
   }
}

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;
    }
}

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


7

我在这里发布了一个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;
        }
    }
}

1
尽管这还不错,但是您失去了设置简单属性(如padding和tabindex)的能力
Julien

1
泰勒(Taylor),我内插了要点,以便可以在答案中找到它。(否则,它看起来像是仅链接的答案。.只是试图避免这样被删除。)随意弄乱内联的内容。
Lynn在2015年

@Julien,但您可以使用样式进行修复。我以类似的方式解决了这个问题,但是我使用a ContentControl,然后可以只使用PasswordBox作为XAML中适合的内容和样式。的目的ContentControl只是为了订阅PasswordChanged事件并公开双向可绑定属性。总而言之,这是65行代码,几乎可以完成这个装饰类的工作。请参阅以下gist.github.com/leidegre/c7343b8c720000fe3132
John Leidegren

6

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

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

基本上,您创建一个公共的只读属性,该视图可以作为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

我仅使用一个后备字段来进行属性的自初始化。

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

 <ContentControl Grid.Column="1" Grid.Row="1" Height="23" Width="120" Content="{Binding Path=ThePassWordBox}" HorizontalAlignment="Center" VerticalAlignment="Center" />

从那里,您可以完全控制密码框。我还使用PasswordAccessor(仅是字符串函数)在登录时或想要密码时返回密码值。在示例中,我在通用用户对象模型中具有一个公共属性。例:

Public Property PasswordAccessor() As Func(Of String)

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

Public ReadOnly Property PassWord As String
    Get
        Return If((PasswordAccessor Is Nothing), String.Empty, PasswordAccessor.Invoke())
    End Get
End Property

然后,在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

当我需要密码字符串说用于登录时,我只获得了真正调用该函数以获取密码并返回它的User Objects Password属性,那么实际的密码不是由User Object存储的。示例:将在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

那应该做。ViewModel不需要任何有关View控件的知识。视图只是绑定到ViewModel中的属性,与将视图绑定到图像或其他资源没有什么不同。在这种情况下,resource(Property)恰好是一个用户控件。当ViewModel创建并拥有Property且Property独立于View时,它允许进行测试。至于安全性,我不知道这种实现有多好。但是,通过使用函数,值不会存储在属性本身中,而只能由属性访问。


6

为了在不破坏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.");
    }
}

在视图模型中:

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...
}

因为视图模型的用途IWrappedParameter<T>,它并不需要有任何的知识PasswordBoxWrapper,也没有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}}" />

非常优雅的解决方案imo。我基于此。唯一的区别是:我将SecureString SecurePassword传递给登录功能而不是String Password。这样就不会有密码在内存中飞散的未加密字符串。
叫我胡萝卜

已经有一段时间了,但是由于我的RelayCommand,我似乎无法使它正常工作。您介意添加您的吗?
Ecnerwal '16

5

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

对我有用的解决方案是在视图模型中注册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());

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


4

我想我会把解决方案混在一起,因为这是一个普遍的问题……而且拥有很多选择总是一件好事。

我只是将a包裹PasswordBoxUserControl并实现了a DependencyProperty以便进行绑定。我正在尽我所能避免在内存中存储任何明文,因此一切都通过SecureStringPasswordBox.Password属性完成。在foreach循环中,每个字符的确会暴露出来,但这非常简短。老实说,如果您担心WPF应用程序会因这种短暂的暴露而受到损害,那么您将遇到更大的安全问题。

这样做的好处是您不会违反任何MVVM规则,即使是“纯粹的”规则也是UserControl如此,因为这是一个,因此可以在其代码后面加上代码。使用它时,您之间可以进行纯粹的通信,ViewViewModel无需VideModel知道View密码的任何部分或来源。只要确保你绑定到SecureString你的ViewModel

BindablePasswordBox.xaml

<UserControl x:Class="BK.WPF.CustomControls.BindanblePasswordBox"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" d:DesignHeight="22" d:DesignWidth="150">
    <PasswordBox x:Name="PswdBox"/>
</UserControl>

BindablePasswordBox.xaml.cs(版本1-不支持双向绑定。)

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

namespace BK.WPF.CustomControls
{
    public partial class BindanblePasswordBox : UserControl
    {
        public static readonly DependencyProperty PasswordProperty =
            DependencyProperty.Register("Password", typeof(SecureString), typeof(BindanblePasswordBox));

        public SecureString Password
        {
            get { return (SecureString)GetValue(PasswordProperty); }
            set { SetValue(PasswordProperty, value); }
        }

        public BindanblePasswordBox()
        {
            InitializeComponent();
            PswdBox.PasswordChanged += PswdBox_PasswordChanged;
        }

        private void PswdBox_PasswordChanged(object sender, RoutedEventArgs e)
        {
            var secure = new SecureString();
            foreach (var c in PswdBox.Password)
            {
                secure.AppendChar(c);
            }
            Password = secure;
        }
    }
}

版本1的用法:

<local:BindanblePasswordBox Width="150" HorizontalAlignment="Center"
                            VerticalAlignment="Center"
                            Password="{Binding Password, Mode=OneWayToSource}"/>

BindablePasswordBox.xaml.cs(版本2-具有双向绑定支持。)

public partial class BindablePasswordBox : UserControl
{
    public static readonly DependencyProperty PasswordProperty =
        DependencyProperty.Register("Password", typeof(SecureString), typeof(BindablePasswordBox),
        new PropertyMetadata(PasswordChanged));

    public SecureString Password
    {
        get { return (SecureString)GetValue(PasswordProperty); }
        set { SetValue(PasswordProperty, value); }
    }

    public BindablePasswordBox()
    {
        InitializeComponent();
        PswdBox.PasswordChanged += PswdBox_PasswordChanged;
    }

    private void PswdBox_PasswordChanged(object sender, RoutedEventArgs e)
    {
        var secure = new SecureString();
        foreach (var c in PswdBox.Password)
        {
            secure.AppendChar(c);
        }
        if (Password != secure)
        {
            Password = secure;
        }
    }

    private static void PasswordChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var pswdBox = d as BindablePasswordBox;
        if (pswdBox != null && e.NewValue != e.OldValue)
        {
            var newValue = e.NewValue as SecureString;
            if (newValue == null)
            {
                return;
            }

            var unmanagedString = IntPtr.Zero;
            string newString;
            try
            {
                unmanagedString = Marshal.SecureStringToGlobalAllocUnicode(newValue);
                newString = Marshal.PtrToStringUni(unmanagedString);
            }
            finally
            {
                Marshal.ZeroFreeGlobalAllocUnicode(unmanagedString);
            }

            var currentValue = pswdBox.PswdBox.Password;
            if (currentValue != newString)
            {
                pswdBox.PswdBox.Password = newString;
            }
        }
    }
}

版本2的用法:

<local:BindanblePasswordBox Width="150" HorizontalAlignment="Center"
                            VerticalAlignment="Center"
                            Password="{Binding Password, Mode=TwoWay}"/>

我已经尝试实现此功能,但是在更新UI上的密码时会遇到无限循环。因为if (Password != secure)SecureString不会覆盖equals,所以它将始终为false。有什么想法吗?
simonalexander2005 '18


2

我使用了这种方法并通过了密码框,尽管这确实违反了MVVM,但对我而言却是必不可少的,因为我在具有复杂模板环境的shell中使用带有数据模板的内容控件进行登录。因此,访问外壳程序背后的代码将很糟糕。

据我所知,传递密码箱与我从后面的代码访问控件一样。我同意密码,不要保留在内存中,等等。在此实现中,我没有视图模型中密码的属性。

按钮指令

Command="{Binding Path=DataContext.LoginCommand, ElementName=MyShell}" CommandParameter="{Binding ElementName=PasswordBox}"

视图模型

private void Login(object parameter)
{
    System.Windows.Controls.PasswordBox p = (System.Windows.Controls.PasswordBox)parameter;
    MessageBox.Show(p.Password);
}

这明显违反了MVVM模式。该模式不允许处理视图模型中的控件。
BionicCode

2

对我来说,这两件事都是错的:

  • 实施明文密码属性
  • 发送PasswordBox作为命令参数的视图模型

Steve在CO中描述的那样传送SecurePassword(SecureString实例)似乎是可以接受的。我更喜欢Behaviors在后面编写代码,并且还具有能够从viewmodel重置密码的其他要求。

Xaml(Password是ViewModel属性):

<PasswordBox>
    <i:Interaction.Behaviors>
        <behaviors:PasswordBinding BoundPassword="{Binding Password, Mode=TwoWay}" />
    </i:Interaction.Behaviors>
</PasswordBox>

行为:

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

namespace Evidence.OutlookIntegration.AddinLogic.Behaviors
{
    /// <summary>
    /// Intermediate class that handles password box binding (which is not possible directly).
    /// </summary>
    public class PasswordBoxBindingBehavior : Behavior<PasswordBox>
    {
        // BoundPassword
        public SecureString BoundPassword { get { return (SecureString)GetValue(BoundPasswordProperty); } set { SetValue(BoundPasswordProperty, value); } }
        public static readonly DependencyProperty BoundPasswordProperty = DependencyProperty.Register("BoundPassword", typeof(SecureString), typeof(PasswordBoxBindingBehavior), new FrameworkPropertyMetadata(OnBoundPasswordChanged));

        protected override void OnAttached()
        {
            this.AssociatedObject.PasswordChanged += AssociatedObjectOnPasswordChanged;
            base.OnAttached();
        }

        /// <summary>
        /// Link up the intermediate SecureString (BoundPassword) to the UI instance
        /// </summary>
        private void AssociatedObjectOnPasswordChanged(object s, RoutedEventArgs e)
        {
            this.BoundPassword = this.AssociatedObject.SecurePassword;
        }

        /// <summary>
        /// Reacts to password reset on viewmodel (ViewModel.Password = new SecureString())
        /// </summary>
        private static void OnBoundPasswordChanged(object s, DependencyPropertyChangedEventArgs e)
        {
            var box = ((PasswordBoxBindingBehavior)s).AssociatedObject;
            if (box != null)
            {
                if (((SecureString)e.NewValue).Length == 0)
                    box.Password = string.Empty;
            }
        }

    }
}

2

对于像我这样的新手,这里有Konamiman上面建议的完整工作样本。谢谢Konamiman

XAML

    <PasswordBox x:Name="textBoxPassword"/>
    <Button x:Name="buttonLogin" Content="Login"
            Command="{Binding PasswordCommand}"
            CommandParameter="{Binding ElementName=textBoxPassword}"/> 

视图模型

public class YourViewModel : ViewModelBase
{
    private ICommand _passwordCommand;
    public ICommand PasswordCommand
    {
        get {
            if (_passwordCommand == null) {
                _passwordCommand = new RelayCommand<object>(PasswordClick);
            }
            return _passwordCommand;
        }
    }

    public YourViewModel()
    {
    }

    private void PasswordClick(object p)
    {
        var password = p as PasswordBox;
        Console.WriteLine("Password is: {0}", password.Password);
    }
}

这明显违反了MVVM模式。该模式不允许处理视图模型中的控件。
BionicCode

1

如您所见,我将绑定到密码,但也许将其绑定到静态类。

它是一个附加属性。这种属性可以应用于任何类型的DependencyObject,而不仅仅是声明它的类型。因此,即使在PasswordHelper静态类中声明了它,也将其应用于PasswordBox使用它的对象。

要使用此附加属性,只需将其绑定到PasswordViewModel中的属性即可:

<PasswordBox w:PasswordHelper.Attach="True" 
         w:PasswordHelper.Password="{Binding Password}"/>

1

我已经完成了:

XAML:

<PasswordBox x:Name="NewPassword" PasswordChanged="NewPassword_PasswordChanged"/>
<!--change tablenameViewSource: yours!-->
<Grid DataContext="{StaticResource tablenameViewSource}" Visibility="Hidden">
        <TextBox x:Name="Password" Text="{Binding password, Mode=TwoWay}"/>
</Grid>

C#:

private void NewPassword_PasswordChanged(object sender, RoutedEventArgs e)
    {
        try
        {
           //change tablenameDataTable: yours! and tablenameViewSource: yours!
           tablenameDataTable.Rows[tablenameViewSource.View.CurrentPosition]["password"] = NewPassword.Password;
        }
        catch
        {
            this.Password.Text = this.NewPassword.Password;
        }
    }

这个对我有用!


你给我一个好主意。:)
安德烈·门登卡

1

如前所述,VM应该不了解View,但是传递整个PasswordBox看起来是最简单的方法。因此,也许不是将传递的参数强制转换为PasswordBox,而是使用Reflection从中提取Password属性。在这种情况下,VM需要具有属性Password(我正在使用MVMM Light-Toolkit中的RelayCommands)的某种Password Container:

public RelayCommand<object> SignIn
{
    get
    {
        if (this.signIn == null)
        {
            this.signIn = new RelayCommand<object>((passwordContainer) => 
                {
                    var password = passwordContainer.GetType().GetProperty("Password").GetValue(passwordContainer) as string;
                    this.authenticationService.Authenticate(this.Login, password);
                });
        }

        return this.signIn;
    }
}

可以使用匿名类轻松测试它:

var passwordContainer = new
    {
        Password = "password"
    };

评论不作进一步讨论;此对话已转移至聊天
塞缪尔·刘

1

在Windows通用应用中

您可以将此代码与属性“ Password”一起使用,并与modelView绑定

 <PasswordBox x:Uid="PasswordBox" Password="{Binding Waiter.Password, Mode=TwoWay}" Name="txtPassword" HorizontalAlignment="Stretch" Margin="50,200,50,0" VerticalAlignment="Top"/>


1

对于任何意识到此实现方式带来的风险的人,只需将Mode = OneWayToSource添加到您的ViewModel即可,以将密码同步到您的ViewModel 。

XAML

<PasswordBox
    ff:PasswordHelper.Attach="True"
    ff:PasswordHelper.Password="{Binding Path=Password, Mode=OneWayToSource}" />

为什么不做OneWayToSource呢?
BK 2015年

@BK编辑了我的答案。谢谢。
凯文

1
模式不应该在绑定括号内吗?
Mat 2002年

@Mat Yap。谢谢。
凯文

1

这是我的看法:

  1. 使用附加属性绑定密码会破坏保护密码的目的。密码框的“密码”属性由于某种原因无法绑定。

  2. 将密码框作为命令参数传递将使ViewModel知道该控件。如果您计划使ViewModel可重用的跨平台,则将无法使用。不要让您的VM知道您的View或任何其他控件。

  3. 我认为引入一个新的属性,一个接口,订阅密码更改的事件或任何其他复杂的事情对于提供密码这样一个简单的任务并不需要。

XAML

<PasswordBox x:Name="pbPassword" />
<Button Content="Login" Command="{Binding LoginCommand}" x:Name="btnLogin"/>

后面的代码-使用后面的代码不一定违反MVVM。只要您不添加任何业务逻辑。

btnLogin.CommandParameter = new Func<string>(()=>pbPassword.Password); 

视图模型

LoginCommand = new RelayCommand<Func<string>>(getpwd=> { service.Login(username, getpwd()); });

0

您可以在WPF应用程序框架(WAF)项目的ViewModel示例应用程序中找到PasswordBox的解决方案。

但是,贾斯汀是对的。不要在View和ViewModel之间以纯文本形式传递密码。请改用SecureString(请参阅MSDN PasswordBox)。


2
WAF的Pop3SettingsView中使用的方法很有趣。PasswordBox passwordBox =(PasswordBox)发送者; 如果(ViewModel!= null){ViewModel.Pop3Password = passwordBox.Password; } ViewModel的Pop3Password是string属性。因此,它也不安全..最好使用附加属性
Michael Sync 2010年

0

我使用了一个身份验证检查,然后使用一个由调解器类调用的子视图(该视图也实现了身份验证检查)将密码写入数据类。

这不是一个完美的解决方案。但是,它解决了我无法移动密码的问题。


0

我正在使用尚未提及的简洁的MVVM友好解决方案。首先,我在XAML中命名PasswordBox:

<PasswordBox x:Name="Password" />

然后我在视图构造函数中添加一个方法调用:

public LoginWindow()
{
    InitializeComponent();
    ExposeControl<LoginViewModel>.Expose(this, view => view.Password,
        (model, box) => model.SetPasswordBox(box));
}

就是这样。通过DataContext将视图模型附加到视图时,视图模型将收到通知,而从模型分离时,视图模型将收到另一个通知。该通知的内容可以通过lambda进行配置,但通常只是视图模型上的setter或方法调用,将有问题的控件作为参数传递。

通过使用视图公开界面而不是子控件,可以非常容易地使MVVM友好。

上面的代码依赖于我博客上发布的帮助程序类


0

我花了很长时间试图使它起作用。最后,我放弃了,只使用了DevExpress的PasswordBoxEdit。

这是有史以来最简单的解决方案,因为它允许绑定而不会产生任何可怕的技巧。

DevExpress网站上的解决方案

根据记录,我与DevExpress无关。


0

<UserControl x:Class="Elections.Server.Handler.Views.LoginView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
             xmlns:cal="http://www.caliburnproject.org"
             mc:Ignorable="d" 
             Height="531" Width="1096">
    <ContentControl>
        <ContentControl.Background>
            <ImageBrush/>
        </ContentControl.Background>
        <Grid >
            <Border BorderBrush="#FFABADB3" BorderThickness="1" HorizontalAlignment="Left" Height="23" Margin="900,100,0,0" VerticalAlignment="Top" Width="160">
                <TextBox TextWrapping="Wrap"/>
            </Border>
            <Border BorderBrush="#FFABADB3" BorderThickness="1" HorizontalAlignment="Left" Height="23" Margin="900,150,0,0" VerticalAlignment="Top" Width="160">
                <PasswordBox x:Name="PasswordBox"/>
            </Border>
            <Button Content="Login" HorizontalAlignment="Left" Margin="985,200,0,0" VerticalAlignment="Top" Width="75">
                <i:Interaction.Triggers>
                    <i:EventTrigger EventName="Click">
                        <cal:ActionMessage MethodName="Login">
                            <cal:Parameter Value="{Binding ElementName=PasswordBox}" />
                        </cal:ActionMessage>
                    </i:EventTrigger>
                </i:Interaction.Triggers>
            </Button>

        </Grid>
    </ContentControl>
</UserControl>

using System;
using System.Windows;
using System.Windows.Controls;
using Caliburn.Micro;

namespace Elections.Server.Handler.ViewModels
{
    public class LoginViewModel : PropertyChangedBase
    {
        MainViewModel _mainViewModel;
        public void SetMain(MainViewModel mainViewModel)
        {
            _mainViewModel = mainViewModel;
        }

        public void Login(Object password)
        {
            var pass = (PasswordBox) password;
            MessageBox.Show(pass.Password);

            //_mainViewModel.ScreenView = _mainViewModel.ControlPanelView;
            //_mainViewModel.TitleWindow = "Panel de Control";
            //HandlerBootstrapper.Title(_mainViewModel.TitleWindow);
        }
    }
}

;) 简单!


0

它非常简单。创建密码的另一个属性,并将其与TextBox绑定

但是所有输入操作都使用实际的密码属性执行

私人字串_Password;

    public string PasswordChar
    {
        get
        {
            string szChar = "";

            foreach(char szCahr in _Password)
            {
                szChar = szChar + "*";
            }

            return szChar;
        }

        set
        {
            _PasswordChar = value; NotifyPropertyChanged();
        }
    }

公共字符串密码{get {return _Password; }

        set
        {
            _Password = value; NotifyPropertyChanged();
            PasswordChar = _Password;
        }
    }


密码框不可绑定的原因是,我们不想将密码存储在清晰的字符串中。字符串是不可变的,我们不确定它将在内存中保留多长时间。
兰斯,

0

好吧,我的回答更简单了,只是针对MVVM模式

在类中

public string password;

PasswordChangedCommand = new DelegateCommand<RoutedEventArgs>(PasswordChanged);

Private void PasswordChanged(RoutedEventArgs obj)

{

    var e = (WatermarkPasswordBox)obj.OriginalSource;

    //or depending or what are you using

    var e = (PasswordBox)obj.OriginalSource;

    password =e.Password;

}

win提供的PasswordBox或XCeedtoolkit提供的WatermarkPasswordBox的password属性将生成RoutedEventArgs,以便您可以绑定它。

现在处于xmal视图

<Xceed:WatermarkPasswordBox Watermark="Input your Password" Grid.Column="1" Grid.ColumnSpan="3" Grid.Row="7" PasswordChar="*" >

        <i:Interaction.Triggers>

            <i:EventTrigger EventName="PasswordChanged">

                <prism:InvokeCommandAction Command="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path= DataContext.PasswordChangedCommand}" CommandParameter="{Binding RelativeSource={RelativeSource Self}, Path= Password}"/>

            </i:EventTrigger>

        </i:Interaction.Triggers>

    </Xceed:WatermarkPasswordBox>

要么

<PasswordBox Grid.Column="1" Grid.ColumnSpan="3" Grid.Row="7" PasswordChar="*" >

        <i:Interaction.Triggers>

            <i:EventTrigger EventName="PasswordChanged">

                <prism:InvokeCommandAction Command="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path= DataContext.PasswordChangedCommand}" CommandParameter="{Binding RelativeSource={RelativeSource Self}, Path= Password}"/>

            </i:EventTrigger>

        </i:Interaction.Triggers>

    </PasswordBox>

0

SecureString使用“附加行为”向视图模型发送ICommand

实现MVVM时,代码隐藏没有问题。MVVM是一种架构模式,旨在将视图与模型/业务逻辑分开。MVVM描述了如何以可重现的方式(模式)实现此目标。它不关心实现细节,例如如何构造或实现视图。它只是划定了边界,并根据此模式的术语定义了视图,视图模型以及模型。

MVVM不在乎语言(XAML或C#)或编译器(partial类)。语言独立是设计模式的强制性特征-必须与语言无关。

但是,隐藏代码在XAML和C#之间随意分布时,会有一些缺点,例如使UI逻辑更难以理解。但是,与使用XAML相比,在C#中最重要的实现UI逻辑或对象(如模板,样式,触发器,动画等)非常复杂且丑陋/可读性较差。XAML是一种标记语言,它使用标签和嵌套来可视化对象层次结构。使用XAML创建UI非常方便。尽管在某些情况下您可以选择用C#(或隐藏代码)实现UI逻辑。处理PasswordBox就是一个例子。

因此PasswordBox,通过处理PasswordBox.PasswordChanged并不违反MVVM模式。

明显的违规行为是将控件(PasswordBox)传递给视图模型。许多解决方案都建议这样做,例如,通过传递PasswordBoxas 的实例ICommand.CommandParameter将视图。显然,这是一个非常糟糕且不必要的建议。

如果您不关心使用C#,而只是想保持代码隐藏文件的清洁,或者只是想封装行为/ UI逻辑,则可以始终使用附加属性并实现附加行为。

与臭名昭著的广泛传播帮助程序相反,该帮助程序可以绑定到纯文本密码(确实存在不良的反模式和安全风险),每当引发事件时,此行为就会使用ICommand来向SecureString视图模型发送密码。PasswordBoxPasswordBox.PasswordChanged

MainWindow.xaml

<Window>
  <Window.DataContext>
    <ViewModel />
  </Window.DataContext>

  <PasswordBox PasswordBox.Command="{Binding VerifyPasswordCommand}" />
</Window>

ViewModel.cs

public class ViewModel : INotifyPropertyChanged
{
  public ICommand VerifyPasswordCommand => new RelayCommand(VerifyPassword);

  public void VerifyPassword(object commadParameter)
  {
    if (commandParameter is SecureString secureString)
    {
      IntPtr valuePtr = IntPtr.Zero;
      try
      {
        valuePtr = Marshal.SecureStringToGlobalAllocUnicode(value);
        string plainTextPassword = Marshal.PtrToStringUni(valuePtr);

        // Handle plain text password. 
        // It's recommended to convert the SecureString to plain text in the model, when really needed.
      } 
      finally 
      {
        Marshal.ZeroFreeGlobalAllocUnicode(valuePtr);
      }
    }
  }
}

密码箱

// Attached behavior
class PasswordBox : DependencyObject
{
  #region Command attached property

  public static readonly DependencyProperty CommandProperty =
    DependencyProperty.RegisterAttached(
      "Command",
      typeof(ICommand),
      typeof(PasswordBox),
      new PropertyMetadata(default(ICommand), PasswordBox.OnSendPasswordCommandChanged));

  public static void SetCommand(DependencyObject attachingElement, ICommand value) =>
    attachingElement.SetValue(PasswordBox.CommandProperty, value);

  public static ICommand GetCommand(DependencyObject attachingElement) =>
    (ICommand) attachingElement.GetValue(PasswordBox.CommandProperty);

  #endregion

  private static void OnSendPasswordCommandChanged(
    DependencyObject attachingElement,
    DependencyPropertyChangedEventArgs e)
  {
    if (!(attachingElement is System.Windows.Controls.PasswordBox passwordBox))
    {
      throw new ArgumentException("Attaching element must be of type 'PasswordBox'");
    }

    if (e.OldValue != null)
    {
      return;
    }

    WeakEventManager<object, RoutedEventArgs>.AddHandler(
      passwordBox,
      nameof(System.Windows.Controls.PasswordBox.PasswordChanged),
      SendPassword_OnPasswordChanged);
  }

  private static void SendPassword_OnPasswordChanged(object sender, RoutedEventArgs e)
  {
    var attachedElement = sender as System.Windows.Controls.PasswordBox;
    SecureString commandParameter = attachedElement?.SecurePassword;
    if (commandParameter == null || commandParameter.Length < 1)
    {
      return;
    }

    ICommand sendCommand = GetCommand(attachedElement);
    sendCommand?.Execute(commandParameter);
  }
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.