PasswordBoxのバリデーションをMVVMアーキテクチャで実装する
前提のはなし
WPFでマスクのついたテキストボックスを作成するには、PasswordBoxというコントロールが用意されている。
PasswordBoxに入力した値は、string型のPasswordプロパティから取得できるが、セキュリティ上はSecureString型のSecurePasswordプロパティから取得した方が良いという話になっていた。
.NET Coreでは方針が変更されて、SecurePasswordプロパティは推奨されなくなった。それならばと、Passwordプロパティを使うことにした。
Passwordプロパティは、コードビハインドで使用するならば特に障害は無いが、MVVMアーキテクチャでバインディングを利用しようとすると、バインディングできるように設計されていないので、単純にバインディングする事は出来ない。
バインディングできるようにヘルパークラスを作って公開している人がいるため、それを利用させてもらう。詳しくは、以下で説明されている。
本題
PasswordBoxに入力した値をViewModelのプロパティにバインディング出来るようになったので、ViewModelにバリデーションを実装した。
上記サイトとあまり変わらないが、以下に作成したヘルパークラス(PasswordBoxHelper.cs)、XAMLの使用箇所と、バリデーションを実装したViewModel(CreateUserViewModel.cs)をのせておく。
PasswordBoxHelper.cs
internal class PasswordBoxHelper : DependencyObject { public static readonly DependencyProperty AttachmentProperty = DependencyProperty.RegisterAttached( "Attachment", typeof(bool), typeof(PasswordBoxHelper), new FrameworkPropertyMetadata(false, AttachmentProperty_Changed)); public static readonly DependencyProperty PasswordProperty = DependencyProperty.RegisterAttached( "Password", typeof(string), typeof(PasswordBoxHelper), new FrameworkPropertyMetadata(default(string), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault)); public static bool GetAttachment(DependencyObject dp) { return (bool)dp.GetValue(AttachmentProperty); } public static void SetAttachment(DependencyObject dp, bool value) { dp.SetValue(AttachmentProperty, value); } 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 void AttachmentProperty_Changed(DependencyObject sender, DependencyPropertyChangedEventArgs e) { if (!(sender is PasswordBox passwordBox)) return; if ((bool)e.NewValue) { passwordBox.PasswordChanged += PasswordBox_PasswordChanged; } } private static void PasswordBox_PasswordChanged(object sender, RoutedEventArgs e) { var passwordBox = (PasswordBox)sender; SetPassword(passwordBox, passwordBox.Password); } }
XAML抜粋
<UserControl x:Class="MainMenu.Views.CreateUser" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:prism="http://prismlibrary.com/" xmlns:helper="clr-namespace:MainMenu.Utilities" prism:ViewModelLocator.AutoWireViewModel="True" Height="350" Width="500"> --省略 <PasswordBox Grid.Row="4" Grid.Column="1" VerticalAlignment="Top" HorizontalAlignment="Left" Width="200" Height="24" Validation.ErrorTemplate="{StaticResource ErrorTemplate}" helper:PasswordBoxHelper.Attachment="True" helper:PasswordBoxHelper.Password="{Binding Password, UpdateSourceTrigger=PropertyChanged}" /> --省略
CreateUserViewModel.cs
using Prism.Mvvm; using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Runtime.CompilerServices; namespace MainMenu.ViewModels { public class CreateUserViewModel : BindableBase, INotifyDataErrorInfo { private readonly ErrorsContainer<string> _errors; private string _password; [Required(ErrorMessage = "パスワードを入力してください")] public string Password { get { return _password; } set { if (SetProperty(ref _password, value)) { ValidateProperty(value); } } } public CreateUserViewModel() { _errors = new ErrorsContainer<string>(RaiseErrorsChanged); } protected void ValidateProperty(object value, [CallerMemberName] string propertyName = null) { var context = new ValidationContext(this) { MemberName = propertyName }; var validationErrors = new List<ValidationResult>(); if (!Validator.TryValidateProperty(value, context, validationErrors)) { _errors.SetErrors(propertyName, validationErrors.Select(error => error.ErrorMessage)); } else { _errors.ClearErrors(propertyName); } } private void RaiseErrorsChanged([CallerMemberName] string propertyName = null) { ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName)); } public IEnumerable GetErrors(string propertyName) => _errors.GetErrors(propertyName); public bool HasErrors => _errors.HasErrors; public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged; } }
解説
(XAML)PasswordBoxコントロールのPasswordプロパティは依存関係プロパティではないので、ヘルパークラスで添付プロパティ(Password)を作成して、VIewModelのプロパティ(Password)とバインディングする。
(PasswordBoxHelper.cs)PasswordBoxのPasswordChangedイベントに処理を追加して、PasswordBoxの値が変わったら、ヘルパークラスの添付プロパティに同じ値を入力する。
(XAML)ヘルパークラスの添付プロパティと、VIewModelのプロパティがバインディングされているので、ViewModelのプロパティ(Password)が更新される。
バリデーションをしない場合は、PasswordBoxの値がバインディングしたViewModelのプロパティから取得できれば良い、つまり画面からソースへの反映ができれば良く、ViewModelのプロパティをPasswordBoxに反映する、つまりソースから画面への反映は必要無いので、バインドモードはOneWayToSourceでも良いはず。
バリデーションはTextBoxコントロールみたいに、Validation.ErrorTemplateを使用した。
バリデーションを使用した場合は、ViewModelのバリデーション結果によって、画面を更新する(メッセージの表示等)ため、バインドモードはTwoWayを指定する必要があった。指定しないとバリデーションを設定しても行われなかった。
動作環境
- .NET Core 3.1
- Prism 7.2
以上