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 については、以下を眺めれば何となくわかると思われる。
- https://github.com/runceel/ReactiveProperty
- http://okazuki.jp/ReactiveProperty/
- https://blog.okazuki.jp/entry/2015/12/05/221154
- https://qiita.com/YSRKEN/items/5a36fb8071104a989fb8
このパターンだと、上述した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 については、基本便利だけど、細かい部分で動きがつかみ辛く、
自分のノウハウ貯めないと仕事では使えないかなーって感じ。
サンプルソースは以下。見たい方はどうぞ。
https://github.com/shuntaro4/WPFSampleTextBoxListNotification