2020/09/17 追記
ReactiveProperty Ver. 7.0.0以降はこの記事の内容は動作しません。
概要
ReactivePropertyをMessagePackCSharpでMessagePack形式とJSON形式にシリアライズ・デシリアライズする方法を紹介します。
ReactivePropertyはViewModel層で使用されることが多いライブラリですが、Model層に使用しても問題ありません。
アプリケーションの状態をファイルに保存したいときなどに、Modelのクラスをまるごと保存できると便利です。
しかしReactivePropertyは標準ではそのまま保存できません。
そこで、今回はModel層にReactivePropertyがあり、それをシリアライズ/デシリアライズする方法を紹介します。
解説
シリアライズ方法を説明する前に、使用するライブラリの紹介を簡単にしておきます。
ReactiveProperty
ReactivePropertyはWPFやUWPでの、MVVM + リアクティブプログラミングの組み合わせを快適にするためのライブラリです。
詳しくは以下を参照ください。
https://blog.okazuki.jp/entry/2015/12/05/221154
MessagePack
MessagePackは、効率の良いバイナリ形式のオブジェクト・シリアライズ フォーマットです。JSONの置き換えとして使うことができ、様々なプログラミング言語をまたいでデータを交換することが可能です。しかも、JSONよりも速くてコンパクトです。
詳しくは以下を参照ください。
https://msgpack.org/ja.html
MessagePack-CSharp
上記MessagePackをC#で扱うためのライブラリです。
MessagePackだけではなく、JSONにも対応しています。
同じ作者(nueueccさん)のUtf8Jsonというライブラリもありますが、ReactivePropertyを使うのであれば、対応した拡張パッケージがあるぶん、こちらの方が便利です。
詳しくは以下を参照ください。
http://neue.cc/2017/03/13_550.html
シリアライズ/デシリアライズ方法
nugetで以下のライブラリを取得する。
- ReactiveProperty
- MessagePack
- MessagePack.ReactiveProperty
- System.Reactive.Compatibility
ReactivePropertyのResolverを作成して、登録します。
デフォルトに登録せず、シリアライズ/デシリアライズ時にオプションとして指定することもできます。
var resolver = MessagePack.Resolvers.CompositeResolver.Create(
ReactivePropertyResolver.Instance,
MessagePack.Resolvers.ContractlessStandardResolver.Instance,
MessagePack.Resolvers.StandardResolver.Instance
);
MessagePackSerializer.DefaultOptions = MessagePack.MessagePackSerializerOptions.Standard.WithResolver(resolver);
ReactivePropertyを作成して、シリアライズします。
var rpText = new ReactivePropertySlim<string>("ABC");
//JSON形式にシリアライズ
string jsonRpText = MessagePack.MessagePackSerializer.SerializeToJson(rpText);
Console.WriteLine($"JSON:{jsonRpText}");
//JSON:[3,"ABC"]
//MessagePack形式にシリアライズ
byte[] mPackRpText = MessagePack.MessagePackSerializer.Serialize(rpText);
Console.WriteLine($"MessagePack:{String.Join(" ", mPackRpText.Select(x => x.ToString("X2")))}");
//MessagePack:92 03 A3 41 42 43
なお、シリアライズ結果内の"3"はReactivePropertyMode
です。
イベントハンドラなどはシリアライズのしようがありませんので、含まれていません。
デシリアライズではJSON形式は一度MessagePack形式を経由してからデシリアライズします。
byte[] bytesRpText = MessagePack.MessagePackSerializer.ConvertFromJson(jsonRpText);
ReactivePropertySlim<string> desiRpText = MessagePack.MessagePackSerializer.Deserialize<ReactivePropertySlim<string>>(bytesRpText);
Console.WriteLine($"desiRpText = {desiRpText}");
//desiRpText = ABC
なお、ReactivePropertyだけでなく、ReactivePropertySlimやIReactivePropertyでもシリアライズできます。ReadOnlyReactivePropertyはできません。
もちろんReactiveProperty単体ではなく、これを複数内包したクラスごとシリアライズすることもできます。
注意点
{get;}
だとシリアライズできない
ReactivePropertyを使用する場合、.Value
は変更されますが、ReactiveProperty自体を変更することはまずないので、プロパティのアクセッサは{get;}
で十分なことが多いです。
しかし、シリアライズ/デシリアライズする場合は{ get; set; }
にする必要があります。
競合エラー
ReactivePropertyのVer5以降とMessagePack.ReactivePropertyを導入していると、
System.Reactiveのバージョン間の整合性の問題で、以下のエラーが発生します。
エラー CS0121 次のメソッドまたはプロパティ間で呼び出しが不適切です:
'System.Reactive.Linq.Observable.Select<TSource, TResult>(System.IObservable<TSource>, System.Func<TSource, TResult>)' と
'System.Reactive.Linq.Observable.Select<TSource, TResult>(System.IObservable<TSource>, System.Func<TSource, TResult>)'
System.Reactive.Compatibilityをnugetで導入すると解決します。
デモアプリ
クラスをまるごとシリアライズ/デシリアライズできるデモアプリを作成してみました。
動作画面
姓と名の入力欄があり、変更するとフルネームが代わります。
[Serialize]ボタンを押下すると、下にシリアライズされたMessagePack(の16進数表示)とJSONの結果が表示されます。
下のJSONの結果を一部書き換えて("Anakin"👉"Luke")から、[Desirialize]ボタンを押下すると、上のReactivePropertyの結果が変わります。
デモアプリコード
全体はGithubにおいておきます。
https://github.com/soi013/ReactivePropertySerializeDemo
シリアライズ対象となるクラスは以下です。
public class RpNames
{
public ReactiveProperty<string> NameRp { get; set; } = new ReactiveProperty<string>("Anakin");
//ReactivePropertyでもReactivePropertySlimでもできる。
public ReactivePropertySlim<string> NameRps { get; set; } = new ReactivePropertySlim<string>("Skywalker");
//ReadOnlyはSerializeできない。
[IgnoreMember]
public ReadOnlyReactivePropertySlim<string> NameRorps { get; set; }
public RpNames()
{
//姓と名の変更を購読して、フルネームにする
NameRorps = Observable
.CombineLatest(NameRp, NameRps, (x, y) => $"{x}={y}")
.ToReadOnlyReactivePropertySlim();
}
}
シリアライズを行うMainWindowViewModelが以下です。
public class MainWindowViewModel : INotifyPropertyChanged
{
//メモリリークを防ぐためのダミー実装
public event PropertyChangedEventHandler PropertyChanged;
public RpNames Names { get; } = new RpNames();
public ReactiveProperty<string> MessagePackSerializedNames { get; } = new ReactiveProperty<string>();
public ReactiveProperty<string> JsonSerializedNames { get; } = new ReactiveProperty<string>();
public ReactiveCommand SerializeCommand { get; }
public ReactiveCommand DesirializeCommand { get; }
public MainWindowViewModel()
{
SerializeCommand = Names.NameRorps
.Select(x => x?.Length >= 3)
.ToReactiveCommand()
.WithSubscribe(() => Serialize());
DesirializeCommand = JsonSerializedNames
.Select(x => x?.Length > 5)
.ToReactiveCommand()
.WithSubscribe(() => Desirialize());
//ReactiveProperty用を含んだResolverのセットをデフォルトに設定しておく
var resolver = MessagePack.Resolvers.CompositeResolver.Create(
ReactivePropertyResolver.Instance,
MessagePack.Resolvers.ContractlessStandardResolver.Instance,
MessagePack.Resolvers.StandardResolver.Instance
);
MessagePackSerializer.DefaultOptions = MessagePack.MessagePackSerializerOptions.Standard.WithResolver(resolver);
}
private void Serialize()
{
var messagePackNames = MessagePackSerializer.Serialize(Names);
this.MessagePackSerializedNames.Value = String.Join(" ",
messagePackNames.Select(x => $"{x:X2}"));
this.JsonSerializedNames.Value = MessagePackSerializer.SerializeToJson(Names);
}
private void Desirialize()
{
//JSON側からデシリアライズ
var mPack = MessagePack.MessagePackSerializer.ConvertFromJson(JsonSerializedNames.Value);
var deserializedRPNames = MessagePackSerializer.Deserialize<RpNames>(mPack);
this.Names.NameRp.Value = deserializedRPNames.NameRp.Value;
this.Names.NameRps.Value = deserializedRPNames.NameRps.Value;
}
}
デモではViewModel内のクラスをシリアライズして、再び表示していますが、通常はModel層のクラスをファイルなりデータベースなりに保存する形になると思います。
表示するViewは以下です。
<Window
x:Class="ReactivePropertySerializeDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:ReactivePropertySerializeDemo"
Title="ReactiveProperty Serialize Demo"
Width="800" Height="450"
TextElement.FontSize="24">
<Window.DataContext>
<local:MainWindowViewModel />
</Window.DataContext>
<StackPanel>
<TextBox Margin="5" Text="{Binding Names.NameRp.Value}" />
<TextBox Margin="5" Text="{Binding Names.NameRps.Value}" />
<TextBlock Margin="5" Text="{Binding Names.NameRorps.Value}" />
<StackPanel Orientation="Horizontal">
<Button
Margin="10"
Command="{Binding SerializeCommand}"
Content="Serialize" />
<Button
Margin="10"
Command="{Binding DesirializeCommand}"
Content="Desirialize" />
</StackPanel>
<TextBlock Text="MessagePack Serialized" />
<TextBox
Margin="5,0,5,10"
IsReadOnly="True"
Text="{Binding MessagePackSerializedNames.Value}"
TextWrapping="Wrap" />
<TextBlock Text="Json Serialized" />
<TextBox
Margin="5,0,5,10"
Text="{Binding JsonSerializedNames.Value}"
TextWrapping="Wrap" />
</StackPanel>
</Window>
環境
VisualStudio2019
.NET Core 3.1
C#8
ReactiveProperty 6.2.0
MessagePack 2.1.90
MessagePack.ReactiveProperty 2.1.90
System.Reactive.Compatibility 4.4.1
謝辞
困っていたところを@okazuki さんに助けていただきました。
https://twitter.com/okazuki/status/1246384566756986881
ありがとうございます。