How to bind to a PasswordBox in MVVM

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

    I spent ages trying to get this working. In the end, I gave up and just used the PasswordBoxEdit from DevExpress.

    It is the simplest solution ever, as it allows binding without pulling any horrible tricks.

    Solution on DevExpress website

    For the record, I am not affiliated with DevExpress in any way.

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

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

    ;) easy!

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

    A simple solution without violating the MVVM pattern is to introduce an event (or delegate) in the ViewModel that harvests the password.

    In the ViewModel:

    public event EventHandler<HarvestPasswordEventArgs> HarvestPassword;

    with these EventArgs:

    class HarvestPasswordEventArgs : EventArgs
    {
        public string Password;
    }
    

    in the View, subscribe to the event on creating the ViewModel and fill in the password value.

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

    In the ViewModel, when you need the password, you can fire the event and harvest the password from there:

    if (HarvestPassword == null)
      //bah 
      return;
    
    var pwargs = new HarvestPasswordEventArgs();
    HarvestPassword(this, pwargs);
    
    LoginHelpers.Login(Username, pwargs.Password);
    
    0 讨论(0)
  • 2020-11-22 12:17

    As you can see i am binding to Password, but maybe its bind it to the static class..

    It is an attached property. This kind of property can be applied to any kind of DependencyObject, not just the type in which it is declared. So even though it is declared in the PasswordHelper static class, it is applied to the PasswordBox on which you use it.

    To use this attached property, you just need to bind it to the Password property in your ViewModel :

    <PasswordBox w:PasswordHelper.Attach="True" 
             w:PasswordHelper.Password="{Binding Password}"/>
    
    0 讨论(0)
  • 2020-11-22 12:18

    Maybe I am missing something, but it seems like most of these solutions overcomplicate things and do away with secure practices.

    This method does not violate the MVVM pattern and maintains complete security. Yes, technically it is code behind, but it is nothing more than a "special case" binding. The ViewModel still has no knowledge of the View implementation, which in my mind it does if you are trying to pass the PasswordBox in to the ViewModel.

    Code Behind != Automatic MVVM violation. It all depends on what you do with it. In this case, we are just manually coding a binding, so its all considered part of the UI implementation and therefore is ok.

    In the ViewModel, just a simple property. I made it "write only" since there shouldn't be a need to retrieve it from outside the ViewModel for any reason, but it doesn't have to be. Note that it is a SecureString, not just a string.

    public SecureString SecurePassword { private get; set; }
    

    In the xaml, you set up a PasswordChanged event handler.

    <PasswordBox PasswordChanged="PasswordBox_PasswordChanged"/>
    

    In the code behind:

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

    With this method, your password remains in a SecureString at all times and therefore provides maximum security. If you really don't care about security or you need the clear text password for a downstream method that requires it (note: most .NET methods that require a password also support a SecureString option, so you may not really need a clear text password even if you think you do), you can just use the Password property instead. Like this:

    (ViewModel property)

    public string Password { private get; set; }
    

    (Code behind)

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

    If you wanted to keep things strongly typed, you could substitute the (dynamic) cast with the interface of your ViewModel. But really, "normal" data bindings aren't strongly typed either, so its not that big a deal.

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

    So best of all worlds - your password is secure, your ViewModel just has a property like any other property, and your View is self contained with no external references required.

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

    While I agree it's important to avoid storing the password anywhere, I still need the ability to instantiate the view model without a view and execute my tests against it.

    The solution that worked for me was to register the PasswordBox.Password function with the view model, and have the view model invoke it when executing the login code.

    This does mean a line of code in the view's codebehind.

    So, in my Login.xaml I have

    <PasswordBox x:Name="PasswordBox"/>
    

    and in Login.xaml.cs I have

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

    then in LoginViewModel.cs I have the PasswordHandler defined

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

    and when login needs to happen the code invokes the handler to get the password from the view...

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

    This way, when I want to test the viewmodel I can simply set PasswordHandler to an anonymous method that lets me deliver whatever password I want to use in the test.

    0 讨论(0)
提交回复
热议问题