Two way binding with PasswordBox using ReactiveUI

Viewed 404

I have an issue how to properly bind in two-way a password in PasswordBox. My current solution almost works. Issue with it is that cursor during typing stays at beginning, doesn't go with text at the end. Typing 12345, my View Model receives value 54321.

I'm using WFP and XAML. Code behind of view looks like this:

Observable.FromEventPattern(SecretKey, nameof(SecretKey.PasswordChanged))
    .Subscribe(evt => ViewModel.SecretKey = SecretKey.Password)
    .DisposeWith(disposableRegistration);

this.OneWayBind(ViewModel, viewModel => viewModel.SecretKey, view => view.SecretKey.Password)
    .DisposeWith(disposableRegistration);

Cursor behaves correct when one of those constructions are commented, so issue is with binding in two way, and I don't know how to solve it. As you now PasswordBox.Password can't be binded from view to view model, that I need use event.

3 Answers

I don't think there is an 'offical' way to do it and I'm not sure if you should.
But you can set the cursor position manually after you set the value to solve this problem (source).

private void SetCursor(PasswordBox passwordBox, int index)
{
    passwordBox.GetType().GetMethod("Select", BindingFlags.Instance | BindingFlags.NonPublic).Invoke(passwordBox, new object[] { index, 0 });
}

The 'hack'-binding:

this.WhenAnyValue(x => x.ViewModel.SecretKey)
    .Where(pw => pw != null)
    .Do(pw =>
    {
        secretKey.Password = pw;
        SetCursor(secretKey, pw.Length);
    })
    .Subscribe()
    .DisposeWith(disposableRegistration);

PasswordBox usually is handled by attached properties. I made research and ReactiveUi binding can't handle two way binding with attached property. On the other hand there is no red flag for binding in XAML when you use ReactiveUi - it is discouraged, but not prohibited. Keeping in mind that attached properties are not achievable by pure ReactiveUi I decided that I will make exception in binding for PasswordBox and I think it's reasonable and correct.

I used PasswordHelper which implements attached properties from tutorial on wpftutorial.net

public static class PasswordHelper
{
    public static readonly DependencyProperty PasswordProperty =
        DependencyProperty.RegisterAttached("Password",
        typeof(string), typeof(PasswordHelper),
        new FrameworkPropertyMetadata(string.Empty, OnPasswordPropertyChanged));
 
    public static readonly DependencyProperty AttachProperty =
        DependencyProperty.RegisterAttached("Attach",
        typeof(bool), typeof(PasswordHelper), new PropertyMetadata(false, Attach));
 
    private static readonly DependencyProperty IsUpdatingProperty =
       DependencyProperty.RegisterAttached("IsUpdating", typeof(bool), 
       typeof(PasswordHelper));
 
 
    public static void SetAttach(DependencyObject dp, bool value)
    {
        dp.SetValue(AttachProperty, value);
    }
 
    public static bool GetAttach(DependencyObject dp)
    {
        return (bool)dp.GetValue(AttachProperty);
    }
 
    public static string GetPassword(DependencyObject dp)
    {
        return (string)dp.GetValue(PasswordProperty);
    }
 
    public static void SetPassword(DependencyObject dp, string value)
    {
        dp.SetValue(PasswordProperty, value);
    }
 
    private static bool GetIsUpdating(DependencyObject dp)
    {
        return (bool)dp.GetValue(IsUpdatingProperty);
    }
 
    private static void SetIsUpdating(DependencyObject dp, bool value)
    {
        dp.SetValue(IsUpdatingProperty, value);
    }
 
    private static void OnPasswordPropertyChanged(DependencyObject sender,
        DependencyPropertyChangedEventArgs e)
    {
        PasswordBox passwordBox = sender as PasswordBox;
        passwordBox.PasswordChanged -= PasswordChanged;
 
        if (!(bool)GetIsUpdating(passwordBox))
        {
            passwordBox.Password = (string)e.NewValue;
        }
        passwordBox.PasswordChanged += PasswordChanged;
    }
 
    private static void Attach(DependencyObject sender,
        DependencyPropertyChangedEventArgs e)
    {
        PasswordBox passwordBox = sender as PasswordBox;
 
        if (passwordBox == null)
            return;
 
        if ((bool)e.OldValue)
        {
            passwordBox.PasswordChanged -= PasswordChanged;
        }
 
        if ((bool)e.NewValue)
        {
            passwordBox.PasswordChanged += PasswordChanged;
        }
    }
 
    private static void PasswordChanged(object sender, RoutedEventArgs e)
    {
        PasswordBox passwordBox = sender as PasswordBox;
        SetIsUpdating(passwordBox, true);
        SetPassword(passwordBox, passwordBox.Password);
        SetIsUpdating(passwordBox, false);
    }
}

In XAML I use standard binding for PasswordBox with attached properties:

<PasswordBox Name="PasswordBox"
             local:PasswordHelper.Attach="True"
             local:PasswordHelper.Password="{Binding Password, Mode=TwoWay}"/>

In code behind I made a binding ViewModel to DataContext.

this.WhenActivated(d =>
{
    this.WhenAnyValue(x => x.ViewModel).BindTo(this, x => x.DataContext).DisposeWith(d);
});

Example application you can find in GitHub repository.

I've encountered with a similar issue recently and the next solution works quite well for me. May be it'd help somebody else.

ViewModel.WhenAnyValue(x => x.Password)
    .Where(password => string.Equals(password, PasswordBox.Password, StringComparison.CurrentCulture) is false)
    .ObserveOn(RxApp.MainThreadScheduler)
    .Subscribe(password => PasswordBox.Password = password)
    .DisposeWith(disposableRegistration);

Observable.FromEventPattern<RoutedEventHandler, RoutedEventArgs>(
        h => PasswordBox.PasswordChanged += h,
        h => PasswordBox.PasswordChanged -= h)
    .Select(_ => PasswordBox.Password)
    .Where(password => string.Equals(password, ViewModel.Password, StringComparison.CurrentCulture) is false)
    .ObserveOn(RxApp.MainThreadScheduler)
    .Subscribe(password => ViewModel.Password = password)
    .DisposeWith(disposableRegistration);
Related