Caliburn.Micro support for PasswordBox?

2020-02-22 23:40发布

问题:

The Caliburn.Micro home page at http://caliburnmicro.com makes the below claim but I am unable to make CM work with a PasswordBox control using any variation I can think of from that example. Don't see how this would work anyway since the names are not the same case. Does anyone have a CM example that does allow me to get the value of the PasswordBox? Is there a particular version of CM required? I'm running version 1.5.2 of CM. Ideally w/o using Attached Properties but if can work with CM and the only way then fine. Please no lectures on security issues as that is not an issue in my case.


Apply methods between your view and view model automatically with parameters and guard methods

<StackPanel>
    <TextBox x:Name="Username" />
    <PasswordBox x:Name="Password" />
    <Button x:Name="Login" Content="Log in" />
</StackPanel>

public bool CanLogin(string username, string password)
{
    return !String.IsNullOrEmpty(username) && !String.IsNullOrEmpty(password);
}

public string Login(string username, string password)
{
    ...
}

回答1:

I've only been able to get it to work with dependency properties, effectively bypassing the convention binding goodness that Caliburn.Micro supplies. I recognize that's not your ideal, but pragmatically this is the solution I regularly use. I believe when I hit this snag historically, I found this post on StackOverflow that led me in this direction. For your consideration:

public class BoundPasswordBox
    {
        private static bool _updating = false;

        /// <summary>
        /// BoundPassword Attached Dependency Property
        /// </summary>
        public static readonly DependencyProperty BoundPasswordProperty =
            DependencyProperty.RegisterAttached("BoundPassword",
                typeof(string),
                typeof(BoundPasswordBox),
                new FrameworkPropertyMetadata(string.Empty, OnBoundPasswordChanged));

        /// <summary>
        /// Gets the BoundPassword property.
        /// </summary>
        public static string GetBoundPassword(DependencyObject d)
        {
            return (string)d.GetValue(BoundPasswordProperty);
        }

        /// <summary>
        /// Sets the BoundPassword property.
        /// </summary>
        public static void SetBoundPassword(DependencyObject d, string value)
        {
            d.SetValue(BoundPasswordProperty, value);
        }

        /// <summary>
        /// Handles changes to the BoundPassword property.
        /// </summary>
        private static void OnBoundPasswordChanged(
            DependencyObject d,
            DependencyPropertyChangedEventArgs e)
        {
            PasswordBox password = d as PasswordBox;
            if (password != null)
            {
                // Disconnect the handler while we're updating.
                password.PasswordChanged -= PasswordChanged;
            }

            if (e.NewValue != null)
            {
                if (!_updating)
                {
                    password.Password = e.NewValue.ToString();
                }
            }
            else 
            {
                password.Password = string.Empty;
            }
            // Now, reconnect the handler.
            password.PasswordChanged += PasswordChanged;
        }

        /// <summary>
        /// Handles the password change event.
        /// </summary>
        static void PasswordChanged(object sender, RoutedEventArgs e)
        {
            PasswordBox password = sender as PasswordBox;
            _updating = true;
            SetBoundPassword(password, password.Password);
            _updating = false;
        }
    }

Then, in your XAML:

<PasswordBox pwbx:BoundPasswordBox.BoundPassword="{Binding UserPassword, Mode=TwoWay,UpdateSourceTrigger=PropertyChanged,NotifyOnValidationError=True,ValidatesOnDataErrors=True}" />

and pwbx is found as a namespace on the Window tag:

<Window x:Class="MyProject.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" 
             mc:Ignorable="d" 
             xmlns:pwbx="clr-namespace:MyProject.Client.Controls">

The ViewModel:

using Caliburn.Micro;
using MyProject.Core;
using MyProject.Repositories;
using MyProject.Types;
using MyProject.ViewModels.Interfaces;

namespace MyProject.ViewModels
{
    public class LoginViewModel : Screen, ILoginViewModel
    {
        private readonly IWindowManager _windowManager;
        private readonly IUnitRepository _unitRepository;
        public bool IsLoginValid { get; set; }
        public Unit LoggedInUnit { get; set; }

        private string _password;
        public string UserPassword
        {
            get { return _password; }
            set
            {
                _password = value;
                NotifyOfPropertyChange(() => UserPassword);
                NotifyOfPropertyChange(() => CanLogin);
            }
        }

        private string _name;
        public string Username
        {
            get { return _name; }
            set
            {
                _name = value;
                NotifyOfPropertyChange(() => Username);
                NotifyOfPropertyChange(() => CanLogin);
            }
        }
        public LoginViewModel(IWindowManager windowManager,IUnitRepository unitRepository)
        {
            _windowManager = windowManager;
            _unitRepository = unitRepository;
            DisplayName = "MyProject - Login";
            Version = ApplicationVersionRepository.GetVersion();
        }

        public string Version { get; private set; }

        public void Login()
        {
            // Login logic
            var credentials = new UserCredentials { Username = Username, Password=UserPassword };

            var resp = _unitRepository.AuthenticateUnit(credentials);
            if (resp == null) return;
            if (resp.IsValid)
            {
                IsLoginValid = true;
                LoggedInUnit = resp.Unit;
                TryClose();
            }
            else
            {
                var dialog = new MessageBoxViewModel(DialogType.Warning, DialogButton.Ok, "Login Failed", "Login Error: " + resp.InvalidReason);
                _windowManager.ShowDialog(dialog);
            }
        }

        public bool CanLogin
        {
            get
            {
                return !string.IsNullOrEmpty(Username) && !string.IsNullOrEmpty(UserPassword);
            }
        }
    }
}


回答2:

Here's a much more simplified example, including a binding convention so that PasswordBox binding in Caliburn.Micro Just Works™:

public static class PasswordBoxHelper
{
    public static readonly DependencyProperty BoundPasswordProperty =
        DependencyProperty.RegisterAttached("BoundPassword",
            typeof(string),
            typeof(PasswordBoxHelper),
            new FrameworkPropertyMetadata(string.Empty, OnBoundPasswordChanged));

    public static string GetBoundPassword(DependencyObject d)
    {
        var box = d as PasswordBox;
        if (box != null)
        {
            // this funny little dance here ensures that we've hooked the
            // PasswordChanged event once, and only once.
            box.PasswordChanged -= PasswordChanged;
            box.PasswordChanged += PasswordChanged;
        }

        return (string)d.GetValue(BoundPasswordProperty);
    }

    public static void SetBoundPassword(DependencyObject d, string value)
    {
        if (string.Equals(value, GetBoundPassword(d)))
            return; // and this is how we prevent infinite recursion

        d.SetValue(BoundPasswordProperty, value);
    }

    private static void OnBoundPasswordChanged(
        DependencyObject d,
        DependencyPropertyChangedEventArgs e)
    {
        var box = d as PasswordBox;

        if (box == null)
            return;

        box.Password = GetBoundPassword(d);
    }

    private static void PasswordChanged(object sender, RoutedEventArgs e)
    {
        PasswordBox password = sender as PasswordBox;

        SetBoundPassword(password, password.Password);

        // set cursor past the last character in the password box
        password.GetType().GetMethod("Select", BindingFlags.Instance | BindingFlags.NonPublic).Invoke(password, new object[] { password.Password.Length, 0 }); 
    }

}

Then, in your bootstrapper:

public sealed class Bootstrapper : BootstrapperBase
{
    public Bootstrapper()
    {
        Initialize();

        ConventionManager.AddElementConvention<PasswordBox>(
            PasswordBoxHelper.BoundPasswordProperty,
            "Password",
            "PasswordChanged");
    }

    // other bootstrapper stuff here
}


回答3:

The solutions provided here seem to be needlessly complicated.

We can very easily use Caliburn.Micro actions to send our password to the ViewModel.

XAML:

<PasswordBox cal:Message.Attach="[Event PasswordChanged] = [Action OnPasswordChanged($source)]" />

ViewModel:

public void OnPasswordChanged(PasswordBox source)
{
    password = source.Password;
}

Then remember to clear the password fields so they don't remain in memory.

NOTE: Obviously this solution doesn't allow you to easily change the password from the ViewModel, if that is necessary, then it's probably best to go with the attached property approach.