search
LoginSignup
6

More than 1 year has passed since last update.

posted at

updated at

ReactivePropertyをMessagePackとJSONにシリアライズ/デシリアライズする


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で導入すると解決します。

デモアプリ

クラスをまるごとシリアライズ/デシリアライズできるデモアプリを作成してみました。

動作画面

ReactiveProperty Serialize Demo 2020-04-05 17-07-32.gif

姓と名の入力欄があり、変更するとフルネームが代わります。

[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は以下です。

MainWindow.xaml
<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
ありがとうございます。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
6