How to bind to a PasswordBox in MVVM

前端 未结 30 1724
执念已碎
执念已碎 2020-11-22 11:50

I have come across a problem with binding to a PasswordBox. It seems it\'s a security risk but I am using the MVVM pattern so I wish to bypass this. I found som

相关标签:
30条回答
  • 2020-11-22 12:27

    You find a solution for the PasswordBox in the ViewModel sample application of the WPF Application Framework (WAF) project.

    However, Justin is right. Don't pass the password as plain text between View and ViewModel. Use SecureString instead (See MSDN PasswordBox).

    0 讨论(0)
  • 2020-11-22 12:31

    To solve the OP problem without breaking the MVVM, I would use custom value converter and a wrapper for the value (the password) that has to be retrieved from the password box.

    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.");
        }
    }
    

    In the view model:

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

    Because the view model uses IWrappedParameter<T>, it does not need to have any knowledge about PasswordBoxWrapper nor PasswordBoxConverter. This way you can isolate the PasswordBox object from the view model and not break the MVVM pattern.

    In the view:

    <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}}" />
    
    0 讨论(0)
  • 2020-11-22 12:31

    Send a SecureString to the view model using an Attached Behavior and ICommand

    There is nothing wrong with code-behind when implementing MVVM. MVVM is an architectural pattern that aims to separate the view from the model/business logic. MVVM describes how to achieve this goal in a reproducible way (the pattern). It doesn't care about implementation details, like how do you structure or implement the view. It just draws the boundaries and defines what is the view, the view model and what the model in terms of this pattern's terminology.

    MVVM doesn't care about the language (XAML or C#) or compiler (partial classes). Being language independent is a mandatory characteristic of a design pattern - it must be language neutral.

    However, code-behind has some draw backs like making your UI logic harder to understand, when it is wildly distributed between XAML and C#. But most important implementing UI logic or objects like templates, styles, triggers, animations etc in C# is very complex and ugly/less readable than using XAML. XAML is a markup language that uses tags and nesting to visualize object hierarchy. Creating UI using XAML is very convenient. Although there are situations where you are fine choosing to implement UI logic in C# (or code-behind). Handling the PasswordBox is one example.

    For this reasons handling the PasswordBox in the code-behind by handling the PasswordBox.PasswordChanged, is no violation of the MVVM pattern.

    A clear violation would be to pass a control (the PasswordBox) to the view model. Many solutions recommend this e.g., bay passing the instance of the PasswordBox as ICommand.CommandParameter to the view model. Obviously a very bad and unnecessary recommendation.

    If you don't care about using C#, but just want to keep your code-behind file clean or simply want to encapsulate a behavior/UI logic, you can always make use of attached properties and implement an attached behavior.

    Opposed of the infamous wide spread helper that enables binding to the plain text password (really bad anti-pattern and security risk), this behavior uses an ICommand to send the password as SecureString to the view model, whenever the PasswordBox raises the PasswordBox.PasswordChanged event.

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

    PasswordBox.cs

    // 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);
      }
    }
    
    0 讨论(0)
  • 2020-11-22 12:33

    I spent a great deal of time looking at various solutions. I didn't like the decorators idea, behaviors mess up the validation UI, code behind... really?

    The best one yet is to stick to a custom attached property and bind to your SecureString property in your view model. Keep it in there for as long as you can. Whenever you'll need quick access to the plain password, temporarily convert it to an unsecure string using the code below:

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

    Make sure you allow the GC to collect your UI element, so resist the urge of using a static event handler for the PasswordChanged event on the PasswordBox. I also discovered an anomaly where the control wasn't updating the UI when using the SecurePassword property for setting it up, reason why I'm copying the password into Password instead.

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

    And the XAML usage:

    <PasswordBox controls:PasswordBoxHelper.SecurePassword="{Binding LogonPassword, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}">
    

    My property in the view model looked like this:

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

    The RequiredSecureString is just a simple custom validator that has the following logic:

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

    Here you have it. A complete and tested pure MVVM solution.

    0 讨论(0)
  • 2020-11-22 12:33

    This implementation is slightly different. You pass a passwordbox to the View thru binding of a property in ViewModel, it doesn't use any command params. The ViewModel Stays Ignorant of the View. I have a VB vs 2010 Project that can be downloaded from SkyDrive. Wpf MvvM PassWordBox Example.zip https://skydrive.live.com/redir.aspx?cid=e95997d33a9f8d73&resid=E95997D33A9F8D73!511

    The way that I am Using PasswordBox in a Wpf MvvM Application is pretty simplistic and works well for Me. That does not mean that I think it is the correct way or the best way. It is just an implementation of Using PasswordBox and the MvvM Pattern.

    Basicly You create a public readonly property that the View can bind to as a PasswordBox (The actual control) Example:

    Private _thePassWordBox As PasswordBox
    Public ReadOnly Property ThePassWordBox As PasswordBox
        Get
            If IsNothing(_thePassWordBox) Then _thePassWordBox = New PasswordBox
            Return _thePassWordBox
        End Get
    End Property
    

    I use a backing field just to do the self Initialization of the property.

    Then From Xaml you bind the Content of a ContentControl or a Control Container Example:

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

    From there you have full control of the passwordbox I also use a PasswordAccessor (Just a Function of String) to return the Password Value when doing login or whatever else you want the Password for. In the Example I have a public property in a Generic User Object Model. Example:

    Public Property PasswordAccessor() As Func(Of String)
    

    In the User Object the password string property is readonly without any backing store it just returns the Password from the PasswordBox. Example:

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

    Then in the ViewModel I make sure that the Accessor is created and set to the PasswordBox.Password property' Example:

    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
    

    When I need the Password string say for login I just get the User Objects Password property that really invokes the Function to grab the password and return it, then the actual password is not stored by the User Object. Example: would be in the 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
    

    That should Do It. The ViewModel doesn't need any knowledge of the View's Controls. The View Just binds to property in the ViewModel, not any different than the View Binding to an Image or Other Resource. In this case that resource(Property) just happens to be a usercontrol. It allows for testing as the ViewModel creates and owns the Property and the Property is independent of the View. As for Security I don't know how good this implementation is. But by using a Function the Value is not stored in the Property itself just accessed by the Property.

    0 讨论(0)
  • 2020-11-22 12:33

    For anyone who is aware of the risks this implementation imposes, to have the password sync to your ViewModel simply add Mode=OneWayToSource.

    XAML

    <PasswordBox
        ff:PasswordHelper.Attach="True"
        ff:PasswordHelper.Password="{Binding Path=Password, Mode=OneWayToSource}" />
    
    0 讨论(0)
提交回复
热议问题