ユーザーコントロール内で更新した値を親の ViewModel に反映させる方法

MVVM でユーザーコントロールを作るときに微妙にハマるのがユーザーコントロールです。例えば、ユーザーコントロール内に TextBox を貼り付けておいて、外側の ViewModel からユーザーコントロールに値を設定することはできるのですが、逆にユーザーコントロール内の TextBox に入力された値を ViewModel の反映しようとすると難儀します。というか、難儀していたのだけど、解決できたので書き残しておきます。

image

UserControl に対して ViewModel 側からバインドしたい場合は、DependencyProperty を使って依存関係プロパティを作って設定します。表示のための TextBlock とか DataGrid を使ったリスト表示の場合は、これで十分なのであまり問題はないのですが、さて、UserControl 内に貼り付けた TextBox から ViewModel のプロパティに反映させるためにはどうしたものか?と考えてしまうわけです。

色々調べていくと、ElementName を使って TextBox.Text に直接バインドする仕組みが紹介されているのですが、この方法だと UserControl の独立性が低くなって汎用性が低くなります。じゃあ、標準の TextBox やその他のコントロールのがりがりにカスタムで組んでしまえばできそうなものなのですが、これもちょっと大変すぎる。

BindingOperations.GetBindingExpression を使う

.NET Framework の WPF の TextBox コードを直接眺めていくと https://referencesource.microsoft.com/#PresentationFramework/src/Framework/System/Windows/Controls/TextBox.cs,77a66fa4f401a49f? BindingOperations.GetBindingExpression というメソッドがあって、バインド関係の情報を取得することができます。バインド先のプロパティ名を示す Path プロパティや、結びついているオブジェクトを示す Target プロパティなどがあります。

コントロールから ViewModel への値の設定(コントロールからソースへの反映)は、主にコントロールからフォーカスが外れたときになるので LostFocus イベントで設定すると以下のようにシンプルに書けます。

this.text.LostFocus += (_, __) => {
    BindingExpression beb = BindingOperations.GetBindingExpression(this, YourNameProperty);
    if (beb != null)
    {
        beb.Target.SetValue(YourNameProperty, text.Text);
        beb.UpdateSource();
    }
};

バインドをしていないと、BindingOperations.GetBindingExpression メソッドは null を返すのでそれだけチェックをします。

バインド可能なユーザーコントロールを作る

image

2つのテキストボックスと1つの日付コントロールを持つユーザーコントロールを作ります。ユーザーコントロール内で MVVM してしまうと、MainView の DataContext と競合してしまうので、ユーザーコントロール内では x:Name で名前を付けて参照します。


public partial class PersonControl : UserControl
 {
     public PersonControl()
     {
         InitializeComponent();
         this.Loaded += PersonControl_Loaded;
     }

    public static readonly DependencyProperty YourNameProperty =
         DependencyProperty.Register(
             "YourName",
             typeof(string),
             typeof(PersonControl),
             new FrameworkPropertyMetadata(
                 "",
                 FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                 new PropertyChangedCallback((o, e) => {
                     var uc = o as PersonControl;
                     if (uc != null)
                     {
                         string v = (string)e.NewValue;
                         uc.text.Text = v;
                     }
                 })));
     public static readonly DependencyProperty YourAddrProperty =
         DependencyProperty.Register(
             "YourAddr",
             typeof(string),
             typeof(PersonControl),
             new FrameworkPropertyMetadata(
                 "",
                 FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                 new PropertyChangedCallback((o, e) => {
                     var uc = o as PersonControl;
                     if (uc != null)
                     {
                         string v = (string)e.NewValue;
                         uc.addr.Text = v;
                     }
                 })));
     public static readonly DependencyProperty YourBirthdayProperty =
         DependencyProperty.Register(
             "YourBirthday",
             typeof(DateTime),
             typeof(PersonControl),
             new FrameworkPropertyMetadata(
                 DateTime.Now,
                 FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                 new PropertyChangedCallback((o, e) => {
                     var uc = o as PersonControl;
                     if (uc != null)
                     {
                         DateTime v = (DateTime)e.NewValue;
                         uc.date.SelectedDate = v;
                     }
                 })));
         
     public string YourName { get => this.text.Text; set => this.text.Text = value; }
     public string YourAddr { get => this.addr.Text; set => this.addr.Text = value; }
     public DateTime? YourBirthday { get => this.date.SelectedDate; set => this.date.SelectedDate = value; }

    private void PersonControl_Loaded(object sender, RoutedEventArgs e)
     {
         this.text.LostFocus += (_, __) => {
             BindingExpression beb = BindingOperations.GetBindingExpression(this, YourNameProperty);
             if (beb != null)
             {
                 beb.Target.SetValue(YourNameProperty, text.Text);
                 beb.UpdateSource();
             }
         };
         this.addr.LostFocus += (_, __) => {
             BindingExpression beb = BindingOperations.GetBindingExpression(this, YourAddrProperty);
             if (beb != null)
             {
                 beb.Target.SetValue(YourAddrProperty, addr.Text);
                 beb.UpdateSource();
             }
         };
         this.date.LostFocus += (_, __) => {
             BindingExpression beb = BindingOperations.GetBindingExpression(this, YourBirthdayProperty);
             if (beb != null)
             {
                 if (date.SelectedDate != null)
                 {
                     beb.Target.SetValue(YourBirthdayProperty, date.SelectedDate);
                     beb.UpdateSource();
                }
             }
         };
    }
}

3つのコントロールが貼ってありますので多少コードが長くなっていますが、

  • DependencyProperty で、外側から設定できるプロパティを作る
  • YourName 等で、簡易アクセスができるプロパティを作る
  • LostFocus イベントで、内側から外の ViewModel に値を設定する

ということをやっています。ユーザーコントロールは3つのプロパティ

  • YourName
  • YourAddr
  • YourBirthday

を持ちます。

謎だったユーザーコントロール内の TextBox 等の入力コントロール(これ自体はバインド機能がなくてよい)から、外側の ViewModel にデータを渡すためには、

  • BindingOperations.GetBindingExpression でバインド先を探す
  • Target.SetValue で値を更新する
  • UpdateSourceでソース(バインド先のViewModel)に通知する

という手順になります。通知するタイミングは、サンプルの通り LostFocus イベントでもよいし、TextBox 内のテキストが変更されたタイミング、キータイプされたタイミングでも良いでしょう。

MainWindow から使う

image

メインウィンドウにユーザーコントロールといくつかのボタンを貼り付けます。ユーザーコントロールへのバインドは、独自に作ったプロパティにバインドさせます。

<local:PersonControl 
 YourName="{Binding Name}" 
 YourAddr="{Binding Addr}" 
 YourBirthday="{Binding Birthday}" 
 Margin="4" Grid.Row="1" Height="99" VerticalAlignment="Top"/>

後はふつうの TextBox などと同じように MVVM でバインドするだけですね。これだとユーザーコントロールは一般的なコントロールと変わらないので、


public partial class MainWindow : Window
 {
     public MainWindow()
    {
         InitializeComponent();
         this.Loaded += (_, __) =>
         {
             _vm = new MainViewModel();
             this.DataContext = _vm;
         };
     }
     MainViewModel _vm;

    private void clickSave(object sender, RoutedEventArgs e)
     {
         _vm.Output = $"{_vm.Name}  {_vm.Addr}  {_vm.Birthday.ToString()}"; 
     }

    private void clickLoad(object sender, RoutedEventArgs e)
     {
         _vm.Name = "MASDUA";
         _vm.Addr = "OSAKA";
         _vm.Birthday = DateTime.Parse( "2000/1/1");

    }
 }
 public class MainViewModel : ObservableObject
 {
     private string _name ;
     private string _addr;
     private DateTime? _birthday;
     private string _output;

    public string Name { get => _name; set => SetProperty(ref _name, value, nameof(Name)); }
     public string Addr { get => _addr; set => SetProperty(ref _addr, value, nameof(Addr)); }
     public DateTime? Birthday { get => _birthday; set => SetProperty(ref _birthday, value, nameof(Birthday)); }
     public string Output { get => _output; set => SetProperty(ref _output, value, nameof(Output)); }
 }

実行すると、こんな感じになります。

image

参考にしてみてください。

カテゴリー: C# パーマリンク