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 => ...
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; } }
Elm Architectureを利用したMVVMでのUserControl --- C# WPF
下記記事にて、C#でElm Architectureを利用してWPFアプリケーションを作成しました。
Elm Architectureを利用したMVVM --- C# WPF - 何でもプログラミング
今回はElm ArchitectureでUserControlを実装してみたいと思います。
F#版での実装は下記記事を参照してください。
F#でWPF --- Elm Architectureで実装されたUserControl - 何でもプログラミング
アプリケーションコード
Counterコントロールを配置しただけのViewと、値を保存・変更するだけのModel・Updaterになります。
引き続き、この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 Model { public int Value { get; } public Model(int value) => Value = value; }
class Updater { public Model SetValue(Model model, int value) => new Model(value); }
public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); DataContext = new DataContext(new Model(0), new Updater()); } }
Counterコントロール
Increment及びDecrementが押された場合にMain側に通知し(ValueChanged)、Main側の値(Value)が変わった場合にカウンタの値を更新するように実装してあります。
C#で利用しているDependencyPropertyManagerは下記記事を参照してください。
DependencyProperty定義の記述量削減 - 何でもプログラミング
また、RegisterWithUpdateName、DataContextクラスは後程実装いたします。
<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>
public partial class Counter : UserControl { static DependencyPropertyManager<Counter> _dp = new DependencyPropertyManager<Counter>(); // 値が変わった場合にUpdaterのSetCountを呼ぶように登録 public static readonly DependencyProperty ValueProperty = _dp.RegisterWithUpdateName<int>(nameof(Updater.SetCount)); public int Value { get => (int)_dp.Get(this); set => _dp.Set(this, value); } public static readonly DependencyProperty ValueChangedProperty = _dp.Register<ICommand>(); public ICommand ValueChanged { get => (ICommand)_dp.Get(this); set => _dp.Set(this, value); } class Model { public int Count { get; } public Model(int count) => Count = count; } class Updater { Counter _uc; public Updater(Counter uc) => _uc = uc; public PostCommand Increment(Model model) => new PostCommand(_uc.ValueChanged, model.Count + 1); public PostCommand Decrement(Model model) => new PostCommand(_uc.ValueChanged, model.Count - 1); public Model SetCount(Model model, int count) => new Model(count); } public Counter() { InitializeComponent(); // UserControlのDataContextはMain側で利用されるので、Content(今回はGrid)のDataContextにセット ((FrameworkElement)Content).DataContext = new DataContext(new Model(0), new Updater(this)); } }
RegisterWithUpdateName
値変更のコールバック時に、UserControlのContentのDataContextのExecute(後述します。)を呼び出します。
public DependencyProperty RegisterWithUpdateName<TValue>(string updateName, TValue defaultValue = default(TValue), [CallerMemberName]string name = "") { name = name.Substring(0, name.Length - "Property".Length); var metadata = new PropertyMetadata(defaultValue, (obj, args) => (((obj as UserControl).Content as FrameworkElement).DataContext as DataContext).Execute(updateName, args.NewValue)); return DependencyProperty.Register(name, typeof(TValue), typeof(TClass), metadata); }
DataContext
元々のDataContextではModelを返す関数しか対応していませんでしたが、Model更新後にCommandを実行できるよう、PostCommandクラスを返す関数にも対応させます。
またRegisterWithUpdateNameからCommandを呼び出せるよう、Execute関数も実装します。
public class PostCommand { public Action Execute { get; } public object Model { get; } public PostCommand(ICommand command, object parameter, object model = null) { Model = model; Execute = () => command?.Execute(parameter); } }
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 }; object result = method.Invoke(updater, parameters); if (result.GetType() == _model.GetType()) _model = result; // PostCommandのModelをセット(nullの場合は変更なし) else if (result is PostCommand) _model = ((PostCommand)result).Model ?? _model; if (prevModel != _model) foreach (var property in _propertyInfos.Values) if (property.GetValue(prevModel) != property.GetValue(_model)) PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property.Name)); // PostCommandの場合、Command実行 (result as PostCommand)?.Execute(); })); } public override bool TryGetMember(GetMemberBinder binder, out object result) { result = _propertyInfos.ContainsKey(binder.Name) ? _propertyInfos[binder.Name].GetValue(_model) : _commands[binder.Name]; return true; } // 外部からコマンドを呼び出す用 public void Execute(string name, object parameter) => _commands[name].Execute(parameter); }
Elm Architectureを利用したMVVM --- C# WPF
下記記事にてF#でElm ArchitectureをWPFに導入してみました。
F#でWPF --- Elm Architectureを利用したMVVM - 何でもプログラミング
今回はC#で近いものを実装してみたいと思います。
F#の方ではサポートしたModel→ViewModelの変換は省略していますので、必要な場合は追加で実装してください。
アプリケーションコード
今回もカウンタアプリケーションを実装してみたいと思います。
後述します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>
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; } }
C# Immutableオブジェクト 導出項目のキャッシュ機能
例として下記のようなVector2クラスを考えます。
Lengthプロパティは、呼び出される毎に計算を行っています。
処理が軽い場合は特に問題ありませんが、導出に時間がかかるプロパティは結果をキャッシュしてあるほうが好ましいです。
今回はキャッシュを行う機能を実装してみたいと思います。
public class Vector2 { public int X { get; } public int Y { get; } public double Length => Math.Sqrt(X * X + Y * Y); public Vector2(int x, int y) { X = x; Y = y; } }
Cache構造体
簡単のため、入力はValueTupleを利用します。(ValueTupleを利用しない場合は、TArgsを増やした複数のCache構造体を定義してください。)
入力に変化がない場合は、前回の値を返すように実装しています。
構造体である理由は、Immutableクラスが変更・複製されたときに、キャッシュを共有してしまわないようにするためです。
struct Cache<TArgs, TResult> { bool _initialized; TArgs _args; TResult _result; Func<TArgs, TResult> _f; public Cache(Func<TArgs, TResult> f) { _initialized = false; _args = default(TArgs); _result = default(TResult); _f = f; } public TResult Get(TArgs args) { if (_initialized && args.Equals(_args)) return _result; _initialized = true; _args = args; _result = _f(args); return _result; } }
Vector2書換え
新たにキャッシュのフィールドを追加し、LengthプロパティではGetを呼び出しています。
public class Vector2 { public int X { get; } public int Y { get; } Cache<(int, int), double> _length = new Cache<(int x, int y), double>(args => Math.Sqrt(args.x * args.x + args.y * args.y)); public double Length => _length.Get((X, Y)); public Vector2(int x, int y) { X = x; Y = y; } }
動作確認
同じオブジェクトに対し、二回目以降のLength呼出ではキャッシュが返るようになります。
Withで複製した場合でも問題なく動作していることも確認できます。(Withに関しましては、下記記事を参照してください。)
ネストしたImmutableオブジェクトの更新(C#) - 何でもプログラミング
Vector2 v = new Vector2(1, 1); v.Length; v.Length; // キャッシュが返る Vector2 v2 = v.With(x => x.Y, 2); v.Length; // キャッシュが返る v2.Length; v2.Length; // キャッシュが返る
ImmutableオブジェクトのJSONシリアライズ、デシリアライズ(C#)
今回はNewtonsoft.Jsonを利用して、Immutableオブジェクトをシリアライズ、デシリアライズしてみたいと思います。
.NetにはDataContractJsonSerializerが標準で用意されていますが、DataContract、DataMember属性を付加する必要があり、動作のカスタマイズが大変そうであったため、Newtonsoft.Jsonを採用しました。
対象オブジェクト
下記のクラスをシリアライズ、デシリアライズしてみたいと思います。
FullNameは導出項目のため、シリアライズ対象から外される必要があります。
public class Person { public int Age { get; } public string FirstName { get; } public string LastName { get; } public Person Child { get; } public string FullName => FirstName + " " + LastName; public Person(int age, string firstName, string lastName, Person child) { Age = age; FirstName = firstName; LastName = lastName; Child = child; } }
var person = new Person(35, "John", "Smith", new Person(10, "Richard", "Smith", null));
シリアライズ
デフォルトでは、publicなgetterからの値をシリアライズします。
そのため、FullNameもシリアライズされてしまいます。
そこで導出項目は無視するようContractResolverを独自に定義します。
class SerializeResolver : DefaultContractResolver { protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) { var p = base.CreateProperty(member, memberSerialization); var field = member.DeclaringType.GetField($"<{member.Name}>k__BackingField", BindingFlags.Instance | BindingFlags.NonPublic); // setterがなく、BackingFieldも持たないものは無視 p.Ignored = field == null && p.Writable == false; return p; } }
シリアライズは、このContractResolverを利用して下記のように記述できます。
var setting = new JsonSerializerSettings() { ContractResolver = new SerializeResolver() }; string json = JsonConvert.SerializeObject(person, setting);
問題なくシリアライズされていることが確認できます。(Visual StudioのJSONビューワで確認)
デシリアライズ
デフォルトではsetterが存在しないプロパティはデシリアライズできません。
そこでプロパティが復元できるよう、独自のContractResolverを定義します。
class DeserializeResolver : DefaultContractResolver { protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) { var p = base.CreateProperty(member, memberSerialization); if (p.Writable == false) { var field = member.DeclaringType.GetField($"<{member.Name}>k__BackingField", BindingFlags.Instance | BindingFlags.NonPublic); if (field != null) { // BackingFieldが存在する場合、ValueProviderをセット p.ValueProvider = new FieldValueProvider(field); p.Writable = true; } } return p; } }
FieldValueProviderは下記のように定義できます。
class FieldValueProvider : IValueProvider { FieldInfo _field; public FieldValueProvider(FieldInfo field) => _field = field; public object GetValue(object target) => _field.GetValue(target); public void SetValue(object target, object value) => _field.SetValue(target, value); }
デシリアライズは、このContractResolverを利用して下記のように記述できます。
var setting = new JsonSerializerSettings() { ContractResolver = new DeserializeResolver() }; var person = JsonConvert.DeserializeObject<Person>(json, seting);
BackingFieldを使わない場合
BackingFieldはコンパイラが勝手に作成するFieldのため、名称が変わる可能性は0ではありません。
心配な場合は、private setを追加して独自のContractResolverを定義してください。
ネストしたImmutableオブジェクトの更新(C#)
C#(7.0)でImmutableクラスを作成する場合、下記のような記述になります。
public class Person { public int Age { get; } public string Name { get; } public Person Child { get; } public Person(int age, string name, Person child) { Age = age; Name = name; Child = child; } }
下記のようなデータがあったとして、Richardの年齢を更新したデータを作成するのはとても面倒です。
var person = new Person(35, "John", new Person(10, "Richard", null));
そこでリフレクションを利用して、簡単に更新データを作成できるようにしてみたいと思います。
コード内で利用されているCreate、Scan、Zipオペレータは下記記事を参照してください。
LINQ独自オペレータ メモ - 何でもプログラミング
With関数定義
ExpressionTreeを利用してオブジェクトを更新します。
一つのオブジェクトの更新は、MemberwiseCloneとBackingFieldの更新で実現しています。
自動生成される<Name>k__BackingFieldは予告なく名称が変わる可能性がありますので、不安な場合はprivate set;を追加して、PropertyInfo.SetValueを利用するようにしてください。
public static TObj With<TObj, TValue>(this TObj obj, Expression<Func<TObj, TValue>> expression, TValue value) { // MemberExpressionをたどる var members = EnumerableEx.Create(expression.Body as MemberExpression, x => x.Expression as MemberExpression) .TakeWhile(x => x != null) .ToArray(); // 各メンバの現在値取得 var objs = members.Reverse() .Scan((object)obj, (x, m) => ((PropertyInfo)m.Member).GetValue(x)) .ToArray(); // 値を下層から順次更新 return (TObj)objs.Reverse().Skip(1) .Zip(members.Select(x => x.Member.Name)) .Aggregate((object)value, (state, x) => With(x.Item1, x.Item2, state)); } static object With(object obj, string propertyName, object value) { var type = obj.GetType(); var clone = type.GetMethod("MemberwiseClone", BindingFlags.Instance | BindingFlags.NonPublic).Invoke(obj, null); type.GetField($"<{propertyName}>k__BackingField", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(clone, value); return clone; }
利用
冒頭のデータに対し下記の様に記述することにより、Richardの年齢が11に更新された新たなデータが作成されます。
var newPerson = person.With(x => x.Child.Age, 11);