Caliburn.Micro对PasswordBox的支持?

30

Caliburn.Micro主页在http://caliburnmicro.com上声明如下,但是我无法使用从该示例中想到的任何变体使CM与PasswordBox控件配合工作。也不知道这会如何工作,因为名称的大小写不同。是否有人有一个可以让我获得PasswordBox值的CM示例?是否需要特定版本的CM?我正在运行CM的版本1.5.2。最好不使用Attached Properties,但如果只能通过使用CM,则可以。请不要对安全问题进行讲解,因为这在我的情况下不是问题。


自动使用参数和保护方法在您的视图和视图模型之间应用方法

<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)
{
    ...
}
3个回答

84

这是一个更加简化的示例,包括绑定约定,以便 PasswordBox 在 Caliburn.Micro 中能够正常工作™:

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 }); 
    }

}

然后,在您的引导程序中:

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

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

    // other bootstrapper stuff here
}

1
你的(棒极了)代码奇怪地在每次输入时将插入符号移动到框的开头。你需要在PasswordChanged方法的末尾添加这个:https://dev59.com/rEfSa4cB1Zd3GeqPBv3F#1046920。 - thomasb
7
如cosmo0所指出的,我在PasswordChanged函数中添加了以下代码作为最后一行,以设置正确的光标位置与字符串开头的对齐:password.GetType().GetMethod("Select", BindingFlags.Instance | BindingFlags.NonPublic).Invoke(password, new object[] { password.Password.Length, 0 }); - Chris Johnson
3
@FMM 我跟着Chris和comso的建议修改了代码。 - Eternal21
2
有人能否提供一个如何使用它的示例吗? - Daniel
3
如果您预设了支持PasswordBox值的视图模型属性为 "",则该助手将永远不会被调用,用户输入或更改密码时也不会更新vm的属性。这是因为 FrameworkPropertyMetadata 的默认值设置为 string.Empty。将其设置为 null 即可在这种情况下正常工作。 - ssarabando
显示剩余6条评论

9
这里提供的解决方案似乎过于复杂。
我们可以很容易地使用Caliburn.Micro动作将密码发送到ViewModel中。
XAML:
<PasswordBox cal:Message.Attach="[Event PasswordChanged] = [Action OnPasswordChanged($source)]" />

视图模型:

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

然后记得清空密码字段,以防止它们保留在内存中。

注意:显然,如果需要从ViewModel轻松更改密码,则最好采用附加属性方法。


太好了!谢谢,Sharin。这绝对是一个更简单的解决方案! - Rafael Coelho

5

我只能使用依赖属性来使其工作,这有效地绕过了Caliburn.Micro提供的约定绑定优点。我知道这不是理想的解决方案,但从实用角度出发,这是我经常使用的解决方案。我相信在历史上遇到这个问题时,我在StackOverflow上找到了这篇文章,它引导我朝着这个方向前进。供参考:

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;
        }
    }

然后,在你的XAML中:

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

在 Window 标签上发现了 pwbx 作为命名空间:

<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">

视图模型(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.0版本中有效,但我现在还没有准备好转换到那个版本。我确实明白他们故意没有使用PasswordBox的依赖属性。 - Dave
我已经尝试了添加依赖属性的其他变化以及你提供的方式,但在这两种情况下,即使你附加的属性代码被调用,我的声明的UserPassword在ViewModel代码中也没有被设置。你是如何声明、设置和访问实际的UserPassword变量的?你是使用Caliburn Micro还是代码后台?我猜我不知道如何在Caliburn Micro ViewModel的上下文中实现这一点。谢谢。 - Dave
@Dave,很高兴你解决了问题。我没有在代码后台中添加任何内容,而是将其作为视图模型中的属性。它被访问方式与其他属性相同。在我的XAML中,可以看到该属性有效地绕过了Caliburn.Micro的约定绑定并直接设置了它。我会说CM在其中没有起到任何作用,但我确实将CanLogin保护条款绑定到了登录按钮上,当ZZZ是CM操作时,它会利用CM的“CanZZZ”保护操作,按名称绑定到VM的同名方法。 - Mike L
代码未附加,因此正在尝试再次操作 - 我向CM ViewModel添加了一个对话框属性,该属性从在XAML中定义的视图中获取UserPassword对象。 public PasswordBox Password { get { return mView.UserPassword; } - Dave
@Dave 在视图的XAML中,依赖属性BoundPasswordBox.BoundPassword被绑定到ViewModel类上的UserPassword属性。该属性是非常标准的:private string _password; public string UserPassword { get { return _password; } set { _password = value; NotifyOfPropertyChange(() => UserPassword); NotifyOfPropertyChange(() => CanLogin); } } - Mike L
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接