Caliburn.Micro support for PasswordBox?
Asked Answered
M

3

31

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)
{
    ...
}
Maximamaximal answered 3/6, 2015 at 21:58 Comment(0)
M
6

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);
            }
        }
    }
}
Marla answered 4/6, 2015 at 3:30 Comment(8)
Thanks, I'll try that later today after too many meetings are over, and get back and mark as answer hopefully. Wish I knew what they were claiming in regards to how that example worked. Perhaps the conventions somehow work only in the new 2.0 version but I'm not ready to convert to that right now. I do understand that they purposefully did not have a dependent property with PasswordBox.Maximamaximal
I had tried the other variation of adding a dependency property as well as yours and in both cases my declared UserPassword is not being set in the ViewModel code even though your attached property code is being called. How are you declaring, setting and accessing the actual UserPassword var? Are you using Caliburn Micro or code behind? I guess I don't know how to do this in context of the Caliburn Micro ViewModel. Thx.Maximamaximal
I think I finally figured this out by adding the following code -Maximamaximal
@Maximamaximal glad you figured it out. I don't have anything in code behind, but as a property in the view model. It's being accessed as any other property would be. In my XAML, you can see that this property is effectively bypassing Caliburn.Micro's convention binding and directly setting it. I would say that CM plays no role in it, but I do have a CanLogin guard clause bound to the login button which does leverage CM's "CanZZZ" guard actions when ZZZ is the CM action, bound by name to the VM's method of the same name.Marla
Code did not append so trying again - I added a Dialog Property to the CM ViewModel that fetches the UserPassword object from the View defined in the XAML. public PasswordBox Password { get { return mView.UserPassword; }Maximamaximal
Mike, are you saying you have "PasswordBox UserPassword" defined in the ViewModel and that binds to the xaml attached property UserPassword? I was not able to get that to work and had to reach into the view. If so, I'll try that again as I would prefer to not dig into the view to find the PasswordBox. ThxMaximamaximal
@Maximamaximal In the View's XAML, the dependency property BoundPasswordBox.BoundPassword is databound to the UserPassword property on the ViewModel class. That property is pretty standard: private string _password; public string UserPassword { get { return _password; } set { _password = value; NotifyOfPropertyChange(() => UserPassword); NotifyOfPropertyChange(() => CanLogin); } }Marla
@Maximamaximal I've added the VM to the answer above. See if that helps.Marla
W
85

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
}
Windpipe answered 26/6, 2015 at 18:17 Comment(11)
FMM - Thx. Looks like code for a newer version of CM. Will this work in version 1.5.2 of CM?Maximamaximal
Not sure; the important part of the CM code above is the use of the ConventionManager; I doubt it's changed much.Windpipe
Your (awesome) code, weirdly, moves the caret to the start of the box at every type. You need to add this : https://mcmap.net/q/470352/-how-can-i-set-the-caret-position-to-a-specific-index-in-passwordbox-in-wpf at the end of the PasswordChanged method.Saliferous
Only for unfortunate definitions of awesome, I suppose =) thanks!Windpipe
As cosmo0 points out i added the following line as the last line in the PasswordChanged function to set the correct caret position vs. the start of the string: password.GetType().GetMethod("Select", BindingFlags.Instance | BindingFlags.NonPublic).Invoke(password, new object[] { password.Password.Length, 0 });Machinegun
@Windpipe I modified the code with Chris and comso suggestions.Harts
https://mcmap.net/q/459164/-caliburn-micro-support-for-passwordbox/6749790 I gave it ago but I am unable to get the password box in my login method.Adel
can someone make an example of how to use it please.Adel
A little note for the rushed (like me): if you preset the view model property that backs the PasswordBox's value with "", the helper will never be called and your vm's property will never be updated when the user enters or changes the password. That's because the default value for the FrameworkPropertyMetadata is set to string.Empty. Set it to null for it to work in that case.Penhall
@Adel like anything else with caliburn micro after adding all the code above just use x:Name="" property for the password controller in your xaml and it will bind automatically to what ever string in your viewmodel with the same nameHovey
what about if using HandyControl passwordbox? i tried your solution and its not working :(..since there is Passwordbox.PasswordChanged does not exist for Handycontrol passwordboxOrelu
W
9

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.

Waldon answered 3/7, 2019 at 11:31 Comment(1)
Great!. Thanks, Sharin. Definitely a much simple solution!Hawkinson
M
6

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);
            }
        }
    }
}
Marla answered 4/6, 2015 at 3:30 Comment(8)
Thanks, I'll try that later today after too many meetings are over, and get back and mark as answer hopefully. Wish I knew what they were claiming in regards to how that example worked. Perhaps the conventions somehow work only in the new 2.0 version but I'm not ready to convert to that right now. I do understand that they purposefully did not have a dependent property with PasswordBox.Maximamaximal
I had tried the other variation of adding a dependency property as well as yours and in both cases my declared UserPassword is not being set in the ViewModel code even though your attached property code is being called. How are you declaring, setting and accessing the actual UserPassword var? Are you using Caliburn Micro or code behind? I guess I don't know how to do this in context of the Caliburn Micro ViewModel. Thx.Maximamaximal
I think I finally figured this out by adding the following code -Maximamaximal
@Maximamaximal glad you figured it out. I don't have anything in code behind, but as a property in the view model. It's being accessed as any other property would be. In my XAML, you can see that this property is effectively bypassing Caliburn.Micro's convention binding and directly setting it. I would say that CM plays no role in it, but I do have a CanLogin guard clause bound to the login button which does leverage CM's "CanZZZ" guard actions when ZZZ is the CM action, bound by name to the VM's method of the same name.Marla
Code did not append so trying again - I added a Dialog Property to the CM ViewModel that fetches the UserPassword object from the View defined in the XAML. public PasswordBox Password { get { return mView.UserPassword; }Maximamaximal
Mike, are you saying you have "PasswordBox UserPassword" defined in the ViewModel and that binds to the xaml attached property UserPassword? I was not able to get that to work and had to reach into the view. If so, I'll try that again as I would prefer to not dig into the view to find the PasswordBox. ThxMaximamaximal
@Maximamaximal In the View's XAML, the dependency property BoundPasswordBox.BoundPassword is databound to the UserPassword property on the ViewModel class. That property is pretty standard: private string _password; public string UserPassword { get { return _password; } set { _password = value; NotifyOfPropertyChange(() => UserPassword); NotifyOfPropertyChange(() => CanLogin); } }Marla
@Maximamaximal I've added the VM to the answer above. See if that helps.Marla

© 2022 - 2024 — McMap. All rights reserved.