LoginSignup
9

More than 5 years have passed since last update.

ReactivePropertyを利用してUndo可能なアプリケーションを作成する

Last updated at Posted at 2017-05-17

初めに

ReactiveProperty(とRx)を利用して、Undo可能なアプリケーションを作成してみました。

ソースコードは↓こちら
https://github.com/dyoneda/UndoProject

Redo/Undo

Redo/Undoを実現するために、Redo処理/Undo処理を保持するActionをPropertyとするクラスを用意します。

UndoCommand.cs
public class UndoCommand
{
    public UndoCommand(Action undo, Action redo)
    {
        this.UndoAction = undo;
        this.RedoAction = redo;
    }

    public Action UndoAction { get; }
    public Action RedoAction { get; }
}

使い方はこんな感じ

//// こんなクラスがあるとして
//class Hoge { public string Fuga { get; set; }}

// どこかから対象オブジェクトを取得
var hoge = GetHoge();
// 変更前の値を保持
var oldValue = hoge.Fuga;
// これから変更する値。
var newValue = "new";

// Redo/Undo用のオブジェクトを生成
var command = new UndoCommand(() => hoge.Fuga = oldValue, () => hoge.Fuga = new Vlaue);

// 適用
command.Redo();
// 元に戻す
command.Undo();
// 再適用
command.Redo();

でも、これだけは処理が煩雑になるため、UndoCommandクラスを管理するためのクラスも用意します。Redo/UndoはFILOであるためStackクラスを利用するといい感じです。

UndoCommand.cs
public class UndoStack
{
    private readonly Stack<UndoCommand> _undoStack = new Stack<UndoCommand>();
    private readonly Stack<UndoCommand> _redoStack = new Stack<UndoCommand>();

    public static UndoStack Instance { get; } = new UndoStack();

    public void Push(UndoCommand command)
    {
        this._redoStack.Clear();
        this._undoStack.Push(command);
        command.RedoAction();
    }

    public bool CanUndo() => this._undoStack.Any();
    public bool CanRedo() => this._redoStack.Any();

    public void Undo()
    {
        if (!this.CanUndo()) return;
        var command = this._undoStack.Pop();
        command.UndoAction();
        this._redoStack.Push(command);
    }

    public void Redo()
    {
        if (!this.CanRedo()) return;
        var command = this._redoStack.Pop();
        command.RedoAction();
        this._undoStack.Push(command);
    }
}

ReactivePropertyによるVM/Mの同期

VM/M間の同期の間に先ほどのRedo/Undoの仕組みを組み込んでしまえばほぼほぼできたようなものです。
ReactivePropertyでVM/Mの同期といえば、ToReactivePropertyAsSynchronizedメソッドなのですが、ここでは挙動を柔軟にカスタマイズするためにToReactiveProperty/Subscribeの組み合わせで対応したいと思います。
(参考)https://code.msdn.microsoft.com/windowsdesktop/ReactiveProperty-085c3090

ViewModel.cs
private ReactiveProperty<TProp> CreateReactiveProperty<TObj, TProp>(TObj obj, Expression<Func<TObj, TProp>> exp) 
    where TObj : INotifyPropertyChanged
{
    var latest = default(TProp);
    var rp = obj.ObserveProperty(exp).Do(v => latest = v).ToReactiveProperty().AddTo(this._disposable);
    rp.Where(v => !object.Equals(v, latest)).Subscribe(v =>
    {
        UndoStack.Instance.Push(UndoCommand.CreateCommand(obj, exp, v));
        this._subjects.OnNext(true);
    }).AddTo(this._disposable);
    return rp;
}

上記のような便利メソッドをひとつ定義しておけば、下記のように使いまわせます。

ViewModel.cs
this.StringProperty = CreateReactiveProperty(m, o => o.StringValue);
this.IntProperty = CreateReactiveProperty(m, o => o.IntValue);
this.BoolProperty = CreateReactiveProperty(m, o => o.BoolValue);

あとは、別件ですがToReactivePropertySubscribeは、神経質にDisposeするぐらいでちょうどいいと思います。
Rxが提供するCompositeDisposableが便利なのでぜひ利用しましょう。

最後に

今回はReactiveProeprtyのみNuGetから取得した形&Qiita用やっつけサンプルなのでべた書きな部分が多いですが、その他MVVM系ライブラリを利用するとスマートに書ける部分が多いので、検討してみてくだい。
例えば、
・INotifyPropertyChanged
・Propertyの動的取得
・WindowClose時にVMをDispose
・Undo/RedoボタンのEnableのIObservable化
あたりですね。

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
  3. You can use dark theme
What you can do with signing up
9