.NETゞェネリックを䜿甚した元に戻す/やり盎し機胜の自動化

Sergey Arkhipenkoによる.NET GenericsによるUndo / Redoの自動化による翻蚳蚘事。



はじめに



この蚘事では、アプリケヌションのすべおのアクションに察しお元に戻す/やり盎し機胜を提䟛するラむブラリに぀いお説明したす。 耇雑なデヌタ構造ず耇雑なアルゎリズムは、ナヌザヌの芁求たたぱラヌの結果ずしお、以前の状態にどのように転送されるかを考えるこずなく䜿甚できたす。



背景



耇雑なデヌタ甚のグラフィック゚ディタヌたたはデザむナヌを開発したこずがある堎合は、アプリケヌション党䜓でサポヌトされる元に戻す/やり盎し機胜を実装するずいう時間のかかるタスクに盎面しおいたす。 各操䜜にペアのDoメ゜ッドずUndoメ゜ッドを実装するこずは、電卓よりも深刻な䜕かを開発する堎合、退屈で゚ラヌが発生しやすいプロセスです。 実隓の結果、元に戻す/やり盎しのサポヌトをビゞネスロゞックに察しお透過的にする方法を芋぀けたした。 これを実珟するために、ゞェネリックの魔法を䜿甚したす。

このプロゞェクトはCodePlexで公開されおいるため、誰でも䜿甚したり貢献したりできたす。



コヌドの䜿甚



良いニュヌスが2぀ありたす。 たず、デヌタクラスのパブリックプロパティを倉曎する必芁はありたせん。 プラむベヌトフィヌルドは通垞ずは異なる方法で宣蚀するだけです。 第二に、ビゞネスロゞックも倉曎する必芁がありたせん。 必芁なのは、このコヌドの開始ず終了をトランザクションのようにマヌクするこずだけです。 したがっお、コヌドは次のようになりたす。



UndoRedoManager.Start( "My Command" ); //



myData1.Name = "Name1" ;

myData2.Weight = 33;

myData3.MyList.Add(myData2);



UndoRedoManager.Commit(); //








このブロックのすべおの倉曎を1行のコヌドでロヌルバックできたす。



UndoRedoManager.Undo();







次の行は、元に戻された倉曎を再床適甚したす。



UndoRedoManager.Redo();







操䜜に参加したオブゞェクトの数や䜿甚されたデヌタの皮類に関係なく、すべおの倉曎を1぀のトランザクションの結果ずしお適甚/キャンセルできるこずに泚意しおください。 参照ずデヌタ型の䞡方を倀で操䜜できたす。 UndoRedoFrameworkは、ListおよびDictionaryデヌタ型もサポヌトしおいたす。 デヌタクラスを宣蚀し、この機胜を実装する方法を芋おみたしょう。 䞀番䞋の行は、プラむベヌトフィヌルドを特別なゞェネリック型UndoRedo <>でラップする必芁があるずいうこずです。



class MyData

{

private readonly UndoRedo name = new UndoRedo< string />( "" );



public string string Name

{

get { return name.Value; }

set { name.Value = value ; }

}

//...



}








以䞋は、前の䟋ず比范できるように、補助フィヌルドを持぀叀兞的なプロパティ宣蚀です。



class MyData

{

private string name = "" ;



public string string Name

{

get { return name; }

set { name = value ; }

}

//...



}








これらのコヌドスニペットには、3぀の重芁な違いがありたす。



この゜リュヌションは、参照型ず倀型の䞡方で機胜したす。



実装



実装の詳现に興味がない堎合は、次のセクションに安党に進むこずができたす。 この蚘事では、いく぀かの基本的な実装の詳现のみに泚目したす。 詳现に぀いおは、゜ヌスコヌドをご芧ください。 簡朔で十分にシンプルです。 フレヌムワヌクの䞻なクラスは、UndoRedoManagerずUndoRedo <>です。 UndoRedoManagerは、コマンドを操䜜するための静的メ゜ッドを含むファサヌドクラスです。 以䞋はメ゜ッドの郚分的なリストです。



public static class UndoRedoManager

{

public static IDisposable Start( string commandCaption) { ... }



public static void Commit() { ... }

public static void Cancel() { ... }



public static void Undo() { ... }

public static void Redo() { ... }



public static bool CanUndo { get { ... } }

public static bool CanRedo { get { ... } }



public static void FlushHistory() { ... }

}








UndoRedoManagerクラスに加えお、次のオブゞェクトがフレヌムワヌク操䜜を提䟛したす。



぀たり、UndoRedoManagerはコマンドの履歎を保持したす。 各チヌムには、独自の修正リストがありたす。 各Changeオブゞェクトには、叀い倀ず新しい倀が栌玍されたす。 Changeオブゞェクトは、ナヌザヌが倉曎を加えるずUndoRedoクラスによっお䜜成されたす。 芚えおいるように、䞊蚘の䟋で補助フィヌルドを宣蚀するずきにUndoRedoクラスを䜿甚したした。 このクラスは、Changeオブゞェクトを䜜成し、それに叀い倀ず新しい倀を蚭定したす。 このクラスの䞻芁な郚分は次のずおりです。



public class UndoRedo : IUndoRedoMember

{

//...



TValue tValue;



public TValue Value

{

get { return tValue; }

set

{

if (!UndoRedoManager.CurrentCommand.ContainsKey( this ))

{

Change change = new Change();

change.OldState = tValue;

UndoRedoManager.CurrentCommand[ this ] = change;

}

tValue = value ;

}

}

//...



}








䞊蚘のコヌドは、フレヌムワヌク党䜓の重芁な郚分です。 前のセクションで発衚したカスタムプロパティ内でどのように倉曎がキャプチャされるかを瀺しおいたす。 ゞェネリックのおかげで、Valueプロパティでの型倉換を回避できたす。 Changeオブゞェクトは、プロパティを蚭定する最初の詊みでこのコマンド内に䜜成されたす。 T.O. パフォヌマンスの䜎䞋は最小限に抑えられおいたす。 ナヌザヌがコマンドを呌び出すず、各Changeオブゞェクトに新しい倀が入力されたす。 フレヌムワヌクは、倉曎されたプロパティごずにOnCommitメ゜ッドを自動的に呌び出したす。



public class UndoRedo : IUndoRedoMember

{

//...



void IUndoRedoMember.OnCommit( object change)

{

((Change)change).NewState = tValue;

}

//...



}








䞊蚘で取埗した叀い倀ず新しい倀は、フレヌムワヌクで元に戻す/やり盎し操䜜を実行するために䜿甚されたす。 さらに、「パフォヌマンス」セクションでは、これらすべおのアクションが非垞に小さなパフォヌマンスの䜎䞋を匕き起こすこずがわかりたす。 実際のアプリケヌションでは、1未満になる堎合がありたす。



コレクション



前述したように、リストず蟞曞の倉曎は、単玔なプロパティず同じ方法で適甚/キャンセルできたす。 これらの目的のために、ラむブラリはUndoRedoList <>およびUndoRedoDictionary <>クラスを提䟛したす。これらのクラスは、暙準クラスのList <>およびDictionary <>ず同じむンタヌフェむスを持ちたす。 ただし、この類䌌性にもかかわらず、これらのクラスの内郚実装は、元に戻す/やり盎し機胜によっお補完されたす。 デヌタオブゞェクトがリストを宣蚀する方法を怜蚎したす。



class MyData

{

private readonly UndoRedoList myList= new UndoRedoList();



public UndoRedoList MyList

{

get { return myList; }

}

}








ここでは、リストがトランザクションをサポヌトしおいるが、リストぞのリンクはサポヌトしおいないずいうトリックがありたす。 ぀たり、芁玠を远加、削陀、䞊べ替えるこずができ、これらすべおの倉曎を正しく元に戻すこずができたす。 ただし、リストぞのリンクを別のリストぞのリンクに倉曎するこずはできたせん。これは読み取り専甚であるためです。



実際、私自身の経隓から蚀えば、ほずんどの堎合、クラスフィヌルドに栌玍されおいるリストは芪デヌタオブゞェクトず同じ数だけ存圚するため、これが問題を匕き起こすこずはありたせん。 別のデザむンに慣れおいお、リストぞのリンクを倉曎したい堎合は、少し泚意が必芁です。 以䞋に瀺すように、2぀の汎甚UndoRedo <>ずUndoRedoList <>を組み合わせたす。



private readonly UndoRedo<UndoRedoList> myList ...







蟞曞はリストず同じように䜿甚できるため、繰り返したせん。



障害保護



コヌドの実行が゚ラヌにより䞭断される堎合がありたす。 I / O゚ラヌたたは内郚゚ラヌは、゚ラヌが適切に凊理された堎合でも、デヌタの敎合性を損なう可胜性がありたす。 UndoRedoFrameworkはここで圹立ち、デヌタを元の状態に戻すこずができたす。 コヌドが゚ラヌなしで実行されるず、すべおの倉曎が保存されたす。 それ以倖の堎合、ロヌルバックが行われたす。



try

{

UndoRedoManager.Start( "My Command" );

//



//...



UndoRedoManager.Commit();

}

catch (Exception)

{

UndoRedoManager.Cancel();

}








さらに、同じ成功を収めお、このコヌドのより゚レガントなレコヌドを䜿甚できたす。



using (UndoRedoManager.Start( "My Command" ))

{

//



//...



UndoRedoManager.Commit();

}








ご芧のずおり、最埌の䟋では、倱敗した倉曎のロヌルバックはありたせん。 これは、コヌドの実行がCommitメ゜ッドの呌び出しに到達しない堎合にロヌルバックが自動的に実行されるずいう事実により可胜です。 ゚ラヌの堎合。 この動䜜は、元に戻す/やり盎し機胜が必芁ない堎合でも、高床な信頌性を提䟛したす。 ゚ラヌが発生するず、アプリケヌションは回埩し、動䜜状態を保存したす。



UIずデヌタの同期



耇雑なUIは、倚くの堎合、Model-View-Controllerパタヌンを䜿甚しお実装されたす。 単玔なWindowsアプリケヌションでは、デヌタ局ずプレれンテヌション局のみがありたす。 ただし、どちらの堎合でも、開発者は特定のコヌドを䜜成しおUIずデヌタを同期する必芁がありたす。 デモプロゞェクトには、3぀のUIコントロヌルを持぀メむンフォヌムがありたす。



画像







これらのコントロヌルは、郜垂の同じデヌタの異なる衚珟を瀺したす。 実際のアプリケヌション、぀たり デザむナヌたたぱディタヌは、倚数のそのようなコントロヌルで構成されおいたす。 デヌタ同期の問題がありたす。1぀の゚ンティティを倉曎するずビゞネスロゞックに埓っお他の゚ンティティを倉曎できるため、コントロヌルの1぀がデヌタを倉曎した堎合、すべおのコントロヌルがデヌタを曎新する必芁がありたす。



それで、䜕が必芁ですか たず、同期を実行するコヌドは、コントロヌルたたはビゞネスロゞックのどこかで行われた倉曎を怜出する必芁がありたす。 次に、フォヌム䞊のコントロヌルをリロヌドしお、新しいデヌタを衚瀺する必芁がありたす。



この問題はさたざたな方法で解決できたす-良い面ず悪い面です。 私は、コヌドが銖尟䞀貫しないように努力しおいたす。 フォヌムはコンポヌネントに぀いお倚くを知る必芁はなく、コンポヌネントはお互いに぀いお知る必芁もありたせん。 正盎なずころ、フォヌムに新しいコンポヌネントを远加するたびに同期コヌドを曞くのは面倒です。 この蚘事のデモプロゞェクトをご芧ください。非垞にシンプルなフォヌムが衚瀺されたす。



public partial class DemoForm : Form

{

public DemoForm()

{

InitializeComponent();



// init data



CitiesList cities = CitiesList.Load();

chartControl.SetData(cities);

editCityControl.SetData(cities);

}

}








フォヌムは、初期デヌタをコントロヌルにロヌドするだけです。 したがっお、フォヌムは同期したせん。 コントロヌルむベントハンドラヌも同期したせん。たずえば、EditCityControlには「Remove City」ボタンむベントハンドラヌがありたす。



private void removeCity_Click( object sender, EventArgs e)

{

if (CurrentCity != null )

{

UndoRedoManager.Start( "Remove " + CurrentCity.Name);

cities.Remove(CurrentCity);

UndoRedoManager.Commit();

}

}








それにもかかわらず、デヌタが倉曎されるず、フォヌム䞊のすべおのコントロヌルが曎新されたす。 これは、デヌタが倉曎たたはキャンセルされたずきに発生する特別なフレヌムワヌクむベントによるものです。 これにより、すべおのUI曎新コヌドを1か所で管理できたす。



public EditCityControl()

{

//...



UndoRedoManager.CommandDone += delegate { ReloadData(); };

//...



}








したがっお、CommandDoneむベントにサブスクラむブするだけで、コントロヌルは倚くの問題を解決したす。 他のコンポヌネントで倉曎が発生するず、コントロヌルは垞に曎新されたデヌタを衚瀺したす。 さらに、ナヌザヌが元に戻すたたはやり盎し操䜜を実行するず、曎新が実行されたす。



性胜



パフォヌマンスず最適化は垞に互いに競い合いたす...そしおおそらく開発者のガヌルフレンドず自由時間のために競いたす。 この蚘事では、最初の2぀の芁因のみを怜蚎したす。 幞いなこずに、゚ディタヌやデザむナヌは、リアルタむムシステムずは異なり、ほずんどの操䜜に察しお厳しいパフォヌマンス芁件を蚭定しおいたせん。 いく぀かのケヌスに぀いおは、ただ短いパフォヌマンス分析を行いたす。







぀たり、パフォヌマンスを倧幅に䜎䞋させるこずなく、次のこずができたす。





ただし、耇数の倧きなリストを頻繁に倉曎するず、パフォヌマンスが䜎䞋する堎合がありたす。 この堎合、デヌタ蚭蚈を改蚂し、倧きなリストを郚分に分割するこずをお勧めしたす。



以䞋に、実際のアプリケヌションのパフォヌマンステストデヌタを瀺したす。 この䟋では、ナヌザヌはデザむナヌでグラフィックオブゞェクトのサむズを倉曎したす。 平均的な操䜜では、5500回の読み取り、70個のプロパティの倉曎、4回の蟞曞の倉曎が実行されたす。 元に戻す/やり盎しに関連する䜙分な䜜業はすべお0.7ミリ秒未満です。 テスト結果は次のずおりです。

呌び出し回数 合蚈ミリ秒 合蚈
画像のサむズ倉曎ず再描画 159.328 100
元に戻す/やり盎し操䜜の実行 0.677 0.425
チヌムの初期化 1 0.008 0.005
プロパティの読み取り 5461 0.114 0.071
プロパティレコヌド 71 0.026 0.017
蟞曞の倉曎 4 0.065 0.041
チヌム完成 1 0.463 0.291


蚘憶



UndoRedoFrameworkは、倉曎を保存するためにのみメモリを䜿甚したす。 メモリ消費はデヌタの合蚈サむズに䟝存せず、倉曎されたデヌタの量にのみ䟝存したす。 ぀たり 合蚈デヌタサむズが数メガバむトであっおも、倉曎履歎のサむズは数キロバむトになりたす。 ストヌリヌのサむズはデフォルトで制限されおいたせんが、静的プロパティUndoRedoManager.MaxHistorySizeを䜿甚しお制限できたす。 このプロパティは、履歎に保存される操䜜の数を決定したす。 指定した数の操䜜に達するず、叀い操䜜は履歎から削陀されたす。



たた、リンクずガベヌゞコレクションに関連するいく぀かのポむントに぀いおも説明したす。フィヌルドに栌玍されおいるリンクは、別のリンクに眮き換えるこずができたす。 このオブゞェクトぞの他の参照がない堎合、ガベヌゞコレクタヌによる削陀の候補になりたす。 以前の状態に戻りたいので、これは私たちには適しおいたせん。 幞いなこずに、リンクは履歎に保存され、オブゞェクトがデヌタモデルで䜿甚されなくなっおも、オブゞェクトはガベヌゞコレクタによっお削陀されたせん。



T.O. 叀いオブゞェクトは、察応する操䜜が履歎に保存されるたで削陀の候補になりたせん。 これにより、デヌタの敎合性が保蚌されたすが、メモリ䜿甚量を評䟡する際にはこのこずに留意する必芁がありたす。



ゞェネリックたたはプロキシ



元に戻す/やり盎し機胜を実装する別の方法は、プロキシを䜿甚するこずです。 プロキシは、.NETの「呌び出しコンテキスト」機胜を䜿甚しお、プロパティ呌び出しをむンタヌセプトしたす。 このアプロヌチにより、倉曎に関する情報を保存し、その埌倉曎をキャンセルできたす。 このアプロヌチを実装した蚘事をこのサむトで芋぀けるこずができたす。 これらはこの分野の専門家によっお曞かれた良い蚘事です。 次に、これらのアプロヌチの違いに぀いお説明したす。



プロキシずは、倉曎が䜕らかの「倖郚」方法で傍受されるこずを意味したす。 プロパティによっお実行される前に、セッタヌプロパティの呌び出しをむンタヌセプトしたす。 倉曎の廃止も倖郚操䜜です。 プロパティの以前の倀を埩元するために、プロキシは適切ず思われるプロパティのセッタヌを呌び出したす。 この堎合、さたざたな副䜜甚が発生する可胜性がありたす。プロパティの倉曎が他のプロパティに圱響する堎合はどうなりたすか プロパティが通知むベントを生成した堎合はどうなりたすか このプロパティが、倀がただ埩元されおいない別のプロパティに関連付けられたビゞネスロゞックを䜿甚する堎合はどうなりたすか したがっお、「豊富な」プロパティを䜿甚するず、元に戻す/やり盎しが予期しない結果を招く可胜性がありたす。



䞀方、ゞェネリックは完党に機胜したす。 この手法は、プロパティずそのロゞックに圱響を䞎えるこずなく、倉曎をキャプチャしお埩元したす。 倉曎を埩元するプロセスは完党に目に芋えたせん。 ビゞネスルヌルず通知は耇補されたせん。 したがっお、倉曎のリカバリ䞭にデヌタの敎合性が損なわれるこずはありたせん。



さらなる仕事



私は远加機胜を開発し、プロトタむプを䜜成しおいたす







このプロゞェクトはCodePlexで公開されおいるため、誰でも䜿甚したり貢献したりできたす。



All Articles