Functional Reactive Programming + MVVM --- C# WPF

下記記事にて、Functional Reactive ProgrammingライブラリであるSodiumとWPFを連携させました(F#)
SodiumでFunctional Reactive Programming (F#) --- WPF連携 - 何でもプログラミング

今回は、C#にてSodiumとWPFを連携させてみたいと思います。

アプリケーションコード

いつも通り、カウンタを実装してみたいと思います。

f:id:any-programming:20170606134101p:plain

Streamを引数とするコンストラクタを持ち、DiscreteCellプロパティを公開するクラスを定義します。

引数のStreamがICommandに変換され、DiscreteCellの変更がINotifyPropertyChangedとして通知されるDataContextを利用します。(実装は後述します。)

(Sodiumは最近、CellとDiscreteCellを区別するようになりました。基本はDiscreteCellを使うことになると思います。)

Xaml

<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>

C#

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;
    }
}