How to bind to a PasswordBox in MVVM

前端 未结 30 1726
执念已碎
执念已碎 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:20

    I figured I'd throw my solution in the mix, since this is such a common issue... and having plenty of options is always a good thing.

    I simply wrapped a PasswordBox in a UserControl and implemented a DependencyProperty to be able to bind. I'm doing everything I can to avoid storing any clear text in the memory, so everything is done through a SecureString and the PasswordBox.Password property. During the foreach loop, each character does get exposed, but it's very brief. Honestly, if you're worried about your WPF application to be compromised from this brief exposure, you've got bigger security issues that should be handled.

    The beauty of this is that you are not breaking any MVVM rules, even the "purist" ones, since this is a UserControl, so it's allowed to have code-behind. When you're using it, you can have pure communication between View and ViewModel without your VideModel being aware of any part of View or the source of the password. Just make sure you're binding to SecureString in your 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 (Version 1 - No two-way binding support.)

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

    Usage of Version 1:

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

    BindablePasswordBox.xaml.cs (Version 2 - Has two-way binding support.)

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

    Usage of Version 2:

    <local:BindanblePasswordBox Width="150" HorizontalAlignment="Center"
                                VerticalAlignment="Center"
                                Password="{Binding Password, Mode=TwoWay}"/>
    
    0 讨论(0)
  • 2020-11-22 12:20

    Here's my take on it:

    1. Using an attached property to bind the password defeats the purpose of securing the password. The Password property of a password box is not bindable for a reason.

    2. Passing the password box as command parameter will make the ViewModel aware of the control. This will not work if you plan to make your ViewModel reusable cross platform. Don't make your VM aware of your View or any other controls.

    3. I don't think introducing a new property, an interface, subscribing to password changed events or any other complicated things is necessary for a simple task of providing the password.

    XAML

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

    Code behind - using code behind does not necessarily violate MVVM. As long as you don't put any business logic in it.

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

    ViewModel

    LoginCommand = new RelayCommand<Func<string>>(getpwd=> { service.Login(username, getpwd()); });
    
    0 讨论(0)
  • 2020-11-22 12:21

    well my answerd is more simple just in the for the MVVM pattern

    in class viewmodel

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

    the password property of the PasswordBox that win provides or WatermarkPasswordBox that XCeedtoolkit provides generates an RoutedEventArgs so you can bind it.

    now in xmal view

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

    or

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

    You can use this XAML:

    <PasswordBox>
        <i:Interaction.Triggers>
            <i:EventTrigger EventName="PasswordChanged">
                <i:InvokeCommandAction Command="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=PasswordBox}}" CommandParameter="{Binding ElementName=PasswordBox}"/>
            </i:EventTrigger>
        </i:Interaction.Triggers>
    </PasswordBox>
    

    And this command execute method:

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

    This requires adding the System.Windows.Interactivity assembly to your project and referencing it via xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity".

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

    I have done like:

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

    It works for me!

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

    I posted a GIST here that is a bindable password box.

    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;
            }
        }
    }
    
    0 讨论(0)
提交回复
热议问题