February 3, 2006
日本語版最終更新日 2006 年 5 月 1 日
BackPack API に関する前回までのあらすじ
さて、BackPack API の利用についての Xml4Fun の特集記事は、この第 IV 部で最終回です。これまでの記事で、多くのことを網羅しました。最初の記事では、API についての説明に加えて、ユーザー入力を動的に生成される XML に変換して、アプリケーションと BackPack サーバー間でデータを転送する方法を取り扱いました。第 II 部ではもう一歩進んで、オブジェクト モデルを作成して、XML で表現されたデータを利用しました。また、ディスクからも Web からも XML を利用できるようにし、アプリケーションからはその違いがわからないように構成しました。第 III 部では、情報の流れを明確にしました。情報はユーザー インターフェイスから中間層を通じて BackPack サーバーに流れ、再び中間層を通じて UI に戻ることを確認しました。さらに、TreeView コントロールを追加して BackPack データを視覚的に表現し、各 TreeNode の Tag プロパティに ResourceDescriptor オブジェクトを読み込んで、表示されるデータの種類を迅速かつ簡単に検索できるようにしました (サンプル プログラム ファイル内では実際のコメント行は英語で書かれていますが、この記事内では説明目的で日本語で書かれています)。
削除
前の記事では、オブジェクト モデルの基本に取り組みましたが、重要なコンポーネントを除外しました。それは、ページ、メモ、タスクなどを削除するための機能です。オブジェクトを削除する機能をコーディングするのは極めて簡単であり、既存のインフラストラクチャに少しロジックを加えるだけで済みます。コンテキスト メニューに新しい項目を追加すると、ユーザーが右クリックしてノードを削除できるようになります。
.gif)
エンド ユーザーが削除オプションを選択すると、ノードの Tag プロパティが評価されて、削除フラグの設定されるノードの種類 (ページ、タスク、メモなど) が決定します。削除されるノードが決定すると、PageManager または当該 Page によって (削除の対象が Page の子オブジェクトである場合)、削除要求を表すコマンドがアセンブルされます。次に、コマンドは BackPack サーバーに対する処理のためにローカル BackPackGateway インスタンスに送られます。サーバーに対する処理が完了すると、PageManager によって、この操作を表している当該 PendingOperation インスタンスが削除され、必要に応じてローカル データが変更され (この場合は削除)、操作の完了が UI に通知されます。通知を受けると、この操作に該当するノードが TreeView から削除されます。
ただし、問題点が 1 つあります。サーバーに対する処理のためのコマンドをアセンブルするとき、Page はコマンドを交換するために BackPackGateway オブジェクトと直接に通信を行いません。代わりに、イベントを発生させて、いずれかの Page が変更内容をサーバーにマーシャリングする必要があることを PageManager に知らせます。PageManager は、即時に変更をプッシュするか (オンラインの場合)、保存してから後で (再び接続されたとき) 処理するかどうかを決定します。問題は、Page がイベントを通じて変更内容を通知するため、最初に PageManager のイベント ハンドラをアンバインドしなければ、指定された Page を削除できないことです。イベント ハンドラの削除は、極めて簡単な操作です。コードは次のようになります。
Page removed = e.PendingOperation.State as Page;
if (removed != null)
{
removed.OperationAssembled -= new OperationAssembledEventHandler(this.Page_OperationAssembled);
this._pages.Remove(removed);
removed = null;
}
イベント ハンドラの削除が完了したら、中間層からのページの削除が可能になり、PageManager はページが削除されたことを UI に通知できます。これによって、UI 層では、当該 Node (および対応するすべてのルックアップ) の削除が可能になります。BackPack データを削除するために必要なコードの一部始終を詳しく検討すると、非常に多くのアクティビティが関連していることがわかります。その一部は、ハウスキーピング機能を 2 つの層 (UI 層とビジネス層) に分割することに関連したコードであり、他の一部は、すべての処理がバックグラウンドで発生している間、アプリケーションの Winform の応答性を確保するためにデザインされたスレッド コードに関連がありますが、コードの大部分は、コマンド自体の処理、およびネットワーク接続に基づいたコマンドの処理方法に関連しています。
ネットワーク アウェアネス (自動認識!!!)
現在のネットワーク接続を確認し、変更内容を検出する機能は、オフラインでの作業時にオンライン アプリケーションの操作を可能にすることを目標としたアプリケーションにとって極めて重要な機能です。それには、ネットワーク接続を確認するための優れた方法が必要です。幸いにも、2.0 バージョンの .NET Framework には、これをすべて可能にする新機能が豊富に用意されています。新しい System.Net.NetworkInformation 名前空間には、非常に便利なクラスやユーティリティが多数提供されており、現在のネットワーク情報を簡単に確認できるほか、適切なイベント ハンドラがセットアップされている場合は、その状況での変更内容が通知されます。ただし、機能は複数のクラスに散在するため、このロジックのコア セクションを単一のクラスの中にカプセル化すると、アプリケーションでの機能の操作がいっそう簡単になります。概念は大したものではなく、また時間の都合もあるため、実装の詳細は省略させていただきます (付属のコード NetworkStatus.cs/vb で十分にご理解いただけるかと思います。詳細が必要な場合は、少し前に作成した実装に関するブログを参照してください)。抜粋に成功すると、NetworkStatus クラスは次のようになります。
.gif)
PageManager に NetworkStatus クラスのインスタンスを追加すると、現在のネットワーク接続状況を確認でき、必要に応じて要求された変更内容を処理できます。変更内容は、BackPackGateway オブジェクトを通じてサーバーに直接送られるか、PageManager の内部コレクションである Pending Commands に追加されて、接続が再確立された直後に実行されます。接続が利用可能になる前にアプリケーションを終了した場合、保留中のコマンドはディスクに保存されるため、失われることはありません。この機能を使用すれば、BackPack データをオフラインにすることが可能になり、オフラインでの作業時にも変更を加えられます。
bool connected = this._networkStatus.ConnectivityStatus == ConnectionStatus.Connected;
bool gatewayConfigured = (this._gateway != null && this._gateway.ConnectionInfo != null);
if(connected && gatewayConfigured)
{
AsyncRemoteOperation async = new AsyncRemoteOperation(this._gateway.ExecuteWebMethod);
async.BeginInvoke(url, args, operation, null, null);
}
else
{
this._pendingCommands.Add(new Command(url,args,operation));
}
当然ながら、UI は事実上、このすべてとは無関係に、オフライン時にほぼすべての機能を提供できるように設計されています。オフライン時の新規作成オブジェクトの編集を禁止したのは、オフライン時に行う一時 ID の同期化を最小限に保つためにすぎません。
コマンドを保存する
オブジェクト モデルを持つアプリケーションの開発で特にすばらしい点は、ゴールに近づくほど、既存のコードや機能が利用可能になるため、作業にかかる時間がしだいに短縮されていくことです。さまざまなオブジェクトのコレクションを保存するための作業がすべて完了しているため、コマンドの保存は極めて簡単です。ユーザーが Winform を終了すると、現在のセッション中に保留中の操作が追加されたかどうかのチェックが行われます。追加されている場合は、変更内容を保存するためのメッセージ ボックスが表示されます。次にコード例を示します。
bool changesPending = this._pageManager.PendingOperations.Count > 0;
// 変更が保留されている可能性があります。しかし、変更が以前のセッションから来ている場合もあり、
// その場合保存の必要はありません。
bool madeThisSession = this._pageManager.ChangeCount > 0;
if (changesPending & madeThisSession)
{
string message = "Changes made are still pending against the " +
"server.Do you wish to save changes from this Session?" +
System.Environment.NewLine + "(Changes made in previous " +
"Sessions will still be persisted if you don't save now.)";
DialogResult res = MessageBox.Show(
message, "Save Changes?", MessageBoxButtons.YesNoCancel);
switch (res)
{
case DialogResult.Cancel:
e.Cancel = true;
return;
break;
case DialogResult.No:
this._pageManager.DeletePendingChanges();
this._pageManager.DeleteStateFiles(true);
break;
case DialogResult.Yes:
this._pageManager.PersistPages();
this._pageManager.PersistCommands();
break;
default:
e.Cancel = true;
return;
break;
}
}
PersistPages() と PersistCommands() の呼び出しは、記事 #2 で作成したヘルパ メソッド SerializeToFile() の呼び出しをラップするだけです。このメソッドは、ジェネリックのコレクションをシリアル化してディスクに書き込みます。
Winform が読み込まれるたびに、PageManager は、保存されたコマンドと保留中の操作を探し出し、存在する場合は必要に応じてフラグを設定します。エンド ユーザーがログインし、サーバーから Page を読み込んだ場合は、すべての保留中の操作が破棄されます (以前にオフラインで行った変更内容を読み込むのではなく、サーバーから新しいデータのコピーを取得することが前提となっています)。エンド ユーザーがディスクから Page を読み込んだ場合は、そのデータに対するすべての保留中の操作が読み込まれます。ユーザーの資格情報が入力されている場合は、すべての保留中のコマンドが (存在する場合) サーバーに対して即時実行されます (同様に、オフライン モードで実行されていたアプリケーションが稼働している間にネットワーク接続が再び利用可能になった場合は、"メモリ内で待機している" 保留中のコマンドもサーバーに対して実行されます)。
当然ながら、メニュー オプション、接続状況、および保留中の操作順列のすべての対話を管理し、さまざまな順列を制御するのは、少し乱雑な方法でした。さまざまなビジネス ルールを実装して、実行する処理を "保留状況" および接続状況に基づいて決定するのは、極めて論理的であると考えていますが、筆者の選択した実装方法が、ユーザーのロジックと一致しない可能性は十分にあります (筆者は少しも後悔していません!)。悪い知らせは以上です。良い知らせは、どのような場合に保留中のコマンドをサーバーに送信するかの決定は、いくらか注意が必要ですが、送信自体は極めて簡単だということです。コマンドは、単一のメソッドに送信される保存データにすぎません。したがって、コマンドを再びアクティブにするには、該当メソッドにコマンドを戻すだけで済みます。たとえば、ネットワーク接続が再確立されてコマンドを "再アクティブ化する" ときがきたことを検出するイベント ハンドラを作成し、そのルーチンの中で、シリアル化してディスクに格納されたコマンドを取り出し、既にメモリに存在する保留中のコマンドのリストに追加して、すべてを処理のために送信するだけです。次にコード例を示します。
public void NetworkStatusChanged(object sender, NetworkStatusChangedEventArgs e)
{
bool online = e.ConnectionState == ConnectionStatus.Connected;
bool connected = this._gateway != null && this._gateway.Loaded;
if(online && connected)
{
if(this._savedCommands)
{
SerializableList<Command> savedCommands = (SerializableList<Command>)this.LoadSerializableList(System.Environment.CurrentDirectory + CommandsFileName, typeof(SerializableList<Command>));
if (savedCommands != null && savedCommands.Count > 0)
{
foreach (Command c in savedCommands)
{
if (!this._pendingCommands.Contains(c))
this._pendingCommands.Add(c);
}
}
}
if (this._pendingCommands.Count > 0)
{
Command[] queued = new Command[this._pendingCommands.Count];
this._pendingCommands.CopyTo(queued);
this.ProcessPendingCommands(queued);
}
}
}
コマンド自体の処理については、些細な障害が 1 つあります。それは、接続が存在しなくなった場合はどうするかという問題です。あるいは、そう考えるのが当然でしょう。接続が存在しない場合は、コマンドをゲートウェイに渡す前に PageManager がそれを検出し、コマンドを保存するだけです。コマンドの実際の処理は、ヘルパ関数が取り扱います。ヘルパ関数を使用すると、既に実行中であるアプリケーション インスタンスのネットワーク接続が再開されたときはコマンドの処理を発生させ、既存の接続を使って新しいセッションが開始されたときはコマンドを送信することが可能です。ヘルパ関数は、Command オブジェクトを分割し、BackPackGateway オブジェクトにコマンドを送信する役割を果たすメソッドに送るだけです。
private void ProcessPendingCommands(Command[] commands)
{
for (int i = 0; i < commands.Length; i++)
{
Command current = commands[i];
this.InvokeOperation(current.Url, current.Arguments, current.Operation);
}
}
実際のコマンドを処理するコードは非同期であり、イベントやハンドラを通じて結果を報告するため、これ以上は何も必要ありません。サーバーに対するコマンドの処理が成功した場合は、PageManager および Winform のイベント ハンドラが相応に対処します。問題が発生した場合は、同じイベントによって問題が渡され、必要に応じて PageManager および UI によって処理されます。何らかの原因で接続が "チラッと" 戻り、すぐに消失した場合は、現在実行中のコマンドに重大な問題が発生する可能性があります (これはネットワーク アプリケーションのしくみの問題ですが、ロールバック機能が準備されています)。ただし、後続の保留中のコマンドは、Invoke Operation() メソッドに送られます。このメソッドは接続状況をチェックし、接続が利用可能でない場合はコマンドをキューに入れるため、接続が戻るまでコマンドが再度キューに入れられて (必要に応じて保存される) だけです。
閉じた回路で本格的に運転する
すべての準備が整ったので、本格的なテスト運転を実行できます。"サイン イン" プロセス全体および筆者のメニューの使用を中心として、現段階では UI が多少扱いにくい状態にあるため (筆者のアプリケーションのために 37Signals が廃業になるようなことは避けるべきです)、アプリケーション全体のテスト運転を実行し、オンラインおよびオフライン時の動作を観察します。次のようなテストを提案します (これに類似したものであればどのように置き換えても成功します)。
- F5 キーを押してアプリケーションをビルドおよび展開します。
- アカウント名と API キーを入力し、[Log In] ボタンを押します ([Credentials] | [Save] メニュー オプションを使用して資格情報を保存しておくと再入力する必要がありません)。
- [Pages] メニューを使用して、サーバーからページを読み込みます (当然ながら、この手順を実行するにはオンラインである必要があります)。
- オンライン時に、いくつかの変更を加えます。新しいオブジェクトを追加すると、サーバーで変更内容が承認されるまで、TreeNodes が一時的に "ダーティ" になることがわかります (他のノードを変更すると、ほとんどの場合は処理が速すぎて確認できません。ただし、ノードのタイトルや名前の変更は、サーバー上の変更が完了した後にはじめて UI に反映されるため、動作を確認するのにふさわしい方法です)。
- しばらくアプリケーションをオンラインで使用した後、オフラインにする (なんらかの方法でネットワーク接続を無効にする) か、ページを保存し、アプリケーションを終了してからオフラインにします。
- オフラインの間に、読み込まれたページに対して、いくらかの変更を加えます。変更内容が即時に "非ダーティ" としてマークされず、新しく追加したノードを編集できないことを除いて、すべてがオンライン時と同様に動作することがわかります。
- この時点で、アプリケーションを終了し、変更内容を保存するためのメッセージ ボックスで [Yes] を選択して変更を保存するか、ネットワーク接続を再接続します。
- ネットワーク接続の回復だけを行った場合 (アプリケーションが接続を認識するまでに数秒かかります)、サーバーでの更新が完了すると、"ダーティ" ノードが通常のアイコンに置換されることを確認できます。
- アプリケーションを終了して変更内容を保存した場合、アプリケーションを開いたときには、保留中のノードは保留中としてフラグが設定されたままになっています。保留中のノードは、再接続した後に初めてクリーンとしてマークされるか、非保留状態になります。
.gif)
(画像をクリックすると拡大表示されます。)
隠蔽された内容を理解し、実行される処理を把握していると、アプリケーションのテスト運転が非常に興味深いものになります。このアプリケーションはまだ未完成であり、とんがり頭の上司やおばあちゃんに手渡す前に、いくらか手を加えなければなりません。ただし、希望する場合は、その他の BackPack API 機能 (ページのタグ、リンク、共有、複製など) を挿入する適度なフレームワークが準備されています。言い換えると、このアプリケーションは再販を目的として作成されていません。むしろ、開発者のためのサンプル アプリケーションであり、XML を通信のため、およびインターフェイスとして使用するサーバーとの間で、XML を使用して通信する方法を理解する手だてとなることを目的としています。また、シリアル化や動的な XML の生成も経験できます。つまり、このアプリケーションは XML コーディングを楽しむための例、とも言えます。
Michael K. Campbell は開発者であり、データ処理に力を注いでいます。プログラミング、SQL Server 関連、および XML で長年の経験があります。現在、彼はほとんどの時間を 3Leaf Development でのエバンジェリズムに取り組んでいます。ご意見については、彼のサイト (http://www.angrypets.com/contact/) にアクセスするのが一番簡単な方法です。