web-dev-qa-db-fra.com

Comment se lier à un PasswordBox dans MVVM

J'ai rencontré un problème avec la liaison à un PasswordBox. Il semble que cela pose un risque pour la sécurité, mais j'utilise le modèle MVVM et je souhaite donc éviter cela. J'ai trouvé un code intéressant ici (est-ce que quelqu'un a utilisé ceci ou quelque chose de similaire?)

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

Techniquement, ça a l'air génial, mais je ne sais pas comment récupérer le mot de passe. 

J'ai essentiellement des propriétés dans ma LoginViewModel pour Username et Password. Username est bon et fonctionne car c'est un TextBox.

J'ai utilisé le code ci-dessus comme indiqué et entré ce

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

Lorsque j’avais la PasswordBox en tant que TextBox et Binding Path=Password, la propriété dans ma LoginViewModel était mise à jour.

Mon code est très simple. En gros, j'ai une Command pour ma Button. Lorsque j'appuie dessus, CanLogin est appelé et s'il renvoie true, il appelle Login.
Vous pouvez voir que je vérifie Username ma propriété ici, ce qui fonctionne très bien. 

Dans Login, j'envoie à mon service une Username et Password, Username contient des données de ma View mais Password est Null|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 { }
    }

C'est ce que je fais

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

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

J'ai ma TextBox, ce n'est pas un problème, mais dans ma ViewModel la Password est vide.

Est-ce que je fais quelque chose de mal ou manque une étape?

Je mets un point d'arrêt et le code entre bien sûr dans la classe d'assistance statique, mais il ne met jamais à jour ma Password dans ma ViewModel.

224
mark smith

Désolé, mais vous le faites mal.

Les consignes de sécurité suivantes doivent être tatouées à l’intérieur des paupières:
Ne conservez jamais de mots de passe en texte brut dans la mémoire.

La raison pour laquelle WPF/Silverlight PasswordBox n'expose pas de DP pour la propriété Password est liée à la sécurité.
Si WPF/Silverlight devait conserver un DP pour le mot de passe, il faudrait que la structure conserve le mot de passe non crypté en mémoire. Ce qui est considéré comme un vecteur d’attaque de sécurité assez fastidieux . PasswordBox utilise une mémoire cryptée (de sorte) et le seul moyen d’accéder au mot de passe est de passer par la propriété CLR. 

Je suggèrerais qu'en accédant à la propriété PasswordBox.Password CLR, vous évitiez de la placer dans une variable ou en tant que valeur d'une propriété.
Conserver votre mot de passe en clair sur la machine cliente RAM est une sécurité non-non.
Alors, débarrassez-vous de la "chaîne publique Mot de passe {get; set;}" que vous avez là-haut. 

Lorsque vous accédez à PasswordBox.Password, extrayez-le et envoyez-le au serveur dès que possible. Ne conservez pas la valeur du mot de passe et ne le traitez pas comme un texte de machine cliente. Ne gardez pas les mots de passe en mémoire en mémoire. 

Je sais que cela casse le modèle MVVM, mais vous ne devriez jamais vous lier à PasswordBox.Password Attached DP, stockez votre mot de passe dans ViewModel ou dans toute autre manigance similaire.

Si vous recherchez une solution trop architecturale, en voici une:
1. Créez l'interface IHavePassword avec une méthode qui renvoie le texte en clair du mot de passe.
2. Demandez à votre UserControl d'implémenter une interface IHavePassword.
3. Enregistrez l'instance UserControl auprès de votre IoC en implémentant l'interface IHavePassword.
4. Lorsqu'une requête du serveur nécessitant votre mot de passe est en cours, appelez votre IoC pour la mise en œuvre IHavePassword et obtenez seulement le mot de passe tant convoité.

Juste mon point de vue. 

-- Justin 

148
JustinAngel

Mes 2 centimes:

J'ai développé une fois un dialogue de connexion typique (zones utilisateur et mot de passe, plus bouton "Ok") en utilisant WPF et MVVM. J'ai résolu le problème de liaison de mot de passe en passant simplement le contrôle PasswordBox lui-même en tant que paramètre à la commande associée au bouton "Ok". Donc, dans la vue que j'avais:

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

Et dans le ViewModel, la méthode Execute de la commande attachée était la suivante:

void Execute(object parameter)
{
    var passwordBox = parameter as PasswordBox;
    var password = passwordBox.Password;
    //Now go ahead and check the user name and password
}

Cela viole légèrement le modèle MVVM puisque le ViewModel en sait quelque chose sur la mise en œuvre de View, mais je pouvais me le permettre dans ce projet particulier. J'espère que c'est utile pour quelqu'un aussi.

176
Konamiman

Peut-être me manque quelque chose, mais il semble que la plupart de ces solutions compliquent les choses trop et éliminent les pratiques sécurisées. 

Cette méthode ne viole pas le modèle MVVM et maintient une sécurité complète. Oui, techniquement, c'est du code en retard, mais ce n'est rien de plus qu'une liaison de "cas spécial". Le ViewModel n'a toujours aucune connaissance de l'implémentation de View, ce qui, à mon avis, le fait si vous essayez de transmettre PasswordBox au ViewModel. 

Code Behind! = Violation MVVM automatique. Tout dépend de ce que vous en faites. Dans ce cas, nous codons simplement une liaison manuellement, elle est donc considérée comme faisant partie de la mise en œuvre de l'interface utilisateur et est donc correcte.

Dans le ViewModel, juste une propriété simple. Je l'ai fait "en écriture seule" car il ne devrait pas être nécessaire de le récupérer de l'extérieur du ViewModel pour quelque raison que ce soit, mais ce n'est pas obligatoire. Notez que c'est un SecureString, pas simplement une chaîne.

public SecureString SecurePassword { private get; set; }

Dans le xaml, vous configurez un gestionnaire d'événements PasswordChanged.

<PasswordBox PasswordChanged="PasswordBox_PasswordChanged"/>

Dans le code derrière:

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

Avec cette méthode, votre mot de passe reste dans un SecureString à tout moment et offre donc une sécurité maximale. Si vous ne vous souciez pas vraiment de la sécurité ou si vous avez besoin du mot de passe en texte clair pour une méthode en aval qui l'exige (remarque: la plupart des méthodes .NET qui nécessitent un mot de passe prennent également en charge une option SecureString, vous n'avez donc peut-être pas besoin d'un mot de passe en texte clair. même si vous pensez le faire), vous pouvez simplement utiliser la propriété Password. Comme ça:

(Propriété ViewModel)

public string Password { private get; set; }

(Code derrière)

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

Si vous voulez que les choses restent bien typées, vous pouvez remplacer le casting (dynamique) par l'interface de votre ViewModel. Mais en réalité, les liaisons de données "normales" ne sont pas fortement typées non plus, donc ce n'est pas si grave.

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

Le meilleur de tous les mondes - votre mot de passe est sécurisé, votre ViewModel a une propriété comme toute autre propriété et votre vue est autonome, sans référence externe requise.

151
Steve In CO

Vous pouvez utiliser ce 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>

Et cette commande exécuter la méthode:

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

Cela fonctionne très bien pour moi.

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

Une solution simple sans violer le modèle MVVM consiste à introduire un événement (ou un délégué) dans le ViewModel qui recueille le mot de passe.

Dans le ViewModel :

public event EventHandler<HarvestPasswordEventArgs> HarvestPassword;

avec ces EventArgs:

class HarvestPasswordEventArgs : EventArgs
{
    public string Password;
}

dans le View , abonnez-vous à l'événement sur la création du ViewModel et entrez la valeur du mot de passe.

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

Dans le ViewModel , lorsque vous avez besoin du mot de passe, vous pouvez déclencher l'événement et récupérer le mot de passe à partir de cet emplacement:

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

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

LoginHelpers.Login(Username, pwargs.Password);
10
Jan Willem B

J'ai posté un Gist ici qui est une boîte de mot de passe pouvant être liée.

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;
        }
    }
}
8
Taylor Leese

Cette implémentation est légèrement différente. Vous transmettez un passwordbox à la liaison via une liaison d'une propriété dans ViewModel, elle n'utilise aucun paramètre de commande. Le ViewModel reste ignorant de la vue. J'ai un projet VB vs 2010 qui peut être téléchargé à partir de SkyDrive. Wpf MvvM PassWordBox Example.Zip https://skydrive.live.com/redir.aspx?cid=e95997d33a9f8d73&resid=E95997D33A9F8D73!511

J'utilise PasswordBox dans une application MvvM Wpf plutôt simpliste et fonctionne bien pour moi. Cela ne signifie pas que je pense que c'est la bonne façon ou la meilleure. Ce n'est qu'une implémentation de Using PasswordBox et du modèle MvvM.

De manière générale, vous créez une propriété publique readonly à laquelle la vue peut se lier en tant que PasswordBox (contrôle réel). Exemple:

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

J'utilise un champ de sauvegarde uniquement pour effectuer l'auto-initialisation de la propriété.

Ensuite, depuis Xaml, vous liez le contenu d'un ContentControl ou d'un conteneur de contrôle. Exemple:

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

À partir de là, vous avez le contrôle total de la boîte de mot de passe. J'utilise également un PasswordAccessor (juste une fonction de chaîne) pour renvoyer la valeur du mot de passe lorsque vous vous connectez ou quoi que vous fassiez pour lequel vous voulez utiliser le mot de passe. Dans l'exemple, j'ai une propriété publique dans un modèle d'objet utilisateur générique . Exemple: 

Public Property PasswordAccessor() As Func(Of String)

Dans l'objet utilisateur, la propriété de chaîne de mot de passe est en lecture seule, sans magasin de sauvegarde; elle renvoie simplement le mot de passe de PasswordBox . Exemple: 

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

Ensuite, dans le ViewModel, je m'assure que l'accesseur est créé et défini sur la propriété PasswordBox.Password ' Exemple:

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

Lorsque j'ai besoin de la chaîne Password, par exemple, pour la connexion, je viens d'obtenir la propriété User Objects Password qui appelle vraiment la fonction pour récupérer le mot de passe et le renvoyer. Le mot de passe actuel n'est pas stocké par l'objet User. Exemple: serait dans le 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

Ça devrait le faire. Le ViewModel n'a besoin d'aucune connaissance des commandes de la vue. La vue simplement lie à la propriété du modèle de vue, pas différente de la liaison de vue à une image ou à une autre ressource. Dans ce cas, cette ressource (Propriété) se trouve être simplement un contrôle utilisateur. Il permet d'effectuer des tests lorsque ViewModel crée et possède la propriété et que la propriété est indépendante de la vue . En ce qui concerne la sécurité, je ne sais pas à quel point cette implémentation est performante. Mais en utilisant une fonction, la valeur n'est pas stockée dans la propriété elle-même à laquelle accède la propriété.

6
William Rawson

Pour résoudre le problème de l'OP sans casser le MVVM, j'utiliserais un convertisseur de valeur personnalisé et un wrapper pour la valeur (le mot de passe) à extraire de la zone de mot de passe.

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

Dans le modèle de vue:

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

Étant donné que le modèle de vue utilise IWrappedParameter<T>, il n'est pas nécessaire qu'il ait connaissance de PasswordBoxWrapper ni PasswordBoxConverter. De cette façon, vous pouvez isoler l'objet PasswordBox du modèle de vue et ne pas casser le modèle MVVM.

Dans la vue:

<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}}" />
6
Aoi Karasu

Bien que je convienne qu'il est important d'éviter de stocker le mot de passe n'importe où, j'ai toujours besoin de la possibilité d'instancier le modèle de vue sans vue et d'exécuter mes tests.

La solution qui a fonctionné pour moi a consisté à enregistrer la fonction PasswordBox.Password avec le modèle de vue et à l'invoquer lors de l'exécution du code de connexion.

Ceci signifie signifie une ligne de code dans le codebehind de la vue.

Donc, dans mon Login.xaml j'ai

<PasswordBox x:Name="PasswordBox"/>

et dans Login.xaml.cs j'ai

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

puis dans LoginViewModel.cs j'ai le PasswordHandler défini

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

et lorsque la connexion doit avoir lieu, le code appelle le gestionnaire pour obtenir le mot de passe de la vue ...

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

Ainsi, lorsque je veux tester le modèle de vue, je peux simplement définir PasswordHandler sur une méthode anonyme qui me permet de fournir le mot de passe que je souhaite utiliser lors du test.

5
mike mckechnie

J'ai passé beaucoup de temps à chercher différentes solutions. Je n’ai pas aimé l’idée des décorateurs, les comportements bousillent l’interface utilisateur de validation, le code en retard… vraiment? 

La meilleure solution consiste à s'en tenir à une propriété attachée personnalisée et à lier votre propriété SecureString dans votre modèle de vue. Gardez-le là aussi longtemps que vous le pouvez. Chaque fois que vous aurez besoin d'un accès rapide au mot de passe brut, convertissez-le temporairement en chaîne non sécurisée à l'aide du code ci-dessous:

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

Assurez-vous de permettre au GC de collecter votre élément d'interface utilisateur. Résistez à la tentation d'utiliser un gestionnaire d'événements statique pour l'événement PasswordChanged sur la PasswordBox. J'ai également découvert une anomalie dans laquelle le contrôle ne mettait pas à jour l'interface utilisateur lors de l'utilisation de la propriété SecurePassword pour la configurer, raison pour laquelle je copie le mot de passe dans Password à la place. 

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

Et l'utilisation de XAML:

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

Ma propriété dans le modèle de vue ressemblait à ceci:

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

La RequiredSecureString est juste un simple validateur personnalisé qui a la logique suivante:

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

Ici vous l'avez. Une solution MVVM pure complète et testée.

4
MoonStom

J'ai utilisé cette méthode et transmis la boîte de mot de passe, bien que cela viole le MVVM, il était essentiel pour moi car j'utilisais un contrôle de contenu avec un modèle de données pour mon login dans mon Shell, qui est un environnement Shell complexe. Donc, accéder au code derrière Shell aurait été une merde.

Selon moi, transmettre la passwordbox est identique à accéder au contrôle à partir du code suivant. J'accepte les mots de passe, ne pas garder en mémoire, etc. Dans cette implémentation, je n'ai pas de propriété pour mot de passe dans le modèle de vue.

Bouton de commande

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

ViewModel

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

J'ai pensé que je mettrais ma solution dans le mélange, car il s'agit d'un problème si courant ... et avoir beaucoup d'options est toujours une bonne chose.

J'ai simplement enveloppé une PasswordBox dans une UserControl et mis en place une DependencyProperty pour pouvoir se lier. Je fais tout ce que je peux pour éviter de stocker du texte en clair dans la mémoire. Tout se fait par le biais d'une propriété SecureString et de la propriété PasswordBox.Password. Pendant la boucle foreach, chaque personnage est exposé, mais c'est très bref. Honnêtement, si vous craignez que votre application WPF ne soit compromise par cette brève exposition, vous avez de plus gros problèmes de sécurité à résoudre.

La beauté de ceci est que vous ne respectez pas les règles de MVVM, même les "puristes", car il s'agit d'une UserControl, il est donc permis d'avoir du code-behind. Lorsque vous l'utilisez, vous pouvez avoir une communication pure entre View et ViewModel sans que votre VideModel soit au courant d'une partie de View ou de la source du mot de passe. Assurez-vous simplement que vous êtes lié à SecureString dans votre 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 - Pas de support de liaison bidirectionnelle.)

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

Utilisation de la version 1:

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

BindablePasswordBox.xaml.cs (Version 2 - Prend en charge la liaison bidirectionnelle.)

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

Utilisation de la version 2:

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

vous pouvez le faire avec la propriété attachée, voyez-le .. PasswordBox avec MVVM

2
Rangel

Pour les débutants complets comme moi, voici un échantillon complet de ce que Konamiman a suggéré ci-dessus. Merci Konamiman.

XAML

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

ViewModel

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

Pour moi, ces deux choses ne sont pas bonnes:

  • Implémentation des propriétés de mot de passe en texte clair
  • Envoi de la PasswordBox en tant que paramètre de commande au ViewModel

Le transfert de SecurePassword (instance SecureString) comme décrit par Steve dans CO semble acceptable. Je préfère Behaviors au code en retard, et j’avais également l’exigence supplémentaire de pouvoir réinitialiser le mot de passe à partir du modèle de vue.

Xaml (Password est la propriété ViewModel):

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

Comportement:

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

    }
}
1
Mike Fuchs

Dans l'application universelle Windows

vous pouvez utiliser ce code avec la propriété "Mot de passe" et la liaison avec modelView

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

1
Baz08

Pour ceux qui sont conscients des risques que cette implémentation impose, pour que le mot de passe soit synchronisé avec votre ViewModel, ajoutez simplement Mode = OneWayToSource .

XAML

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

C'est très simple . Créez une autre propriété pour mot de passe et liez-la avec TextBox

Mais toutes les opérations de saisie fonctionnent avec la propriété de mot de passe réelle

chaîne privée _Password;

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

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

            return szChar;
        }

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

chaîne publique Mot de passe { obtenir { return _Password; }

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

1
Niji

Comme vous pouvez le voir, je suis lié à Password, mais peut-être que c'est lié à la classe statique.

C'est un propriété attachée . Ce type de propriété peut être appliqué à tout type de DependencyObject, pas seulement au type dans lequel il est déclaré. Ainsi, même s’il est déclaré dans la classe statique PasswordHelper, il est appliqué à la variable PasswordBox sur laquelle vous l’utilisez.

Pour utiliser cette propriété attachée, il vous suffit de la lier à la propriété Password dans votre ViewModel:

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

Comme mentionné précédemment, VM ne devrait pas être au courant de la vue, mais le passage complet de PasswordBox semble être la méthode la plus simple. Alors peut-être qu'au lieu de passer le paramètre passé à PasswordBox, utilisez Reflection pour en extraire la propriété Password. Dans ce cas, VM attend un conteneur de mots de passe avec la propriété Password (j'utilise RelayCommands de MVMM Light-Toolkit):

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

Il peut être facilement testé avec une classe anonyme:

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

Si vous voulez tout combiner en un seul contrôle et une seule commande

<PasswordBox Name="PasswordBoxPin" PasswordChar="*">
    <PasswordBox.InputBindings>
        <KeyBinding Key="Return" Command="{Binding AuthentifyEmpCommand}" CommandParameter="{Binding ElementName=PasswordBoxPin}"/>
    </PasswordBox.InputBindings>
</PasswordBox>

Et sur votre Vm (comme l'a montré Konamiman)

public void AuthentifyEmp(object obj)
{
    var passwordBox = obj as PasswordBox;
    var password = passwordBox.Password;
}
private RelayCommand _authentifyEmpCommand;
public RelayCommand AuthentifyEmpCommand => _authentifyEmpCommand ?? (_authentifyEmpCommand = new RelayCommand(AuthentifyEmp, null));
0
Cristian G

Vous trouvez une solution pour PasswordBox dans l'exemple d'application ViewModel du projet WPF Application Framework (WAF).

Cependant, Justin a raison. Ne transmettez pas le mot de passe sous forme de texte brut entre View et ViewModel. Utilisez plutôt SecureString (voir MSDN PasswordBox).

0
jbe

J'utilise une solution succincte compatible avec MVVM qui n'a pas encore été mentionnée. Tout d’abord, je nomme PasswordBox en XAML:

<PasswordBox x:Name="Password" />

Ensuite, j'ajoute un seul appel de méthode au constructeur de la vue:

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

Et c'est tout. Le modèle de vue reçoit une notification lorsqu'il est attaché à une vue via DataContext et une autre notification lorsqu'il est détaché. Le contenu de cette notification est configurable via les lambdas, mais il ne s'agit généralement que d'un appel de méthode ou de définition sur le modèle de vue, en transmettant le contrôle problématique en tant que paramètre.

Il est très facile de rendre MVVM-friendly en faisant en sorte que la vue expose une interface plutôt que des contrôles enfants.

Le code ci-dessus repose sur helper class publié sur mon blog.

0
Robert Važan

eh bien, mon answerd est plus simple que pour le modèle MVVM

en classe 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;

}

la propriété password de PasswordBox que win fournit ou WatermarkPasswordBox fournie par XCeedtoolkit génère un RoutedEventArgs afin que vous puissiez le lier.

maintenant en vue 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>

ou

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

J'ai fait comme:

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

Ça marche pour moi!

J'ai passé des heures à essayer de faire fonctionner cela. En fin de compte, j'ai abandonné et je viens d'utiliser le PasswordBoxEdit de DevExpress.

C'est la solution la plus simple à ce jour, car elle permet la reliure sans tirer d'horribles tours.

Solution sur le site DevExpress

Pour mémoire, je ne suis en aucun cas affilié à DevExpress.

0
Contango

J'ai utilisé un contrôle d'authentification suivi par un sous-utilisateur appelé par une classe de médiateur dans la vue (qui implémente également un contrôle d'authentification) pour écrire le mot de passe dans la classe de données.

Ce n'est pas une solution parfaite. cependant, cela a résolu mon problème de ne pas pouvoir déplacer le mot de passe.

0
Miles

Voici mon point de vue sur elle: 

  1. L'utilisation d'une propriété attachée pour lier le mot de passe annule l'objectif de sécurisation du mot de passe. La propriété Mot de passe d'une zone de mot de passe ne peut pas être liée pour une raison.

  2. Si vous transmettez le mot de passe en tant que paramètre de commande, ViewModel sera au courant du contrôle. Cela ne fonctionnera pas si vous envisagez de transformer votre plateforme multiplateforme réutilisable ViewModel. N'informez pas votre VM de votre vue ou de tout autre contrôle.

  3. Je ne pense pas que l'introduction d'une nouvelle propriété, d'une interface, la souscription à des événements de mot de passe modifié ou toute autre chose compliquée soit nécessaire pour une tâche simple consistant à fournir le mot de passe. 

XAML

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

Code derrière - l'utilisation de code derrière ne viole pas nécessairement MVVM. Tant que vous n'y mettez aucune logique métier.

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

ViewModel

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

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

;) facile! 

0
Hector Lobo