次の方法で共有


Coding4Fun: BackPack API を利用する - 第 I 部

BackPack API を利用する - 第 I 部

Michael K. Campbell
3Leaf Development

今ではもう "昔" の話ですが、Palm と Windows CE の支持者の間で激しい論争がありました。論争の焦点は、シンプルな OS と単純な機能を持つ Palm の方がユーザーのニーズを満たしているのか、複雑な機能をすべて搭載した Windows CE の方がよい選択かどうかという問題でした。簡単に言うと、どちらが快適かという議論です。豊富な方が便利なのか、簡潔な方が快適なのか、どちらだと思いますか。私はいつも豊富な方が快適であると思っていました。少なくとも、その論争ではそう思いました。

ところが、簡潔な方が明らかに快適な場合もあるのです。37 Signals は、これをよく理解している 5 人の社員から成る小規模な会社です。彼らは "きわめて単純な" 3 本のオンライン アプリケーションで成功を収め、Web の開発コミュニティでは伝説に残るほど有名になりました。成功の秘訣は、最先端の技術を利用し、入念に目標設定されたアプリケーションです。不要なものをすべて取り除き、驚くほどシンプルなユーザー インターフェイスによって、直感的に理解でき、誰でも簡単にすぐ使用できるように作られています。これらの長所に加えて、"最初は無料" というマーケティング モデルを採用しています。ユーザーは生産性を向上させるため、有料サービスにアップグレードせずにはいられません。彼らが成功した理由を考えるのは難しいことではありません。

37 Signals クールエイドを気に入っていただけて何よりですが、これは XML の記事です

XML について素晴らしい点の 1 つは、ユビキタスに近い存在になっていることです。実際に、データ転送の共通言語になりつつあります (ただし、この世界にはまだ途方もない境界、フラット ファイル、レガシー、弊害があります)。37 Signals では、XML が本来持っているデータ交換機構としてのメリットを理解し、使いやすい API を提供して同社の主力製品の 1 つである BackPack と対話できるようにしました。

BackPack は、間違いなく "簡潔で快適な" アプリケーションです。個人が単独で使用することもできますが、共同作業の効率化のために小グループで使用すると真価を発揮します。これはきわめて簡単なウィキであると考えるのが一番わかりやすい方法です。たいへん使いやすいので、スライシングやダイシングの操作を行っていると、とんがり頭の上司までくつろいだ気分になってきます。成功した理由は、ほとんど説明が要らないからです。ユーザー インターフェイスがユーザーを巧みに案内してくれるので、すぐにこの製品の達人になれます。ユーザー インターフェイスによって、根底にある複雑さがうまく隠蔽されているため、ウィキのようなマニア向けのシステムに入り込んでいることに気が付きません。多くの点で、このようなわかりやすさは、XML の API に巧みに受け継がれています。

XML4Fun と BackPack API

今回の記事から 3 回の連載で BackPack API に注目し、面白そうなので実際に使用してみます。最初の記事では、無料の BackPack アカウントをセットアップする方法について簡単に説明し、BackPack Web サービスとの対話に必要な XML トークンを取得し、サーバーに接続してコマンドやデータを送受信する方法について詳しく調べます。この記事では、おおまかな Windows フォーム アプリケーションを構築して、BackPack ページ オブジェクトのすべての使用可能な操作をモデル化します。この後の 2 回の記事では、その他の API をすばやく肉づけし、アプリケーションの UI に改善を加えた後、XML シリアル化およびその他の要素を使用して、BackPack を携帯用に作成します。つまり、データのキャッシュと、変更内容やコマンドのキューへの保存を可能にすることで、オフラインでもサンプル アプリケーションを使用できるようにします。

アカウントの作成

API を使用するには、アカウントをセットアップする必要があります。基本のアカウントは、無料で簡単にセットアップできます。無料アカウントに登録したら、アカウントの詳細を確認し、サービス トークンを取得します (BackPack サービスの身元確認のための SHA1 セキュリティ トークン)。サービス トークンは、アカウント ページの下部にあります。

API

API は、BackPack サイトでわかりやすい例を示してていねいに説明されています。API の導入部分では、データ交換要件が定義され (詳細はこの後を参照)、投稿されたデータとそれに対応する応答の両方の例が示されています。API について例を挙げて説明し、手抜きがありません。これはめずらしいことです。

この API で気に入っている点は、まったく単純であることです。この API を見ると、安っぽい Web サービス (古すぎるか貧弱すぎて SOAP を使用できない Web サービス) を思い出します。これらの "サービス" の多くは、SOAP が大流行する前から存在し、ほとんどと言っていいほど、クライアントとサーバーが相互に XML パッケージを投げ合っているだけでした。これらの "サービス" は多くの場合、説明もひどく、支離滅裂で理解するのが困難でした。一方、BackPack API は単純で論理的に制約されています。小さな XML パッケージが "メソッド" ごとに論理的に定義されたサーバー上の URL に送信され、XML "データ" パケットが返されて処理されています。

通信機構を作成する

API ドキュメント内をスクロールしてひととおり理解したら、今度はクライアントと BackPack サービスの通信機構を作成します。幸い、.NET に用意された System.Net.HttpWebRequest オブジェクトと、それに対応する System.Net.HttpWebResponse オブジェクトを使用すると、このような通信を非常に簡単に作成できます。標準の POST 画面にいくらか "装飾" を加えるだけで、すぐに XML を送受信できるようになります。

BackPack API では、サーバー上で要求を適切に識別するには、カスタム要求ヘッダー X-POST_DATA_FORMAT (値を "xml" に設定) を含める必要があると指定しています。おまけとして、筆者も可能な限り、必ず MIME の種類を指定しています。次に例を示します。

  HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
request.Method = "POST";
request.Headers.Add("X-POST_DATA_FORMAT", "xml");
request.ContentType = "application/xml";
// 動作しません: エンコードが必要です:
request.ContentLength = xml.Length;
StreamWriter post = new StreamWriter(request.GetRequestStream());
post.Write(xml);
post.Close();
HttpWebResponse response = (HttpWebResponse)request.GetResponse();
StreamReader responseBody = new StreamReader(response.GetResponseStream());
string responseInfo = responseBody.ReadToEnd();
responseBody.Close();

まあこんなものです。HttpWebRequest によって、ソケットの詳細などがすべて取り除かれます。応答は、すべてが適切に処理された場合、簡単な文字列で返され、必要に応じてすぐに利用できます。

API と対話するアプリケーションを構築する

通信コードの適切なチャンクが作成されたら、周辺のアプリケーションを構築します。ここでは、フォーム コントロールの作成とレイアウトを簡単に行える Visual C# Express を使用します。通常、API のコードの作成では、半繰り返し的なコピー/貼り付け/微調整の操作が多く含まれるため、タイプミスが多く発生する可能性があります。それを回避し、このアプリケーションを体裁よく整えるため、ここでは簡単な方法で XML の送受信を表示できるようにします。これはサンプル アプリケーションの大きな部分を占めることになります。こうするとデバッグが簡単になるだけでなく、アプリケーションの動作中にその中で何が起きているかを XML の視点から観察するウィンドウを備えることができます。

この最初の記事では、ページの操作だけを扱います (メモ、タグ、アラームなどは、次回の記事で扱います)。ただし、更新、削除、表示などの対象を選択するには、実行中のページの一覧を表示する方法が必要です。そのため、使用可能なページの一覧を表示するリスト ボックスと、API によって示されたページ操作を実装するための専用の Winform セクションが別途必要になります。また、接続先のアカウントを指定する方法と、要求を送るときに含める API キーを入力する方法も必要です。

ハリッハするよ拡大画像が表示され?す

(画像をクリックすると拡大表示されます。)

上の図のとおり、呼び出す API メソッドの選択は、簡単なラジオ ボタンで制御されています。ただし、当然のことですが、List All メソッドは例外として、[Load Pages] ボタンに結び付けられています。ユーザー入力のためのテキスト ボックスとチェック ボックスがラジオ ボタンの横に並べられ、選択されたラジオ ボタンに応じて、必要なフィールドのオンとオフが切り替わります (37 Signals の UI は、筆者の UI と比較して少し優っているような気がします)。

XML を構築する

当然ですが、XML でサーバーと通信する必要があるアプリケーションでは、Winform にボタンが整然と (あるいは、でたらめに) 配置されているかどうかに関係なく、ボタンをクリックするだけではほとんど意味がありません。この場合も、API が単純であり、また XML も同様に簡単であるため、XML ドキュメント オブジェクト モデル (DOM) の使用は、適切な XML を生成して必要に応じてユーザー入力を変換するための最適な選択となります。

新規に XML を生成する場合、DOM の使用は快適です。基本的な構想では、XML ドキュメントを読み込むと (空のドキュメントであっても)、新しい XML 要素 (または、コメントや属性などのノード) をドキュメントのルートから "フロート" し、ノードの内部データをいじった後、ドキュメント内の選択された特定の場所にノードを追加できます。以前にこの操作を実行したことがあれば、簡単にできます。ただし、この操作がどのようにして行われるかを詳しく調べると、多くのユーザーは圧倒されます。.NET Framework 内で、DOM は System.Xml.XmlDocument オブジェクト内に抽出されます。このオブジェクトは、要素の新規作成、要素へのコンテンツの挿入、および目的の場所への配置に必要なすべての機能を備えています。

たとえば、API では、新しいページを作成するには 2 つの子要素 (タイトルと説明) を用意し、それらの要素がページ要素内にネストされている必要があります。次のコードは、空の XmlDocument インスタンスを使用して、3 つの新しい要素をプログラムで作成する方法を示しています。これらの要素は、作成された時点では、外部からの異種の付属品のように XmlDocument から突き出しています。望ましい形にするには、整理して配置する必要があります。

  XmlDocument args = new XmlDocument();
XmlElement page = args.CreateElement("page");
XmlElement title = args.CreateElement("title");
title.InnerText = this.txtPageTitle.Text.Trim();
XmlElement description = args.CreateElement("description");
description.InnerText = this.txtPageBody.Text.Trim();
page.AppendChild(title);
page.AppendChild(description);

簡単だと思いませんか。ほんの少しのコードで、API との対話に必要な XML が作成されました。

アヒルを集める、または API をラップする方法

接続および XML 操作のロジックが作成され、ユーザーの入力と対話を処理する Winform が整ったら、今度は BackPack API をラップする最適な方法を検討します。API をラップすると、必ず大量のロジックがコード内に埋め込まれます。したがって、重要な目標の 1 つは、繰り返しをなくすことです。繰り返しがあると、コードの保守が困難になります。もう 1 つの目標は、ロジックがロジック自体を "認識" しすぎて、領域 "x" のコードがセクション "y" のコードの動作を予測し、その動作に依存してしまうような事態を招かないようにすることです。この間違いを犯すと、悪質な結合の罪として、自分だけでなく子供の子供のそのまた子供の代まで恥をかき、身を滅ぼし、信用を落とします。

API をラップするときに実施する重要なことの 1 つは、中核的なロジックを可能な限り 1 か所にまとめて配置し、中核的な API (事実上、メソッド名として機能する URL の操作) の周囲に多数の "ロジックの膨らみ" を追加しないことです。これらの "メソッド名" は、ユーザー アクションに基づいて動的に選択される必要がありますが、それ以降は、メソッド間の類似性が事実上崩壊します。ここがデリゲートの活躍のしどころです。デリゲートを使用すると、すべての URL 構築要素を 1 つのルーチン ([Submit] ボタンのイベント ハンドラ) に対してローカルにすることができます。ここで、すべての URL を簡単にマップし、必要に応じて動的に作成できます。各種ユーザー データの XML グラムの構築など、比較的複雑なロジックは、必要に応じて後続の "開始点" メソッドで処理できます。

ただし、デリゲートは、別の問題をもたらします。さまざまなオフボックス サーバー エンドポイントとの通信に時間を費やす Winform では、バックグラウンド スレッドで通信処理を行わないと、通信中に "フリーズ" することがあります (Winform のメイン スレッドは UI タスクの処理のために空けておきます)。デリゲートを使用すると、新しいバックグラウンド スレッドを問題なく簡単に生み出すことができ、このバックグラウンド スレッドを使用して、ユーザー入力を収集し、くるんで、適切な Web メソッドを呼び出すなど、実装の詳細を処理できます。その間、Winform のメイン スレッドは、メッセージ ポンプや UI タスクの処理のために空けておくことができます。以下は、[Submit] ボタンのためのイベント ハンドラ全体のコード スニペットです。必要に応じて URL が構築され、デリゲートが適切な処理ルーチンに接続され、通信を処理するスレッドが起動されています。

  
    private void button2_Click(object sender, EventArgs e)
{
	// 新規ページの作成以外では pageID が必要です。
	if (this._currentOperation != PageOperation.Create)
	{
		if (this._currentPageId == 0)
		{
			MessageBox.Show("Please Select a Page First.");
			return;
		}
	}
	// Backpack サーバーで接続するパスを指定し、適切なメソッドを呼び出します。
	//
		to load xml 'args' as needed:
	OperationDelegate operation = null;
	switch (this._currentOperation)
	{
		//case PageOperation.ListAll:
		//	break;
		case PageOperation.Create:
			this._webMethodPath = "/ws/pages/new";
			operation = new OperationDelegate(this.InvokeCreatePage);
			break;
		case PageOperation.Show:
			this._webMethodPath = string.Format("/ws/page/{0}/show", 
				this._currentPageId.ToString());
			operation = new OperationDelegate(this.InvokeSimpleMethod);
			break;
		case PageOperation.Destroy:
			this._webMethodPath = string.Format("/ws/page/{0}/destroy", 
				this._currentPageId.ToString());
			operation = new OperationDelegate(this.InvokeSimpleMethod);
			break;
	// 簡潔にするため省略します... 
	
		case PageOperation.Email:
			this._webMethodPath = string.Format("/ws/page/{0}/email", 
				this._currentPageId.ToString());
			operation = new OperationDelegate(this.InvokeSimpleMethod);
			break;
		default:
			MessageBox.Show("Woops.");
			break;
	}
	if (operation != null)
	{
		Thread operationHandler = new Thread(new ThreadStart(operation));
		operationHandler.IsBackground = true;
		operationHandler.Start();
	}
}

これで、各 case に大量のコードを詰め込まずに、すべての URL を 1 か所に保持することができ、読みやすさを高めることができました。デリゲートを使用すると、複雑なロジックは、別のスレッドで開始される適切なメソッドで処理できます。たとえば、追加の XML 引数をアセンブルする必要があるページの作成は、次のように処理されます。

  
    private void InvokeCreatePage()
{
	XmlDocument args = new XmlDocument();
	XmlElement page = args.CreateElement("page");
	XmlElement title = args.CreateElement("title");
	title.InnerText = this.txtPageTitle.Text.Trim();
	XmlElement description = args.CreateElement("description");
	description.InnerText = this.txtPageBody.Text.Trim();
	page.AppendChild(title);
	page.AppendChild(description);
	this._request.ExecuteWebMethod(this._webMethodPath, page);
}

サービス トークン以外の XML を必要としない API 操作は、ヘルパ メソッドに対して URL を起動するだけのキャッチ オール メソッドによってすべて処理されます。

  
    private void InvokeSimpleMethod()
{
	this._request.ExecuteWebMethod(this._webMethodPath);
}

これらのデリゲート メソッドを、引数を含まない void メソッド (VB.NET 用語では Sub) にする必要があるため、URL およびその他の要素がデリゲート メソッドに渡されないことに注意してください。これらのシグネチャは、System.Threading.ThreadStart デリゲートのものと一致します。

再び、接続について

接続ロジックと API キーがアプリケーション内で分散しないように、BackPackRequest クラスを作成して、すべての接続の詳細を取り除きました。その目的は、接続の詳細を取り除いて呼び出し側と区別し、一貫した方法で、BackPack サービスの呼び出しが適切にフォーマットされ、必要なサービス トークンがすべて提供されるようにすることです。

Winform でユーザーの資格情報 (ユーザー名と API キー) が入力または変更されると、BackPackRequest オブジェクトの新しいインスタンスが作成され、コンストラクタでアカウント名、API キー、およびログ デリゲート (詳細はこの後を参照) が必要になります。コンストラクタは、基本 BackPack URL を接続し、後続の各接続要求のために API キーを蓄え、呼び出し側への単一連絡点にオーバーロードを提供します。つまり、メソッドの場所の URL と、必要に応じて引数の役割を果たすオプションの XmlElements の配列を受け取るメソッドです。

各要求にサービス トークンを含めるために、プライベートのヘルパ メソッドを作成して必要なトークンを加え、ユーザー入力を表すオプションの XmlElements と共に、要求要素の中にラップしました。ただし、この XML のチャンクを構築するには、BackPackRequest インスタンスが所有する新しい空の XmlDocument から要素をフロートさせる必要があります。追加するオプションの要素は、実際には別のドキュメントにあるため、問題が生じます。それは、ドキュメント上の新しい要素をフロートして目的の場所に追加することはできますが、別のドキュメントへ要素をフロートすることはできないからです。共通のトランクからノードを作成するために単一の XmlDocument を 2 つのクラス (Winform と BackPackRequest インスタンス) で共有することは、理にかなった方法ではありません (耐え難いほど、間違った結合です)。したがって、あるドキュメントから別のドキュメントにノードをインポートする必要がありました。この場合も、フレームワークを使用すると簡単です。下記でおわかりのように、XmlDocument の ImportNode() メソッドを使用すると、複数の要素にゼロを追加するためのロジックを簡単に実装できます。

  
    private XmlNode BuildCommand(XmlElement[] args)
{
	XmlDocument command = new XmlDocument();
	XmlElement request = command.CreateElement("request");
	XmlElement token = command.CreateElement("token");
	token.InnerText = this._apiKey;
	request.AppendChild(token);
	command.AppendChild(request);
	if (args != null)
	{
		for (int i = 0; i < args.Length; i++)
		{
			XmlNode arg = command.ImportNode(args[i], true);
			request.AppendChild(arg);
		}
	}
	return command.SelectSingleNode("/"); // ルート
}

上記のコードでは、インポートするノードの詳細なコピーを作成している点に注意してください (ImportNode() の 2 番目の引数が true に設定されています)。簡易なコピーでは、データの中身なしで要素名だけがインポートされます。

XML の対話の詳細をログに記録する

この時点で残っている大きな作業は、Winform のログ機能の接続です。BackPackRequest インスタンスがデリゲートを呼び出すたびに、実行中の内容について実際には何も認識することなく、Winform に情報が返されます。デリゲートがこれほど強力なツールで、結合との戦いで大きな味方となるのはこのためです。

ログのニーズを満たし、上記の接続機能に対処するには、前に説明した最初の接続ロジックに補足を加えて、次のように非同期で実行される本格的なメソッドにします。

  
    internal string ExecuteWebMethod(string methodLocation, XmlElement[] args)
{
	try
	{
		string url = this._url + methodLocation;
		XmlNode xmlCommandNode = this.BuildCommand(args);
		string xml = xmlCommandNode.InnerXml;
		this._screenWriter(" ");
		this._screenWriter("Send: (" + url + ")" + System.Environment.NewLine + xml);
		HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
		request.Method = "POST";
		// ... 
		// 省略します - (最初のリストとまったく同一の接続コードです)
		// ...	
		responseBody.Close();
		this._screenWriter("Response:" + System.Environment.NewLine + responseInfo);
		return responseInfo;
	}
	catch (Exception ex)
	{
		this._screenWriter("EXCEPTION: " + ex.ToString());
		return null;
	}
}

ただし、このコードには問題があります。上記のコードを実行するのはバックグラウンド スレッドであり、このログの趣旨は Winform の txtLog テキスト ボックスの情報を書き込むことです。しかし、バックグラウンド スレッドが Winform のユーザー コントロールに作用すると、Winform は非常に不安定 (文字どおり) になります (UI スレッドまたはコントロールの所有者だけが作用できます)。このため、簡単なメソッドにデリゲートを送ってローカル変数にログ情報を保存し、画面にログする新しい情報があることを UI スレッドに通知する必要があります。

  
    private void OnLogged(string info)
{
this._logMessage = info;
this.BeginInvoke(new MethodInvoker(this.BindLogMessage));
}

また、ログ情報を画面に出力するとなると、受信した最新の情報が出力されるように、テキスト ボックスの内容を "スクロール" する方法が必要です。これを実現するため、現在フォーカスを持つコントロールを取得し、フォーカスを txtLog テキスト ボックスに渡してテキストを出力し、内容の最後までテキスト ボックスをスクロールした後、フォーカスを持つべき元のコントロールに制御を返しました。

  
    private void BindLogMessage()
{
	// 競合条件、ロックなどに注意する必要があります。
	this.txtLog.Text += System.Environment.NewLine + this._logMessage;
	// 各部分の処理、フォーカスの移動、出力のスクロールなどを行います。
	Control current = this.ActiveControl;
	this.txtLog.Focus();
	this.txtLog.SelectionStart = this.txtLog.Text.Length;
	this.txtLog.SelectionLength = 0;
	this.txtLog.ScrollToCaret();
	current.Focus();
	Application.DoEvents();
	
}

陳腐なやり方ですが、スクロールするには、スクロールの対象となるコントロールにフォーカスを渡す必要があるという事実を考慮すると、これ以上の方法が見つかりませんでした。

BackPack をテスト運転する

この時点で、操作の選択内容が単一のイベント ハンドラに送られるユーザー インターフェイスが完成しました。イベント ハンドラは、呼び出す適切な URL を決定し、必要なユーザー入力を XML にアセンブルして、サーバーに送信します。長い道のりでしたが、これで BackPack API の Page 機能が完全に実装され、テスト運転を実行できます。

ハリッハするよ拡大画像が表示され?す

(画像をクリックすると拡大表示されます。)

[Account Name] と [API Key] の詳細を入力し、[Connection Information] グループ ボックスから離れると、両方のフィールドが入力されていることを Winform が検出し、バックグラウンドで BackPackRequest クラスのインスタンスが作成されます。次に、[Load Pages] をクリックすると、ページが存在する場合はページが読み込まれます。ページが存在しない場合は、ページを作成してから、[Load Pages] をクリックします。ページが読み込まれたら、ページの削除、ページの名前変更、リンクしてページを共有するなどの操作を実行できます。BackPack にページのコピーを電子メールで送信させることもできます (こうすると、どういうわけか上級者になったような満足感があります)。

まとめ - 第 I 部

XML API に注目し、DOM を使用して動的に XML ドキュメントを生成して、独自のおおまかなユーザー インターフェイスから "データ グラム" を作成しました。[Pages] ボックス以外では、返された XML の使用については概ね無視しました。[Pages] ボックスでは、返されたページのノードを繰り返し処理し、ハッシュテーブルおよびリスト ボックスにバインドしているだけです。それ以外では、サービスから返された XML の使用は、ログ機能を介して Winform に出力することだけです。

ただし、次回の記事では、返された XML を有効かつ効率的に利用する方法を詳しく説明します。API のその他の部分を肉づけするほか、アプリケーションに機能を加えます。これにより、Web から BackPack データを取得し、オフラインに設定し、アプリケーションで更新および変更を加えた後、オンラインに戻ったときに変更内容をサーバーにマーシャリングできるようになります。すべてが完了したら、素晴らしいツールになります。

それまでコーディングに励んでください。

Michael K. Campbell は開発者であり、データ処理に力を注いでいます。プログラミング、SQL Server 関連、および XML で長年の経験があります。現在、彼はほとんどの時間を 3Leaf Development でのエバンジェリズムに取り組んでいます。ご意見については、彼のサイト (http://www.angrypets.com/contact/) にアクセスするのが一番簡単な方法です。

top of page Top of Page