#初めに
ReactiveProperty(とRx)を利用して、Undo可能なアプリケーションを作成してみました。
ソースコードは↓こちら
https://github.com/dyoneda/UndoProject
#Redo/Undo
Redo/Undoを実現するために、Redo処理/Undo処理を保持するActionをPropertyとするクラスを用意します。
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クラスを利用するといい感じです。
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
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;
}
上記のような便利メソッドをひとつ定義しておけば、下記のように使いまわせます。
this.StringProperty = CreateReactiveProperty(m, o => o.StringValue);
this.IntProperty = CreateReactiveProperty(m, o => o.IntValue);
this.BoolProperty = CreateReactiveProperty(m, o => o.BoolValue);
あとは、別件ですがToReactivePropertyやSubscribeは、神経質にDisposeするぐらいでちょうどいいと思います。
Rxが提供するCompositeDisposableが便利なのでぜひ利用しましょう。
#最後に
今回はReactiveProeprtyのみNuGetから取得した形&Qiita用やっつけサンプルなのでべた書きな部分が多いですが、その他MVVM系ライブラリを利用するとスマートに書ける部分が多いので、検討してみてくだい。
例えば、
・INotifyPropertyChanged
・Propertyの動的取得
・WindowClose時にVMをDispose
・Undo/RedoボタンのEnableのIObservable化
あたりですね。