Functional Reactive Programming + MVVM --- C# WPF
下記記事にて、Functional Reactive ProgrammingライブラリであるSodiumとWPFを連携させました(F#)
SodiumでFunctional Reactive Programming (F#) --- WPF連携 - 何でもプログラミング
今回は、C#にてSodiumとWPFを連携させてみたいと思います。
アプリケーションコード
いつも通り、カウンタを実装してみたいと思います。
Streamを引数とするコンストラクタを持ち、DiscreteCellプロパティを公開するクラスを定義します。
引数のStreamがICommandに変換され、DiscreteCellの変更がINotifyPropertyChangedとして通知されるDataContextを利用します。(実装は後述します。)
(Sodiumは最近、CellとDiscreteCellを区別するようになりました。基本はDiscreteCellを使うことになると思います。)
<Window x:Class="WpfApp.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="100" Width="200"> <Grid Height="23" Margin="10"> <Grid.ColumnDefinitions> <ColumnDefinition Width="23"/> <ColumnDefinition Width="*"/> <ColumnDefinition Width="23"/> </Grid.ColumnDefinitions> <Button Command="{Binding Decrement}" Content="◀" Grid.Column="0" /> <TextBox Text="{Binding Count}" Grid.Column="1" IsReadOnly="True" TextAlignment="Center" VerticalContentAlignment="Center" /> <Button Command="{Binding Increment}" Content="▶" Grid.Column="2" /> </Grid> </Window>
class Counter { public DiscreteCell<int> Count { get; } public Counter(Stream<Unit> increment, Stream<Unit> decrement) { Count = Transaction.Run(() => { DiscreteCellLoop<int> count = new DiscreteCellLoop<int>(); var stream = increment.MapTo(1).OrElse(decrement.MapTo(-1)).Snapshot(count, (dx, x) => x + dx); count.Loop(stream.Hold(0)); return count; }); } }
public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); DataContext = new DataContext<Counter>(); } }
DataContext
今回もDynamicObjectを利用して実装します。
まずコンストラクタの引数(複数のStream)を作成し、コンストラクタを呼び出してインスタンスを作成します。
作成したStreamのSinkはCommandとして公開されます。
続いてインスタンスのDiscreteCellプロパティをListenしてPropertyChangedに関連付けます。
public class DataContext<T> : DynamicObject, INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; object _instance; Dictionary<string, ICommand> _commands = new Dictionary<string, ICommand>(StringComparer.OrdinalIgnoreCase); Dictionary<string, object> _propertyValues = new Dictionary<string, object>(); List<IListener> _listeners = new List<IListener>(); public DataContext() { // コンストラクタの引数(Streams)を作成し、インスタンス作成 var constructor = typeof(T).GetConstructors().Single(); var args = constructor.GetParameters().Select(parameter => { var type = parameter.ParameterType.GetGenericArguments()[0]; return GetType().GetMethod(nameof(CreateStream), BindingFlags.Instance | BindingFlags.NonPublic) .MakeGenericMethod(type).Invoke(this, new object[] { parameter.Name }); }).ToArray(); _instance = constructor.Invoke(args); // インスタンスのDiscreteCellプロパティとPropertyChangedを関連付け foreach (var property in typeof(T).GetProperties()) { var cell = property.GetValue(_instance); var type = property.PropertyType.GetGenericArguments()[0]; GetType().GetMethod(nameof(Listen), BindingFlags.Instance | BindingFlags.NonPublic) .MakeGenericMethod(type).Invoke(this, new object[] { property.Name, cell }); } } // DiscreteCellをListenし、PropertyChangedを呼び出す void Listen<U>(string name, DiscreteCell<U> cell) => _listeners.Add(cell.Calm().Listen(x => { _propertyValues[name] = x; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); })); // Streamを発火するCommandを作成・保存し、Streamを返す Stream<U> CreateStream<U>(string name) { var sink = new StreamSink<U>(); _commands[name] = new Command<U>(sink.Send); return sink; } public override bool TryGetMember(GetMemberBinder binder, out object result) { result = _propertyInfos.ContainsKey(binder.Name) ? _propertyInfos[binder.Name].GetValue(_model) : _commands[binder.Name]; return true; } }