Functional Reactive Programming + MVVMでのUserControl --- C# WPF
下記記事にてFRPライブラリのSodiumとWPFを連携させてみました。
Functional Reactive Programming + MVVM --- C# WPF - 何でもプログラミング
今回はUserControlでも利用できるよう実装してみたいと思います。
アプリケーションコード
いつも通り、カウンタアプリケーションを実装します。
Main側は純粋にValueの保存と更新を行うのみの実装になっています。
引き続きCounterコントロールを実装していきます。
<Window x:Class="WpfApp.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:WpfApp" Title="MainWindow" Height="100" Width="200"> <Grid> <local:Counter Value="{Binding Value}" ValueChanged="{Binding SetValue}" Margin="10" Height="23" /> </Grid> </Window>
class ViewModel { public DiscreteCell<int> Value { get; } public ViewModel(Stream<int> setValue) => Value = setValue.Hold(0); }
public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); DataContext = new DataContext<ViewModel>(); } }
Counterコントロール
通常のロジックに対し、DependencyPropertyからのDiscreteCellの入力への追加、ICommandへのイベント通知の追加を行います。
それに伴う、DataContextクラスの拡張、DependencyPropertyクラスの作成、Listen拡張メソッドの作成は、後述いたします。
<UserControl x:Class="WpfApp.Counter" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" mc:Ignorable="d" d:DesignHeight="23" d:DesignWidth="200"> <Grid Height="23"> <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> </UserControl>
class CounterVM { List<IListener> listeners = new List<IListener>(); public DiscreteCell<int> Count { get; } public CounterVM(UserControl1 uc, Stream<Unit> increment, Stream<Unit> decrement) { Count = _value.Cell(uc); increment.MapTo(1).OrElse(decrement.MapTo(-1)).Snapshot(Count, (dx, x) => x + dx) .Listen(listeners, () => uc.ValueChanged); } }
public partial class Counter: UserControl { static DependencyProperty<Counter, int> _value = new DependencyProperty<Counter, int>(); public static readonly DependencyProperty ValueProperty = _value.Register(); public int Value { get => _value.Get(this); set => _value.Set(this, value); } static DependencyProperty<Counter, ICommand> _valueChanged = new DependencyProperty<Counter, ICommand>(); public static readonly DependencyProperty ValueChangedProperty = _valueChanged.Register(); public ICommand ValueChanged { get => _valueChanged.Get(this); set => _valueChanged.Set(this, value); } public Counter() { InitializeComponent(); ((FrameworkElement)Content).DataContext = new DataContext<CounterVM>(this); } }
DependencyProperty
通常のRegister、Get、Setのほかに、DescreteCellが取得できるようになっています。
DescreteCellがきちんと解放されるよう、インスタンスはConditionalWeakTableで管理しています。
今回はViewModelからの通知を、DescreteCellSinkにPostSendしています。(ViewModel側もSodiumを利用している場合、Listenの中でSendを呼ぶ可能性があり、その場合例外で止まるので、それを避けるため。)
もしFRPの回路を途切らせたくない場合は、型をDescreteCell
public class DependencyProperty<TClass, TValue> where TClass : DependencyObject { DependencyProperty _property; ConditionalWeakTable<TClass, DiscreteCellSink<TValue>> _sinks = new ConditionalWeakTable<TClass, DiscreteCellSink<TValue>>(); public DependencyProperty Register(TValue defaultValue = default(TValue), [CallerMemberName]string name = "") { name = name.Substring(0, name.Length - "Property".Length); var metadata = new PropertyMetadata(defaultValue, (obj, args) => { if (_sinks.TryGetValue((TClass)obj, out var sink)) Transaction.Post(() => sink.Send((TValue)args.NewValue)); }); _property = DependencyProperty.Register(name, typeof(TValue), typeof(TClass), metadata); return _property; } public TValue Get(TClass obj) => (TValue)obj.GetValue(_property); public void Set(TClass obj, TValue value) => obj.SetValue(_property, value); public DiscreteCell<TValue> Cell(TClass obj) => _sinks.GetValue(obj, x => new DiscreteCellSink<TValue>(Get(x))); }
Listen拡張メソッド
StreamとICommandを結びつける関数です。
ViewModel側がSodiumを利用している可能性があるため、Transaction.PostでExecuteしています。(理由はDependencyPropertyクラスの時と同じです。)
FRP回路を途切らせたくない場合はICommandではなく、型をDiscreteCell
public static void Listen<T>(this Stream<T> source, List<IListener> listeners, Func<ICommand> getCommand) => listeners.Add(source.Listen(x => Transaction.Post(() => getCommand()?.Execute(x))));
DataContext
引数に好きな値を設定できるように修正しています。
public DataContext(params object[] preArgs) { var constructor = typeof(T).GetConstructors().Single(); var args = preArgs.Concat(constructor.GetParameters().Skip(preArgs.Length).Select(parameter => ...