Как привязать к PasswordBox в MVVM
Я столкнулся с проблемой привязки к PasswordBox. Кажется, это угроза безопасности, но я использую шаблон MVVM, поэтому я хочу обойти это. Я нашел интересный код здесь (кто-нибудь использовал это или что-то подобное?)
http://www.wpftutorial.net/PasswordBox.html
Технически это выглядит великолепно, но я не уверен, как восстановить пароль.
У меня в основном есть свойства в моем LoginViewModel
за Username
а также Password
, Username
хорошо и работает, как это TextBox
,
Я использовал код выше, как указано, и ввел это
<PasswordBox ff:PasswordHelper.Attach="True"
ff:PasswordHelper.Password="{Binding Path=Password}" Width="130"/>
Когда я имел PasswordBox
как TextBox
а также Binding Path=Password
тогда собственность в моем LoginViewModel
был обновлен.
Мой код очень прост, в основном у меня есть Command
для меня Button
, Когда я нажимаю это CanLogin
называется, и если он возвращает истину, он вызывает Login
,
Вы можете видеть, что я проверяю свою собственность на Username
здесь, который прекрасно работает.
В Login
Я посылаю к себе на службу Username
а также Password
, Username
содержит данные из моего View
но Password
является 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 { }
}
Это то что я делаю
<TextBox Text="{Binding Path=Username, UpdateSourceTrigger=PropertyChanged}"
MinWidth="180" />
<PasswordBox ff:PasswordHelper.Attach="True"
ff:PasswordHelper.Password="{Binding Path=Password}" Width="130"/>
У меня есть мой TextBox
это не проблема, но по моему ViewModel
Password
пустой.
Я делаю что-то неправильно или пропускаю шаг?
Я поставил точку останова и достаточно уверенно, что код входит в статический вспомогательный класс, но он никогда не обновляет мой Password
в моем ViewModel
,
32 ответа
Извините, но вы делаете это неправильно.
Люди должны иметь следующие татуировки на внутренней стороне век:
Никогда не храните простые текстовые пароли в памяти.
Причина, по которой WPF/Silverlight PasswordBox не предоставляет DP для свойства Password, связана с безопасностью.
Если бы WPF / Silverlight сохранял DP for Password, он потребовал бы от среды, чтобы сам пароль не был зашифрован в памяти. Что считается довольно неприятным вектором атаки безопасности. PasswordBox использует зашифрованную память (своего рода), и единственный способ получить доступ к паролю - через свойство CLR.
Я хотел бы предложить, чтобы при доступе к свойству CLR PasswordBox.Password вы воздерживались от размещения его в любой переменной или в качестве значения для любого свойства.
Хранение пароля в виде обычного текста на оперативной памяти клиентского компьютера - это безопасность, нет-нет.
Так что избавьтесь от той "публичной строки Password { get; set; }", которую вы там получили.
При доступе к PasswordBox.Password просто достаньте его и отправьте на сервер как можно скорее. Не храните значение пароля и не относитесь к нему, как к любому другому тексту клиентского компьютера. Не храните пароли в виде открытого текста в памяти.
Я знаю, что это нарушает шаблон MVVM, но вам никогда не следует связываться с PasswordBox. Пароль, прикрепленный к DP, сохраняйте свой пароль в ViewModel или любых других подобных махинациях.
Если вы ищете решение с чрезмерной архитектурой, вот одно:
1. Создайте интерфейс IHavePassword с помощью одного метода, который возвращает открытый текст пароля.
2. Сделайте так, чтобы ваш UserControl реализовал интерфейс IHavePassword.
3. Зарегистрируйте экземпляр UserControl в IoC как реализующий интерфейс IHavePassword.
4. Когда выполняется запрос на сервер, требующий вашего пароля, позвоните в IoC для реализации IHavePassword и только после этого получите столь желанный пароль.
Просто мой взгляд на это.
Джастин
Может быть, я что-то упускаю, но кажется, что большинство из этих решений усложняют вещи и избавляются от безопасных практик.
Этот метод не нарушает шаблон MVVM и обеспечивает полную безопасность. Да, технически это код, но это не что иное, как "особый случай". ViewModel все еще не знает о реализации View, что, на мой взгляд, происходит, если вы пытаетесь передать PasswordBox в ViewModel.
Код позади!= Автоматическое нарушение MVVM. Все зависит от того, что вы делаете с этим. В этом случае мы просто вручную кодируем привязку, поэтому она считается частью реализации пользовательского интерфейса и, следовательно, в порядке.
Во ViewModel просто простое свойство. Я сделал это "только для записи", так как не должно быть необходимости извлекать его из-за пределов ViewModel по любой причине, но это не обязательно. Обратите внимание, что это SecureString, а не просто строка.
public SecureString SecurePassword { private get; set; }
В xaml вы устанавливаете обработчик событий PasswordChanged.
<PasswordBox PasswordChanged="PasswordBox_PasswordChanged"/>
В коде позади:
private void PasswordBox_PasswordChanged(object sender, RoutedEventArgs e)
{
if (this.DataContext != null)
{ ((dynamic)this.DataContext).SecurePassword = ((PasswordBox)sender).SecurePassword; }
}
При использовании этого метода ваш пароль всегда остается в SecureString и, следовательно, обеспечивает максимальную безопасность. Если вы действительно не заботитесь о безопасности или вам нужен открытый текстовый пароль для нижестоящего метода, который требует его (примечание: большинство методов.NET, для которых требуется пароль, также поддерживают параметр SecureString, поэтому вам может не понадобиться открытый текстовый пароль даже если вы так думаете) вы можете просто использовать свойство Password. Как это:
(Свойство ViewModel)
public string Password { private get; set; }
(Код позади)
private void PasswordBox_PasswordChanged(object sender, RoutedEventArgs e)
{
if (this.DataContext != null)
{ ((dynamic)this.DataContext).Password = ((PasswordBox)sender).Password; }
}
Если вы хотите сохранить строгую типизацию, вы можете заменить (динамическое) приведение интерфейсом вашей ViewModel. Но на самом деле "нормальные" привязки данных также не являются строго типизированными, так что это не такая уж большая проблема.
private void PasswordBox_PasswordChanged(object sender, RoutedEventArgs e)
{
if (this.DataContext != null)
{ ((IMyViewModel)this.DataContext).Password = ((PasswordBox)sender).Password; }
}
Так что лучше всего - ваш пароль надежен, у вашей ViewModel просто есть свойство, как у любого другого свойства, и ваш View самодостаточен без внешних ссылок.
Мои 2 цента:
Однажды я разработал типичный диалог входа в систему (поля пользователя и пароля, а также кнопку "ОК") с использованием WPF и MVVM. Я решил проблему с привязкой пароля, просто передав сам элемент управления PasswordBox в качестве параметра команде, прикрепленной к кнопке "ОК". Итак, по мнению, я имел:
<PasswordBox Name="txtPassword" VerticalAlignment="Top" Width="120" />
<Button Content="Ok" Command="{Binding Path=OkCommand}"
CommandParameter="{Binding ElementName=txtPassword}"/>
И в ViewModel, Execute
Метод прилагаемой команды был следующим:
void Execute(object parameter)
{
var passwordBox = parameter as PasswordBox;
var password = passwordBox.Password;
//Now go ahead and check the user name and password
}
Это немного нарушает шаблон MVVM, так как теперь ViewModel знает кое-что о том, как реализован вид, но в этом конкретном проекте я мог себе это позволить. Надеюсь, это будет полезно и для кого-то.
Вы можете использовать этот XAML:
<PasswordBox Name="PasswordBox">
<i:Interaction.Triggers>
<i:EventTrigger EventName="PasswordChanged">
<i:InvokeCommandAction Command="{Binding PasswordChangedCommand}" CommandParameter="{Binding ElementName=PasswordBox}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</PasswordBox>
И эта команда выполняет метод:
private void ExecutePasswordChangedCommand(PasswordBox obj)
{
if (obj != null)
Password = obj.Password;
}
Я провел много времени, глядя на различные решения. Мне не понравилась идея декораторов, поведение испортило интерфейс проверки, код позади... правда?
Лучше всего придерживаться привязанного свойства и привязать к вашему SecureString
недвижимость в вашем представлении модели. Держите его там как можно дольше. Всякий раз, когда вам понадобится быстрый доступ к простому паролю, временно преобразуйте его в незащищенную строку, используя код ниже:
namespace Namespace.Extensions
{
using System;
using System.Runtime.InteropServices;
using System.Security;
/// <summary>
/// Provides unsafe temporary operations on secured strings.
/// </summary>
[SuppressUnmanagedCodeSecurity]
public static class SecureStringExtensions
{
/// <summary>
/// Converts a secured string to an unsecured string.
/// </summary>
public static string ToUnsecuredString(this SecureString secureString)
{
// copy&paste from the internal System.Net.UnsafeNclNativeMethods
IntPtr bstrPtr = IntPtr.Zero;
if (secureString != null)
{
if (secureString.Length != 0)
{
try
{
bstrPtr = Marshal.SecureStringToBSTR(secureString);
return Marshal.PtrToStringBSTR(bstrPtr);
}
finally
{
if (bstrPtr != IntPtr.Zero)
Marshal.ZeroFreeBSTR(bstrPtr);
}
}
}
return string.Empty;
}
/// <summary>
/// Copies the existing instance of a secure string into the destination, clearing the destination beforehand.
/// </summary>
public static void CopyInto(this SecureString source, SecureString destination)
{
destination.Clear();
foreach (var chr in source.ToUnsecuredString())
{
destination.AppendChar(chr);
}
}
/// <summary>
/// Converts an unsecured string to a secured string.
/// </summary>
public static SecureString ToSecuredString(this string plainString)
{
if (string.IsNullOrEmpty(plainString))
{
return new SecureString();
}
SecureString secure = new SecureString();
foreach (char c in plainString)
{
secure.AppendChar(c);
}
return secure;
}
}
}
Убедитесь, что вы разрешаете GC собирать ваш элемент пользовательского интерфейса, поэтому не поддавайтесь искушению использовать статический обработчик событий для PasswordChanged
событие на PasswordBox
, Я также обнаружил аномалию, когда элемент управления не обновлял пользовательский интерфейс при использовании SecurePassword
свойство для настройки, причина, почему я копирую пароль в Password
вместо.
namespace Namespace.Controls
{
using System.Security;
using System.Windows;
using System.Windows.Controls;
using Namespace.Extensions;
/// <summary>
/// Creates a bindable attached property for the <see cref="PasswordBox.SecurePassword"/> property.
/// </summary>
public static class PasswordBoxHelper
{
// an attached behavior won't work due to view model validation not picking up the right control to adorn
public static readonly DependencyProperty SecurePasswordBindingProperty = DependencyProperty.RegisterAttached(
"SecurePassword",
typeof(SecureString),
typeof(PasswordBoxHelper),
new FrameworkPropertyMetadata(new SecureString(),FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, AttachedPropertyValueChanged)
);
private static readonly DependencyProperty _passwordBindingMarshallerProperty = DependencyProperty.RegisterAttached(
"PasswordBindingMarshaller",
typeof(PasswordBindingMarshaller),
typeof(PasswordBoxHelper),
new PropertyMetadata()
);
public static void SetSecurePassword(PasswordBox element, SecureString secureString)
{
element.SetValue(SecurePasswordBindingProperty, secureString);
}
public static SecureString GetSecurePassword(PasswordBox element)
{
return element.GetValue(SecurePasswordBindingProperty) as SecureString;
}
private static void AttachedPropertyValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
// we'll need to hook up to one of the element's events
// in order to allow the GC to collect the control, we'll wrap the event handler inside an object living in an attached property
// don't be tempted to use the Unloaded event as that will be fired even when the control is still alive and well (e.g. switching tabs in a tab control)
var passwordBox = (PasswordBox)d;
var bindingMarshaller = passwordBox.GetValue(_passwordBindingMarshallerProperty) as PasswordBindingMarshaller;
if (bindingMarshaller == null)
{
bindingMarshaller = new PasswordBindingMarshaller(passwordBox);
passwordBox.SetValue(_passwordBindingMarshallerProperty, bindingMarshaller);
}
bindingMarshaller.UpdatePasswordBox(e.NewValue as SecureString);
}
/// <summary>
/// Encapsulated event logic
/// </summary>
private class PasswordBindingMarshaller
{
private readonly PasswordBox _passwordBox;
private bool _isMarshalling;
public PasswordBindingMarshaller(PasswordBox passwordBox)
{
_passwordBox = passwordBox;
_passwordBox.PasswordChanged += this.PasswordBoxPasswordChanged;
}
public void UpdatePasswordBox(SecureString newPassword)
{
if (_isMarshalling)
{
return;
}
_isMarshalling = true;
try
{
// setting up the SecuredPassword won't trigger a visual update so we'll have to use the Password property
_passwordBox.Password = newPassword.ToUnsecuredString();
// you may try the statement below, however the benefits are minimal security wise (you still have to extract the unsecured password for copying)
//newPassword.CopyInto(_passwordBox.SecurePassword);
}
finally
{
_isMarshalling = false;
}
}
private void PasswordBoxPasswordChanged(object sender, RoutedEventArgs e)
{
// copy the password into the attached property
if (_isMarshalling)
{
return;
}
_isMarshalling = true;
try
{
SetSecurePassword(_passwordBox, _passwordBox.SecurePassword.Copy());
}
finally
{
_isMarshalling = false;
}
}
}
}
}
И использование XAML:
<PasswordBox controls:PasswordBoxHelper.SecurePassword="{Binding LogonPassword, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}">
Моя собственность в виде модели выглядела так:
[RequiredSecureString]
public SecureString LogonPassword
{
get
{
return _logonPassword;
}
set
{
_logonPassword = value;
NotifyPropertyChanged(nameof(LogonPassword));
}
}
RequiredSecureString
это простой пользовательский валидатор, который имеет следующую логику:
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = true)]
public class RequiredSecureStringAttribute:ValidationAttribute
{
public RequiredSecureStringAttribute()
:base("Field is required")
{
}
public override bool IsValid(object value)
{
return (value as SecureString)?.Length > 0;
}
}
Вот, пожалуйста. Полное и проверенное чистое решение MVVM.
Это прекрасно работает для меня.
<Button Command="{Binding Connect}"
CommandParameter="{Binding ElementName=MyPasswordBox}"/>
Простое решение без нарушения шаблона MVVM состоит в том, чтобы ввести событие (или делегат) в ViewModel, которая собирает пароль.
В ViewModel:
public event EventHandler<HarvestPasswordEventArgs> HarvestPassword;
с этими EventArgs:
class HarvestPasswordEventArgs : EventArgs
{
public string Password;
}
в представлении подпишитесь на событие при создании ViewModel и введите значение пароля.
_viewModel.HarvestPassword += (sender, args) =>
args.Password = passwordBox1.Password;
В ViewModel, когда вам нужен пароль, вы можете запустить событие и получить пароль оттуда:
if (HarvestPassword == null)
//bah
return;
var pwargs = new HarvestPasswordEventArgs();
HarvestPassword(this, pwargs);
LoginHelpers.Login(Username, pwargs.Password);
Я разместил здесь GIST, который является привязываемым паролем.
using System.Windows;
using System.Windows.Controls;
namespace CustomControl
{
public class BindablePasswordBox : Decorator
{
/// <summary>
/// The password dependency property.
/// </summary>
public static readonly DependencyProperty PasswordProperty;
private bool isPreventCallback;
private RoutedEventHandler savedCallback;
/// <summary>
/// Static constructor to initialize the dependency properties.
/// </summary>
static BindablePasswordBox()
{
PasswordProperty = DependencyProperty.Register(
"Password",
typeof(string),
typeof(BindablePasswordBox),
new FrameworkPropertyMetadata("", FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, new PropertyChangedCallback(OnPasswordPropertyChanged))
);
}
/// <summary>
/// Saves the password changed callback and sets the child element to the password box.
/// </summary>
public BindablePasswordBox()
{
savedCallback = HandlePasswordChanged;
PasswordBox passwordBox = new PasswordBox();
passwordBox.PasswordChanged += savedCallback;
Child = passwordBox;
}
/// <summary>
/// The password dependency property.
/// </summary>
public string Password
{
get { return GetValue(PasswordProperty) as string; }
set { SetValue(PasswordProperty, value); }
}
/// <summary>
/// Handles changes to the password dependency property.
/// </summary>
/// <param name="d">the dependency object</param>
/// <param name="eventArgs">the event args</param>
private static void OnPasswordPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs eventArgs)
{
BindablePasswordBox bindablePasswordBox = (BindablePasswordBox) d;
PasswordBox passwordBox = (PasswordBox) bindablePasswordBox.Child;
if (bindablePasswordBox.isPreventCallback)
{
return;
}
passwordBox.PasswordChanged -= bindablePasswordBox.savedCallback;
passwordBox.Password = (eventArgs.NewValue != null) ? eventArgs.NewValue.ToString() : "";
passwordBox.PasswordChanged += bindablePasswordBox.savedCallback;
}
/// <summary>
/// Handles the password changed event.
/// </summary>
/// <param name="sender">the sender</param>
/// <param name="eventArgs">the event args</param>
private void HandlePasswordChanged(object sender, RoutedEventArgs eventArgs)
{
PasswordBox passwordBox = (PasswordBox) sender;
isPreventCallback = true;
Password = passwordBox.Password;
isPreventCallback = false;
}
}
}
Чтобы решить проблему OP, не нарушая MVVM, я бы использовал конвертер пользовательских значений и оболочку для значения (пароля), которое необходимо извлечь из поля пароля.
public interface IWrappedParameter<T>
{
T Value { get; }
}
public class PasswordBoxWrapper : IWrappedParameter<string>
{
private readonly PasswordBox _source;
public string Value
{
get { return _source != null ? _source.Password : string.Empty; }
}
public PasswordBoxWrapper(PasswordBox source)
{
_source = source;
}
}
public class PasswordBoxConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
// Implement type and value check here...
return new PasswordBoxWrapper((PasswordBox)value);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new InvalidOperationException("No conversion.");
}
}
В представлении модель:
public string Username { get; set; }
public ICommand LoginCommand
{
get
{
return new RelayCommand<IWrappedParameter<string>>(password => { Login(Username, password); });
}
}
private void Login(string username, string password)
{
// Perform login here...
}
Поскольку модель представления использует IWrappedParameter<T>
, это не должно иметь никаких знаний о PasswordBoxWrapper
ни PasswordBoxConverter
, Таким образом, вы можете изолировать PasswordBox
объект из модели представления и не нарушать шаблон MVVM.
По мнению:
<Window.Resources>
<h:PasswordBoxConverter x:Key="PwdConverter" />
</Window.Resources>
...
<PasswordBox Name="PwdBox" />
<Button Content="Login" Command="{Binding LoginCommand}"
CommandParameter="{Binding ElementName=PwdBox, Converter={StaticResource PwdConverter}}" />
Эта реализация немного отличается. Вы передаете поле пароля для привязки View через свойство в ViewModel, оно не использует никаких параметров команды. Модель представления остается неосведомленной о представлении. У меня есть проект VB vs 2010, который можно загрузить со SkyDrive. Wpf MvvM PassWordBox Example.zip https://skydrive.live.com/redir.aspx?cid=e95997d33a9f8d73&resid=E95997D33A9F8D73!511
То, как я использую PasswordBox в приложении Wpf MvvM, довольно упрощенно и хорошо работает для меня. Это не значит, что я думаю, что это правильный или лучший способ. Это всего лишь реализация использования PasswordBox и MvvM Pattern.
По сути, вы создаете общедоступное свойство только для чтения, к которому представление может привязываться как PasswordBox (фактический элемент управления) Пример:
Private _thePassWordBox As PasswordBox
Public ReadOnly Property ThePassWordBox As PasswordBox
Get
If IsNothing(_thePassWordBox) Then _thePassWordBox = New PasswordBox
Return _thePassWordBox
End Get
End Property
Я использую вспомогательное поле только для самостоятельной инициализации свойства.
Затем из Xaml вы связываете содержимое ContentControl или Контейнер элемента управления. Пример:
<ContentControl Grid.Column="1" Grid.Row="1" Height="23" Width="120" Content="{Binding Path=ThePassWordBox}" HorizontalAlignment="Center" VerticalAlignment="Center" />
Оттуда у вас есть полный контроль над полем паролей. Я также использую PasswordAccessor (просто функция строки) для возврата значения пароля при входе в систему или для чего-либо еще, для чего вы хотите пароль. В Примере у меня есть открытое свойство в Общей Объектной Модели Пользователя. Пример:
Public Property PasswordAccessor() As Func(Of String)
В пользовательском объекте свойство строки пароля доступно только для чтения без какого-либо резервного хранилища, оно просто возвращает пароль из PasswordBox. Пример:
Public ReadOnly Property PassWord As String
Get
Return If((PasswordAccessor Is Nothing), String.Empty, PasswordAccessor.Invoke())
End Get
End Property
Затем в ViewModel я проверяю, что Accessor создан и установлен в свойство PasswordBox.Password 'Пример:
Public Sub New()
'Sets the Accessor for the Password Property
SetPasswordAccessor(Function() ThePassWordBox.Password)
End Sub
Friend Sub SetPasswordAccessor(ByVal accessor As Func(Of String))
If Not IsNothing(VMUser) Then VMUser.PasswordAccessor = accessor
End Sub
Когда мне нужна строка "Пароль", скажем, для входа в систему, я просто получаю свойство "Пароль объекта пользователя", которое действительно вызывает функцию, чтобы получить пароль и вернуть его, тогда фактический пароль не сохраняется объектом пользователя. Пример: будет в ViewModel
Private Function LogIn() as Boolean
'Make call to your Authentication methods and or functions. I usally place that code in the Model
Return AuthenticationManager.Login(New UserIdentity(User.UserName, User.Password)
End Function
Это должно сделать это. ViewModel не требует никаких знаний об элементах управления View. Представление просто привязывается к свойству в ViewModel, ничем не отличающимся от представления, связывающегося с изображением или другим ресурсом. В этом случае этот ресурс (свойство) просто является пользовательским контролем. Это позволяет проводить тестирование, поскольку ViewModel создает и владеет свойством, а свойство не зависит от представления. Что касается безопасности, я не знаю, насколько хороша эта реализация. Но при использовании функции значение не сохраняется в самом свойстве, просто доступ к нему осуществляется.
Хотя я согласен с тем, что важно избегать хранения пароля в любом месте, мне все еще нужна возможность создания экземпляра модели представления без представления и выполнения моих тестов с ним.
Решение, которое работало для меня, состояло в том, чтобы зарегистрировать функцию PasswordBox.Password в модели представления и заставить модель представления вызывать ее при выполнении кода входа в систему.
Это означает строку кода в коде представления.
Итак, в моем Login.xaml у меня есть
<PasswordBox x:Name="PasswordBox"/>
а в Login.xaml.cs у меня есть
LoginViewModel.PasswordHandler = () => PasswordBox.Password;
тогда в LoginViewModel.cs я определил PasswordHandler
public Func<string> PasswordHandler { get; set; }
и когда вход в систему должен произойти, код вызывает обработчик, чтобы получить пароль от представления...
bool loginResult = Login(Username, PasswordHandler());
Таким образом, когда я хочу протестировать модель представления, я могу просто установить PasswordHandler на анонимный метод, который позволяет мне предоставлять любой пароль, который я хочу использовать в тесте.
Я решил добавить свое решение в микс, так как это такая распространенная проблема... и наличие большого количества вариантов всегда хорошо.
Я просто завернула PasswordBox
в UserControl
и реализовал DependencyProperty
чтобы иметь возможность связывать. Я делаю все возможное, чтобы избежать сохранения любого открытого текста в памяти, поэтому все делается через SecureString
и PasswordBox.Password
имущество. В течение foreach
цикл, каждый персонаж действительно выставляется, но это очень кратко. Честно говоря, если вы беспокоитесь о том, что ваше приложение WPF может быть скомпрометировано из-за этого краткого разоблачения, у вас есть более серьезные проблемы с безопасностью, которые следует решать.
Прелесть этого в том, что вы не нарушаете никаких правил MVVM, даже "пуристических", так как это UserControl
так что разрешено иметь код-позади. Когда вы используете его, вы можете иметь чистую связь между View
а также ViewModel
без вашего VideModel
быть в курсе какой-либо части View
или источник пароля. Просто убедитесь, что вы привязаны к SecureString
в вашем ViewModel
,
BindablePasswordBox.xaml
<UserControl x:Class="BK.WPF.CustomControls.BindanblePasswordBox"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d" d:DesignHeight="22" d:DesignWidth="150">
<PasswordBox x:Name="PswdBox"/>
</UserControl>
BindablePasswordBox.xaml.cs (Версия 1 - без поддержки двустороннего связывания.)
using System.ComponentModel;
using System.Security;
using System.Windows;
using System.Windows.Controls;
namespace BK.WPF.CustomControls
{
public partial class BindanblePasswordBox : UserControl
{
public static readonly DependencyProperty PasswordProperty =
DependencyProperty.Register("Password", typeof(SecureString), typeof(BindanblePasswordBox));
public SecureString Password
{
get { return (SecureString)GetValue(PasswordProperty); }
set { SetValue(PasswordProperty, value); }
}
public BindanblePasswordBox()
{
InitializeComponent();
PswdBox.PasswordChanged += PswdBox_PasswordChanged;
}
private void PswdBox_PasswordChanged(object sender, RoutedEventArgs e)
{
var secure = new SecureString();
foreach (var c in PswdBox.Password)
{
secure.AppendChar(c);
}
Password = secure;
}
}
}
Использование версии 1:
<local:BindanblePasswordBox Width="150" HorizontalAlignment="Center"
VerticalAlignment="Center"
Password="{Binding Password, Mode=OneWayToSource}"/>
BindablePasswordBox.xaml.cs (Версия 2 - имеет поддержку двустороннего связывания.)
public partial class BindablePasswordBox : UserControl
{
public static readonly DependencyProperty PasswordProperty =
DependencyProperty.Register("Password", typeof(SecureString), typeof(BindablePasswordBox),
new PropertyMetadata(PasswordChanged));
public SecureString Password
{
get { return (SecureString)GetValue(PasswordProperty); }
set { SetValue(PasswordProperty, value); }
}
public BindablePasswordBox()
{
InitializeComponent();
PswdBox.PasswordChanged += PswdBox_PasswordChanged;
}
private void PswdBox_PasswordChanged(object sender, RoutedEventArgs e)
{
var secure = new SecureString();
foreach (var c in PswdBox.Password)
{
secure.AppendChar(c);
}
if (Password != secure)
{
Password = secure;
}
}
private static void PasswordChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var pswdBox = d as BindablePasswordBox;
if (pswdBox != null && e.NewValue != e.OldValue)
{
var newValue = e.NewValue as SecureString;
if (newValue == null)
{
return;
}
var unmanagedString = IntPtr.Zero;
string newString;
try
{
unmanagedString = Marshal.SecureStringToGlobalAllocUnicode(newValue);
newString = Marshal.PtrToStringUni(unmanagedString);
}
finally
{
Marshal.ZeroFreeGlobalAllocUnicode(unmanagedString);
}
var currentValue = pswdBox.PswdBox.Password;
if (currentValue != newString)
{
pswdBox.PswdBox.Password = newString;
}
}
}
}
Использование версии 2:
<local:BindanblePasswordBox Width="150" HorizontalAlignment="Center"
VerticalAlignment="Center"
Password="{Binding Password, Mode=TwoWay}"/>
Мне обе эти вещи кажутся неправильными:
- Реализация свойств открытого текста пароля
- Отправка
PasswordBox
в качестве параметра команды для ViewModel
Передача SecurePassword (экземпляр SecureString), как описано Стивом в CO, кажется приемлемой. я предпочитаю Behaviors
код, и у меня также было дополнительное требование, чтобы иметь возможность сбросить пароль из модели представления.
Xaml (Password
является свойством ViewModel):
<PasswordBox>
<i:Interaction.Behaviors>
<behaviors:PasswordBinding BoundPassword="{Binding Password, Mode=TwoWay}" />
</i:Interaction.Behaviors>
</PasswordBox>
Поведение:
using System.Security;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interactivity;
namespace Evidence.OutlookIntegration.AddinLogic.Behaviors
{
/// <summary>
/// Intermediate class that handles password box binding (which is not possible directly).
/// </summary>
public class PasswordBoxBindingBehavior : Behavior<PasswordBox>
{
// BoundPassword
public SecureString BoundPassword { get { return (SecureString)GetValue(BoundPasswordProperty); } set { SetValue(BoundPasswordProperty, value); } }
public static readonly DependencyProperty BoundPasswordProperty = DependencyProperty.Register("BoundPassword", typeof(SecureString), typeof(PasswordBoxBindingBehavior), new FrameworkPropertyMetadata(OnBoundPasswordChanged));
protected override void OnAttached()
{
this.AssociatedObject.PasswordChanged += AssociatedObjectOnPasswordChanged;
base.OnAttached();
}
/// <summary>
/// Link up the intermediate SecureString (BoundPassword) to the UI instance
/// </summary>
private void AssociatedObjectOnPasswordChanged(object s, RoutedEventArgs e)
{
this.BoundPassword = this.AssociatedObject.SecurePassword;
}
/// <summary>
/// Reacts to password reset on viewmodel (ViewModel.Password = new SecureString())
/// </summary>
private static void OnBoundPasswordChanged(object s, DependencyPropertyChangedEventArgs e)
{
var box = ((PasswordBoxBindingBehavior)s).AssociatedObject;
if (box != null)
{
if (((SecureString)e.NewValue).Length == 0)
box.Password = string.Empty;
}
}
}
}
Отправить SecureString
к модели представления с помощью Attached Behavior и ICommand
Нет ничего плохого в коде программной части при реализации MVVM. MVVM - это архитектурный паттерн, который направлен на отделение представления от модели / бизнес-логики. MVVM описывает, как достичь этой цели воспроизводимым способом (шаблоном). Его не волнуют детали реализации, например, как вы структурируете или реализуете представление. Он просто рисует границы и определяет, что такое представление, модель представления и какая модель с точки зрения терминологии этого шаблона.
MVVM не заботится о языке (XAML или C#) или компиляторе (partial
классы). Независимость от языка является обязательной характеристикой шаблона проектирования - она должна быть независимой от языка.
Однако код программной части имеет некоторые недостатки, например, затрудняет понимание логики пользовательского интерфейса, когда она сильно распределяется между XAML и C#. Но наиболее важная реализация логики пользовательского интерфейса или таких объектов, как шаблоны, стили, триггеры, анимация и т. Д., На C# очень сложна и уродлива / менее читабельна, чем использование XAML. XAML - это язык разметки, который использует теги и вложение для визуализации иерархии объектов. Создание пользовательского интерфейса с использованием XAML очень удобно. Хотя бывают ситуации, когда вы можете реализовать логику пользовательского интерфейса на C# (или в коде программной части). ОбработкаPasswordBox
это один из примеров.
По этим причинам обработка PasswordBox
в коде программной части, обрабатывая PasswordBox.PasswordChanged
, не является нарушением шаблона MVVM.
Явным нарушением будет передача контроля (PasswordBox
) к модели просмотра. Многие решения рекомендуют это, например, залив, передавая экземплярPasswordBox
как ICommand.CommandParameter
к модели просмотра. Очевидно, очень плохая и ненужная рекомендация.
Если вы не заботитесь об использовании C#, а просто хотите, чтобы ваш файл кода программной части был чистым или просто хотите инкапсулировать логику поведения / пользовательского интерфейса, вы всегда можете использовать присоединенные свойства и реализовать присоединенное поведение.
В отличие от печально известного широко распространенного помощника, который обеспечивает привязку к паролю в виде простого текста (действительно плохой антишаблон и угроза безопасности), это поведение использует ICommand
отправить пароль как SecureString
к модели представления, когда PasswordBox
поднимает PasswordBox.PasswordChanged
событие.
MainWindow.xaml
<Window>
<Window.DataContext>
<ViewModel />
</Window.DataContext>
<PasswordBox PasswordBox.Command="{Binding VerifyPasswordCommand}" />
</Window>
ViewModel.cs
public class ViewModel : INotifyPropertyChanged
{
public ICommand VerifyPasswordCommand => new RelayCommand(VerifyPassword);
public void VerifyPassword(object commadParameter)
{
if (commandParameter is SecureString secureString)
{
IntPtr valuePtr = IntPtr.Zero;
try
{
valuePtr = Marshal.SecureStringToGlobalAllocUnicode(value);
string plainTextPassword = Marshal.PtrToStringUni(valuePtr);
// Handle plain text password.
// It's recommended to convert the SecureString to plain text in the model, when really needed.
}
finally
{
Marshal.ZeroFreeGlobalAllocUnicode(valuePtr);
}
}
}
}
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);
}
}
Я использовал этот метод и передал поле пароля, хотя это нарушает MVVM, это было важно для меня, потому что я использовал элемент управления контентом с шаблоном данных для входа в систему в моей оболочке, которая является сложной средой оболочки. Таким образом, доступ к коду позади оболочки был бы дерьмом.
Насколько я знаю, передача пароля должна быть такой же, как и контроль доступа из кода. Я согласен с паролями, не храню в памяти и т. Д. В этой реализации у меня нет свойства для пароля в модели представления.
Кнопка Команда
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);
}
Вы можете сделать это с прикрепленным свойством, посмотрите это. PasswordBox с MVVM
Для полных новичков, как я, вот полный рабочий пример того, что Konamiman
предложено выше. Спасибо 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);
}
}
<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);
}
}
}
;) легко!
Как упоминалось ранее, VM не должна знать о View, но передача всего PasswordBox выглядит как самый простой подход. Поэтому, возможно, вместо приведения переданного параметра к PasswordBox используйте Reflection для извлечения из него свойства Password. В этом случае VM ожидает некоторый контейнер паролей со свойством Password(я использую RelayCommands из 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;
}
}
Это легко проверить с помощью анонимного класса:
var passwordContainer = new
{
Password = "password"
};
В Windows универсальное приложение
Вы можете использовать этот код со свойством "Пароль" и связывание с modelView
<PasswordBox x:Uid="PasswordBox" Password="{Binding Waiter.Password, Mode=TwoWay}" Name="txtPassword" HorizontalAlignment="Stretch" Margin="50,200,50,0" VerticalAlignment="Top"/>
Для любого, кто знает о рисках, связанных с этой реализацией, для синхронизации пароля с вашей ViewModel просто добавьте Mode = OneWayToSource.
XAML
<PasswordBox
ff:PasswordHelper.Attach="True"
ff:PasswordHelper.Password="{Binding Path=Password, Mode=OneWayToSource}" />
Это очень просто. Создайте другое свойство для пароля и свяжите это с TextBox
Но все операции ввода выполняются с фактическим свойством пароля
приватная строка _Password;
public string PasswordChar
{
get
{
string szChar = "";
foreach(char szCahr in _Password)
{
szChar = szChar + "*";
}
return szChar;
}
set
{
_PasswordChar = value; NotifyPropertyChanged();
}
}
публичная строка Password { get { return _Password; }
set
{
_Password = value; NotifyPropertyChanged();
PasswordChar = _Password;
}
}
Я сделал как:
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;
}
}
Меня устраивает!
Вот мой взгляд на это:
Использование присоединенного свойства для привязки пароля сводит на нет цель его защиты. Свойство Password для поля пароля не может быть привязано по какой-либо причине.
Передача поля пароля в качестве параметра команды сделает ViewModel осведомленным об элементе управления. Это не будет работать, если вы планируете сделать вашу ViewModel многоразовой кроссплатформенной. Не информируйте вашу виртуальную машину о вашем View или других элементах управления.
Я не думаю, что введение нового свойства, интерфейса, подписка на события, измененные паролем, или любые другие сложные вещи необходимы для простой задачи предоставления пароля.
XAML
<PasswordBox x:Name="pbPassword" />
<Button Content="Login" Command="{Binding LoginCommand}" x:Name="btnLogin"/>
Код позади - использование кода сзади не обязательно нарушает MVVM. Пока вы не вкладываете в это бизнес-логику.
btnLogin.CommandParameter = new Func<string>(()=>pbPassword.Password);
ViewModel
LoginCommand = new RelayCommand<Func<string>>(getpwd=> { service.Login(username, getpwd()); });
Как вы можете видеть, я привязываю к паролю, но, возможно, его привязывают к статическому классу.
Это прикрепленное свойство. Этот вид имущества может применяться к любому виду DependencyObject
, а не только тип, в котором он объявлен. Так что, хотя это заявлено в PasswordHelper
статический класс, он применяется к PasswordBox
на котором вы его используете.
Чтобы использовать это прикрепленное свойство, вам просто нужно привязать его к Password
Свойство в вашей ViewModel:
<PasswordBox w:PasswordHelper.Attach="True"
w:PasswordHelper.Password="{Binding Password}"/>
К сожалению, у меня недостаточно репутации, чтобы прокомментировать другой ответ :( Просто хотел продолжить ответ «Стива в CO». Обновил свое событие «PasswrodChanged» следующим образом, чтобы разрешить использование одного и того же события PasswordChanged для всех PasswordBox, если у вас есть несколько в одном представлении, используя свойство Name отправителя. Может быть лучший способ сделать это, и если есть какие-либо причины не делать этого, дайте мне знать :)
private void PasswordBox_PasswordChanged(object sender, RoutedEventArgs e)
{
if (sender is PasswordBox) {
if (this.DataContext != null)
{ ((dynamic)this.DataContext).GetType().GetProperty(((PasswordBox)sender).Name).SetValue((dynamic)this.DataContext, ((PasswordBox)sender).Password, null); }
}
}
Я использовал проверку подлинности, за которой последовала подпрограмма, вызываемая классом-посредником для View (которая также реализует проверку подлинности), чтобы записать пароль для класса данных.
Это не идеальное решение; однако это исправило мою проблему невозможности переместить пароль.
Ну мой ответ более прост только в шаблоне MVVM
в классе 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;
}
свойство пароля выигравшего PasswordBox или WatermarkPasswordBox, предоставляемого XCeedtoolkit, генерирует RoutedEventArgs, чтобы вы могли связать его.
сейчас в формате xmal
<Xceed:WatermarkPasswordBox Watermark="Input your Password" Grid.Column="1" Grid.ColumnSpan="3" Grid.Row="7" PasswordChar="*" >
<i:Interaction.Triggers>
<i:EventTrigger EventName="PasswordChanged">
<prism:InvokeCommandAction Command="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path= DataContext.PasswordChangedCommand}" CommandParameter="{Binding RelativeSource={RelativeSource Self}, Path= Password}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</Xceed:WatermarkPasswordBox>
или же
<PasswordBox Grid.Column="1" Grid.ColumnSpan="3" Grid.Row="7" PasswordChar="*" >
<i:Interaction.Triggers>
<i:EventTrigger EventName="PasswordChanged">
<prism:InvokeCommandAction Command="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path= DataContext.PasswordChangedCommand}" CommandParameter="{Binding RelativeSource={RelativeSource Self}, Path= Password}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</PasswordBox>
Я потратил целую вечность, пытаясь заставить это работать. В конце концов, я сдался и просто использовал PasswordBoxEdit от DevExpress.
Это самое простое решение, так как оно позволяет связывать без каких-либо ужасных трюков.
Кстати, я никак не связан с DevExpress.
Я использую сжатое MVVM-дружественное решение, которое еще не было упомянуто. Сначала я называю PasswordBox на XAML:
<PasswordBox x:Name="Password" />
Затем я добавляю единственный вызов метода в конструктор представления:
public LoginWindow()
{
InitializeComponent();
ExposeControl<LoginViewModel>.Expose(this, view => view.Password,
(model, box) => model.SetPasswordBox(box));
}
И это все. Модель представления получит уведомление, когда оно присоединено к представлению через DataContext, и другое уведомление, когда оно отсоединено. Содержимое этого уведомления настраивается через лямбды, но обычно это просто вызов метода или метода для модели представления, передавая проблемный элемент управления в качестве параметра.
Это очень легко сделать MVVM-дружественным, если вместо дочерних элементов управления использовать интерфейс представления.
Приведенный выше код опирается на вспомогательный класс, опубликованный в моем блоге.