30

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)
{
    ...
}
Rob
  • 26,989
  • 16
  • 82
  • 98
Dave
  • 1,822
  • 2
  • 27
  • 36

3 Answers3

84

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
}
Community
  • 1
  • 1
FMM
  • 4,289
  • 1
  • 25
  • 44
  • FMM - Thx. Looks like code for a newer version of CM. Will this work in version 1.5.2 of CM? – Dave Jun 26 '15 at 22:11
  • Not sure; the important part of the CM code above is the use of the `ConventionManager`; I doubt it's changed much. – FMM Jun 29 '15 at 14:10
  • 1
    Your (awesome) code, weirdly, moves the caret to the start of the box at every type. You need to add this : http://stackoverflow.com/a/1046920/6776 at the end of the `PasswordChanged` method. – thomasb Apr 25 '16 at 15:26
  • Only for unfortunate definitions of awesome, I suppose =) thanks! – FMM Apr 25 '16 at 15:53
  • 7
    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 }); – Chris Johnson May 03 '16 at 22:23
  • 3
    @FMM I modified the code with Chris and comso suggestions. – Eternal21 May 24 '16 at 13:14
  • https://stackoverflow.com/q/47003975/6749790 I gave it ago but I am unable to get the password box in my login method. – Daniel Oct 29 '17 at 19:50
  • 2
    can someone make an example of how to use it please. – Daniel Oct 30 '17 at 22:21
  • 3
    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. – ssarabando Aug 01 '19 at 14:09
  • @Daniel 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 name – Ahmed Mohammed Aug 22 '19 at 11:18
  • what about if using HandyControl passwordbox? i tried your solution and its not working :(..since there is Passwordbox.PasswordChanged does not exist for Handycontrol passwordbox – Froodo Mar 14 '22 at 11:30
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.

Shahin Dohan
  • 6,149
  • 3
  • 41
  • 58
5

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);
            }
        }
    }
}
Community
  • 1
  • 1
Mike L
  • 4,693
  • 5
  • 33
  • 52
  • 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. – Dave Jun 04 '15 at 10:31
  • 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. – Dave Jun 04 '15 at 11:24
  • I think I finally figured this out by adding the following code - – Dave Jun 04 '15 at 12:32
  • @Dave 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. – Mike L Jun 04 '15 at 14:43
  • 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; } – Dave Jun 05 '15 at 01:29
  • 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. Thx – Dave Jun 05 '15 at 13:29
  • @Dave 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); } } – Mike L Jun 05 '15 at 17:55
  • @Dave I've added the VM to the answer above. See if that helps. – Mike L Jun 05 '15 at 18:01