やる気駆動型エンジニアの備忘録

WPF(XAML+C#)の話題を中心に.NET/Android/CI やたまに趣味に関するブログです

シリアライザ~BinaryFormatter~

前回リアライザについての説明を軽くしました。
今回はその中の一つ、BinaryFormatterについて説明していこうと思います。

特徴

MSDNのページはこちら↓↓↓
BinaryFormatter クラス (System.Runtime.Serialization.Formatters.Binary)
BinaryFormatterはその名の通り、オブジェクトをバイナリに書式化します。
さらに、たいていの型に対応しているため、とりあえずシリアライズするならこれを使用する場合が多いでしょう。
パフォーマンス的にも高速です。が、あるケースによっては速度が激遅になるんだとか…。
詳しくは以下のページをご覧いただけるとわかるはずです。
neue cc - .NET(C#)におけるシリアライザのパフォーマンス比較

使い方

    public static T DeepCopy<T>(this T self)
    {
        T result;

        using (var ms = new MemoryStream())
        {
            var formatter = new BinaryFormatter();
            formatter.Serialize(ms, self);
            ms.Position = 0;
            result = (T)formatter.Deserialize(ms);
        }

        return result;
    }

これは拡張メソッドで何でもコピー可能にしていますが、シリアライズしたいオブジェクトとStreamでシリアライズし、その結果をデシリアライズします。
そうすることでオブジェクトのコピーが可能になっています。

さらに、シリアライズするクラスにはSerializableAttributeを定義する必要があります。もちろん基底クラスにもです。

シリアライズ元となるクラスは以下のようにしました。
TimeTracerは処理速度を出力するための機能です。
<ViewModelBase.cs>

    /// <summary>
    /// すべてのViewModelの基底クラスです。
    /// </summary>
    [Serializable]
    public abstract class ViewModelBase : BindableBase
    {
        /// <summary>
        /// オブジェクトの状態を保存するためのスナップショットデータです。
        /// </summary>
        private ViewModelBase _snapshot;

        /// <summary>
        /// スナップショットデータを保存します。
        /// </summary>
        public void SaveSnapshotData()
        {
            using (new TimeTracer("オブジェクトのコピー"))
            {
                _snapshot = this.DeepCopy();
            }
        }

        /// <summary>
        /// オブジェクトが更新されたかどうかを判定します。
        /// </summary>
        /// <returns>
        /// オブジェクトが更新されている場合は、true を
        /// 更新されていない場合は、false を返します。
        /// </returns>
        public bool IsUpdate()
        {
            using (new TimeTracer("更新チェック"))
            {
                if (_snapshot == null)
                {
                    return true;
                }

                return !this.DeepCopy().Equals(_snapshot);
            }
        }

        /// <summary>
        /// オブジェクトの状態を元に戻します。
        /// </summary>
        public void Reset()
        {
            using (new TimeTracer("オブジェクトにリセット"))
            {
                if (_snapshot != null)
                {
                    UpdateObject(_snapshot);
                }
            }
        }

        /// <summary>
        /// オブジェクトを更新します。
        /// </summary>
        /// <param name="obj">オブジェクトデータ</param>
        protected virtual void UpdateObject(ViewModelBase obj)
        {

        }
    }

<BinaryFormatterViewModel.cs>

    /// <summary>
    /// BinaryForamtterView用のViewModelクラスです。
    /// </summary>
    [Serializable]
    public class BinaryFormatterViewModel : ViewModelBase
    {
        private int _age;

        private string _name;

        private DateTime _birthday;

        private BloodType _bloodType;

        private Gender _gender;

        public BinaryFormatterViewModel()
        {
            Age = 20;
            Name = string.Empty;
            Birthday = new DateTime(1990, 1, 1);
            BloodType = SelializeSample.BloodType.TypeA;
            Gender = SelializeSample.Gender.Male;
        }

        /// <summary>年齢</summary>
        public int Age
        {
            get { return _age; }
            set { SetProperty<int>(ref _age, value); }
        }

        /// <summary>名前</summary>
        public string Name
        {
            get { return _name; }
            set { SetProperty<string>(ref _name, value); }
        }

        /// <summary>誕生日</summary>
        public DateTime Birthday
        {
            get { return _birthday; }
            set { SetProperty<DateTime>(ref _birthday, value); }
        }

        /// <summary>血液型</summary>
        public BloodType BloodType
        {
            get { return _bloodType; }
            set { SetProperty<BloodType>(ref _bloodType, value); }
        }

        /// <summary>性別</summary>
        public Gender Gender
        {
            get { return _gender; }
            set { SetProperty<Gender>(ref _gender, value); }
        }

        /// <summary>
        /// オブジェクトを更新します。
        /// </summary>
        /// <param name="obj">オブジェクトデータ</param>
        protected override void UpdateObject(ViewModelBase obj)
        {
            base.UpdateObject(obj);

            var vm = obj as BinaryFormatterViewModel;
            if (vm != null)
            {
                Age       = vm.Age;
                Name      = vm.Name;
                Birthday  = vm.Birthday;
                BloodType = vm.BloodType;
                Gender    = vm.Gender;
            }
        }

        /// <summary>
        /// オブジェクト同士を比較します。
        /// </summary>
        /// <param name="obj">比較対象のオブジェクト</param>
        /// <returns>true:一致, false:不一致</returns>
        public override bool Equals(object obj)
        {
            var other = obj as BinaryFormatterViewModel;
            if (other == null)
            {
                return false;
            }
            return Age == other.Age
                 & Name.Equals(other.Name)
                 & Birthday == other.Birthday
                 & BloodType == other.BloodType
                 & Gender == other.Gender;
        }
    }

画面で使用する場合はこのようにしました。
<BinaryFormatterView.xaml.cs>

    /// <summary>
    /// BinaryFormatterView.xaml の相互作用ロジック
    /// </summary>
    public partial class BinaryFormatterView : Window
    {
        /// <summary>
        /// 当画面のViewModel
        /// </summary>
        private BinaryFormatterViewModel _vm;

        public BinaryFormatterView()
        {
            InitializeComponent();
        }

        /// <summary>
        /// リセットボタンをクリックした際に呼ばれるイベントハンドラです。
        /// </summary>
        /// <param name="sender">イベント発行元オブジェクト</param>
        /// <param name="e">イベント引数オブジェクト</param>
        private void ResetButton_Click(object sender, RoutedEventArgs e)
        {
            // リセット
            _vm.Reset();
        }

        /// <summary>
        /// 戻るボタンをクリックした際に呼ばれるイベントハンドラです。
        /// </summary>
        /// <param name="sender">イベント発行元オブジェクト</param>
        /// <param name="e">イベント引数オブジェクト</param>
        private void BackButton_Click(object sender, RoutedEventArgs e)
        {
            if (_vm.IsUpdate())
            {
                var result = MessageBox.Show("データが更新されています。\r\n入力内容を破棄しますか?"
                                            , "確認"
                                            , MessageBoxButton.YesNo
                                            , MessageBoxImage.Question
                                            , MessageBoxResult.No);
                if (result != MessageBoxResult.Yes)
                {
                    return;
                }
            }
            Close();
        }

        /// <summary>
        /// 当ウィンドウがロードされた際に呼ばれるイベントハンドラです。
        /// </summary>
        /// <param name="sender">イベント発行元オブジェクト</param>
        /// <param name="e">イベント引数オブジェクト</param>
        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            _vm = DataContext as BinaryFormatterViewModel;
            if (_vm != null)
            {
                // これ以降に変更されたら変更ダイアログを表示します。
                _vm.SaveSnapshotData();
            }
        }
    }

画面イメージはこんな感じです。
f:id:iyemon018:20151223181836p:plain

これに値を入力し、
f:id:iyemon018:20151223181912p:plain

戻るボタンをクリックすると、ダイアログが表示されます。
f:id:iyemon018:20151223181940p:plain


さて、シリアライズの話と言いつつ話の本筋はオブジェクトのコピーになっていますがそれはさておき。
このように属性を付加するだけでオブジェクトのコピーが可能になるのがBinaryFormatterの強みです。

今回使用したサンプルはこちら↓↓↓
github.com