WPF で ItemsControl 内の子コントロールの変更を親へ通知する
◆ はじめに
ItemsControl にバインドしたリスト要素に応じて、TextBox が生成されるような場面で、TextBox の値が変わったらその都度変更通知を受け取りたい、そんなニッチな要望を満たすためにかなりはまったのでメモ。素の WPF ならどう実現できるか、ライブラリを駆使したらどうなるか、など色々思考錯誤してみた。
◆ 具体的にはどんな画面か
サンプルとして、以下イメージの画面で実装していく。ItemsControl にバインドされたリスト(数値)分だけ TextBox が生成され、その TextBox の合計が一番下の TextBlock に表示される。合計の値は、ItemsControl 内 TextBox の値が変更されるたびに更新されるようにする。
実現方法については、3つの方法で試してみた。
- 素の WPF のみ
- Prism
- Prism + ReactiveProperty
◆ 実現方法
1. 素の WPF のみ
リストに表示するためのデータをクラスとして作成し、そのクラス内のプロパティ変更箇所( Setter )で、親(リストが配置されている ViewModel )に対しての変更通知処理を実行する。ポイントは、以下3点かな。
- データクラスに INotifyPropertyChanged インターフェースを実装
- リストは ObservableCollection で定義
- 親への変更通知は、 Action で実現し、データクラスのコンストラクタで設定
多分この方法が一番簡単だと思います(優しいマサカリ期待)。
Number.cs
using System; using System.ComponentModel; namespace WPFSampleTextBoxListNotification.Models { public class Number : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; public void OnPropertyChanged(string name) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); private int _amount; public int Amount { get => _amount; set { _amount = value; OnPropertyChanged(nameof(Amount)); _amountChangedNotification?.Invoke(); } } private Action _amountChangedNotification = null; public Number(int amount, Action amountChangedNotification = null) { Amount = amount; _amountChangedNotification = amountChangedNotification; } } }
NormalWindowViewModel.cs
using System.Collections.ObjectModel; using System.ComponentModel; using System.Linq; using WPFSampleTextBoxListNotification.Models; namespace WPFSampleTextBoxListNotification.ViewModels { public class NormalWindowViewModel : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; public void OnPropertyChanged(string name) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); public string _title = "素のWPF"; public string Title { get => _title; set { _title = value; OnPropertyChanged(nameof(Title)); } } public ObservableCollection<Number> _numbers; public ObservableCollection<Number> Numbers { get => _numbers; set { _numbers = value; OnPropertyChanged(nameof(Title)); } } public int _total; public int Total { get => Numbers.Sum(x => x.Amount); } public NormalWindowViewModel() { var list = new ObservableCollection<Number>() { new Number(100, () => OnPropertyChanged(nameof(Total))), new Number(200, () => OnPropertyChanged(nameof(Total))), new Number(300, () => OnPropertyChanged(nameof(Total))), }; Numbers = list; } } }
NormalWindow.xaml
<Window x:Class="WPFSampleTextBoxListNotification.Views.NormalWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" Title="NormalWindow" Width="300" Height="400" WindowStartupLocation="CenterScreen" mc:Ignorable="d"> <Grid Margin="10"> <Grid.RowDefinitions> <RowDefinition Height="auto" /> <RowDefinition Height="*" /> <RowDefinition Height="auto" /> </Grid.RowDefinitions> <Label Grid.Row="0" Content="素のWPF" /> <ItemsControl Grid.Row="1" Margin="5" Padding="5" ItemsSource="{Binding Numbers}"> <ItemsControl.ItemTemplate> <DataTemplate> <TextBox Margin="5" Padding="5" HorizontalContentAlignment="Right" Text="{Binding Amount, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" /> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl> <TextBlock Grid.Row="2" Margin="5" Padding="5" HorizontalAlignment="Right" Text="{Binding Total, Mode=OneWay}" /> </Grid> </Window>
2. Prism
次は、 MVVM フレームワークである Prism を導入したパターンで実装してみる。基本、素の WPF と同じものになる。変更通知のメソッドが Prsim が提供する RaisePropertyChanged になっただけ。また、 INotifyPropertyChanged のインターフェースも自動実装されているので、 ViewModel 内の記述量が減った感じ。
3. Prism + ReactiveProperty
最後は、Prism + ReactiveProperty で実装したパターン。ReactiveProperty については、以下を眺めれば何となくわかると思われる。
このパターンだと、上述した2つのパターンのようにリスト要素のデータクラス内に通知する仕組みを実装する必要がない。ReactiveProperty が提供する ObserveElementObservableProperty でリスト内要素の変更監視が可能となっている。ただ、リスト要素のデータクラスを Prism の変更通知で実装するとうまく動かなかった。なので、 ReactiveProperty で全て実装している。
また、はまりどころとしては、using Sysmte;
の記述がないと、Subscribe でエラーとなる。
NumberReactiveProperty.cs
using Prism.Mvvm; using Reactive.Bindings; namespace WPFSampleTextBoxListNotification.Models { public class NumberReactiveProperty : BindableBase { public ReactiveProperty<int> Amount { get; set; } = new ReactiveProperty<int>(); public NumberReactiveProperty(int amount) { Amount.Value = amount; } } }
ReactivePropertyWindowViewModel.cs
using Prism.Mvvm; using Reactive.Bindings; using Reactive.Bindings.Extensions; using System; using System.Collections.ObjectModel; using System.Linq; using WPFSampleTextBoxListNotification.Models; namespace WPFSampleTextBoxListNotification.ViewModels { public class ReactivePropertyWindowViewModel : BindableBase { public ReactiveProperty<string> Title { get; set; } = new ReactiveProperty<string>("Prism + ReactiveProperty"); public ReactiveCollection<NumberReactiveProperty> Numbers { get; } = new ReactiveCollection<NumberReactiveProperty>(); public ReactiveProperty<int> Total { get; } = new ReactiveProperty<int>(); public ReactivePropertyWindowViewModel() { var list = new ObservableCollection<NumberReactiveProperty>() { new NumberReactiveProperty(100), new NumberReactiveProperty(200), new NumberReactiveProperty(300), }; Numbers .ObserveElementObservableProperty(x => x.Amount) .Subscribe(x => { Total.Value = Numbers.Sum(y => y.Amount.Value); }); Numbers.AddRange(list); } } }
ReactivePropertyWindow.xaml
<Window x:Class="WPFSampleTextBoxListNotification.Views.ReactivePropertyWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:prism="http://prismlibrary.com/" Title="{Binding Title.Value}" Width="300" Height="400" prism:ViewModelLocator.AutoWireViewModel="True" WindowStartupLocation="CenterScreen"> <Grid Margin="10"> <Grid.RowDefinitions> <RowDefinition Height="auto" /> <RowDefinition Height="*" /> <RowDefinition Height="auto" /> </Grid.RowDefinitions> <Label Grid.Row="0" Content="Prism + ReactiveProperty" /> <ItemsControl Grid.Row="1" Margin="5" Padding="5" ItemsSource="{Binding Numbers}"> <ItemsControl.ItemTemplate> <DataTemplate> <TextBox Margin="5" Padding="5" HorizontalContentAlignment="Right" Text="{Binding Amount.Value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" /> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl> <TextBlock Grid.Row="2" Margin="5" Padding="5" HorizontalAlignment="Right" Text="{Binding Total.Value, Mode=OneWay}" /> </Grid> </Window>
◆ まとめ
なんかできそうなんだけど、デフォルトではできないみたいなニッチさで、調べたり実験したりするのが大変だった。どの実装でもそこそこ綺麗にできたので満足している。ReactiveProperty については、基本便利だけど、細かい部分で動きがつかみ辛く、自分のノウハウ貯めないと仕事では使えないかなーって感じ。サンプルソースは以下。見たい方はどうぞ。