Elm Architectureを利用したMVVM --- C# WPF

下記記事にてF#でElm ArchitectureをWPFに導入してみました。
F#でWPF --- Elm Architectureを利用したMVVM - 何でもプログラミング

今回はC#で近いものを実装してみたいと思います。

F#の方ではサポートしたModel→ViewModelの変換は省略していますので、必要な場合は追加で実装してください。

アプリケーションコード

今回もカウンタアプリケーションを実装してみたいと思います。
f:id:any-programming:20170606134101p:plain

後述しますDataContextクラスを利用すると、下記のようなアプリケーションコードになります。

F#の時と異なり、Modelの更新は判別共用体を利用するのではなく、メソッドを定義することでCommandとして呼び出されるようにしています。

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 Model
{
    public int Count { get; }
    public Model(int count) =>
        Count = count;
}

class Updater
{
    public Model Increment(Model model) =>
        new Model(model.Count + 1);
    public Model Decrement(Model model) =>
        new Model(model.Count - 1);
}

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        DataContext = new DataContext(new Model(0), new Updater());
    }
}


DataContextクラス

今回もDynamicObjectを利用して実装します。

Modelのプロパティと、Updaterで定義されたメソッドを呼び出すCommandを公開しています。

Commandクラスは単にExecuteをラムダで登録できるようにしたクラスです。

public class DataContext : DynamicObject, INotifyPropertyChanged
{
    object _model;
    Dictionary<string, PropertyInfo> _propertyInfos;
    Dictionary<string, ICommand> _commands;
    public event PropertyChangedEventHandler PropertyChanged;
    public DataContext(object initialModel, object updater)
    {
        _model = initialModel;
        _propertyInfos = initialModel.GetType().GetProperties().ToDictionary(x => x.Name);
        _commands = updater.GetType().GetMethods().ToDictionary(
            method => method.Name,
            method => (ICommand)new Command<object>(parameter =>
            {
                object prevModel = _model;
                object[] parameters = parameter == null 
                    ? new[] { prevModel } 
                    : new[] { prevModel, parameter };
                _model = method.Invoke(updater, parameters);

                // 変更箇所通知
                if (prevModel != _model)
                    foreach (var property in _propertyInfos.Values)
                        if (property.GetValue(prevModel) != property.GetValue(_model))
                            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property.Name));
            }));
    }
    public override bool TryGetMember(GetMemberBinder binder, out object result)
    {
        result = _propertyInfos.ContainsKey(binder.Name)
            ? _propertyInfos[binder.Name].GetValue(_model)
            : _commands[binder.Name];
        return true;
    }
}