JSON
Windows ランタイム コンポーネントで JSON 文字列を解析する
JavaScript を使って構築した Windows ストア アプリでは、HTML と JavaScript のスキルがあればだれでもネイティブ Windows アプリを構築できますが、JavaScript が常にすべての問題を解決する最高の選択肢になるとは限りません。アプリの一部の動作が、C#、Visual Basic、または C++ を使用したよりオブジェクト指向の方法で実装する方が適している場合があります。また、コードの特定の側面が、UI 層のデータを必要とする複数の Windows ランタイム (WinRT) コンポーネント間で再利用の候補になる場合があります。どちらの状況でも、データを JavaScript から WinRT コンポーネントに渡して UI に返すことを理解しておくことが重要です。
Web では、データがクライアントからサーバーに送信されて JSON オブジェクト形式で返されることがよくあります。さまざまなコンテキストで、ASP.NET Web フォームや ASP.NET MVC などのフレームワークには、モデル バインダーなどの機能、または少なくとも JSON オブジェクトを解析するためのある種のサーバー側の自動処理機能が含まれています。WinRT コンポーネントには JSON を解析できるオブジェクトが含まれていますが、サポートのレベルが低く、対話を簡略化するには、開発者側で明示的な処理が必要です。
この記事では、厳密に型指定されたオブジェクトを処理して結果を UI に返すために、WinRT コンポーネントに渡した JSON 文字列を確実に解析する方法を説明します。
WinRT コンポーネントと対話するための制限
JSON オブジェクト解析の詳細について説明する前に、まず WinRT コンポーネントと対話するための要件と制限をよく理解していただく必要があります。MSDN ヘルプ トピック「C++ および Visual Basic での Windows ランタイム コンポーネントの作成」(bit.ly/WgBBai) では、メソッド パラメーターと戻り値の型の宣言に関して WinRT コンポーネントに必要な要素を詳しく説明しています。使用できる型の大部分はプリミティブ型で、コレクション型はわずかなので、未加工の JSON オブジェクトをコンポーネントに渡そうとすることはできません。マネージ コンポーネントに JSON オブジェクトを渡す最良の方法は、最初に (JSON.stringify メソッドを使用して) JSON オブジェクトをシリアル化することです。これは、これらのクラスでは文字列が完全にサポートされているためです。
マネージ コードで JSON オブジェクトを解析する
Windows.Data.Json 名前空間には、JsonValue、JsonArray、JsonObject クラスなど、厳密に型指定された方法で JSON オブジェクトを操作するように設計されたさまざまなクラスが含まれています。JsonValue クラスは、文字列、数値、ブール値、配列、またはオブジェクト形式で公開された JSON 値を表します (詳細については、bit.ly/14AcTmFを参照してください)。JSON 文字列を解析するには、JsonValue に未加工の文字列を渡す必要があります。文字列を渡すと、JsonValue から JsonObject のインスタンスを返すことができます。
JsonObject クラスは完全な JSON オブジェクトを表し、ソース オブジェクトを操作するためのメソッドが含まれています。JsonObject クラスを使用して、メンバーの追加と削除、メンバーからのデータ抽出、各メンバーの反復処理、さらにはオブジェクトの再シリアル化を実行できます。JsonObject クラスの詳細については、bit.ly/WDWZkGを参照してください。
JsonArray クラスは JSON 配列を表し、JsonObject と同様に、配列要素の反復処理、追加、削除など、配列を制御するためのメソッドが多数含まれています。JsonArray クラスのインターフェイスの詳細については、bit.ly/XVUZo1を参照してください。
このようなクラスの使用を開始する方法の例として、JavaScript で次の JSON オブジェクトについて考えてみましょう。
{
firstName: "Craig"
}
このオブジェクトを WinRT コンポーネントに渡そうとする前に、JSON.stringify 関数を使用してこのオブジェクトを文字列にシリアル化する必要があります。このオブジェクトがシリアル化された後の影響に注意してください。まったく同じオブジェクトが次のように表現されます。
"{
'_backingData': {
'firstName': 'Craig'
},
'firstName': 'Craig',
'backingData': {
'firstName':'Craig'}
}"
Web ブラウザーでまったく同じ関数を呼び出す場合はオブジェクトにメンバーを追加することなくオブジェクトを文字列にシリアル化するだけなので、この結果には驚かれるかもしれません。JSON 文字列構造体の変更は、オブジェクトからデータを抽出する方法に影響します。
WinRT コンポーネントでこのデータを読み取るための最初の手順は、JsonValue インスタンスとして入力文字列の解析を試みることです。解析が成功した場合、ルート JsonValue インスタンスの JsonObject を要求できます。この場合、JsonValue は stringify 関数の呼び出しによって作成されるルート オブジェクトであり、JsonObject によって、JavaScript で開始した元のオブジェクトへのアクセスが許可されます。
次のコードは、JsonObject を利用できるようになったら GetNamedString メソッドを使用して “firstName” メンバーの値を抽出して変数に代入する方法を示しています。
JsonValue root;
JsonObject jsonObject;
string firstName;
if (JsonValue.TryParse(jsonString, out root))
{
jsonObject = root.GetObject();
if (jsonObject.ContainsKey("firstName"))
{
firstName = jsonObject.GetNamedString("firstName");
}
}
ブール値メンバーや数値メンバーにアクセスする場合も、同様のアプローチを使用します。これらのメンバーでは、GetNamedBoolean メソッドと GetNamedNumber メソッドを使用できます。次の手順では、JSON データへのアクセスを簡略化するために JsonObject の拡張メソッドを実装します。
JsonObject の拡張メソッド
JsonObject クラスの既定の実装には低レベルの動作が用意されています。この低レベルの動作は、不完全な形式を処理できソースにメンバーが存在しない場合に例外を回避できるシンプルなメソッドを使用して、大幅に強化できます。つまり、JavaScript で作成したオブジェクトには、例外の原因となる形式や構造の問題がいつか発生します。次の拡張メソッドを JsonObject クラスに追加すると、このような問題を軽減できます。
追加する最初の拡張メソッドは GetStringValue と呼ばれます。図 1 は GetStringValue の実装を示しています。この実装では、まずオブジェクトにメンバーが存在しているかどうかを確認します。この場合、key パラメーターが JSON オブジェクト プロパティの名前です。メンバーが存在していることを確認できたら、TryGetValue メソッドを使用して JsonObject インスタンスからデータにアクセスを試みます。値が見つかった場合、その値は IJsonValue インターフェイスを実装するオブジェクトとして返されます。
図 1 GetStringValue 拡張メソッドの実装
public static string GetStringValue(this JsonObject jsonObject,
string key)
{
IJsonValue value;
string returnValue = string.Empty;
if (jsonObject.ContainsKey(key))
{
if (jsonObject.TryGetValue(key, out value))
{
if (value.ValueType == JsonValueType.String)
{
returnValue = jsonObject.GetNamedString(key);
}
else if (value.ValueType == JsonValueType.Number)
{
returnValue = jsonObject.GetNamedNumber(key).ToString();
}
else if (value.ValueType == JsonValueType.Boolean)
{
returnValue = jsonObject.GetNamedBoolean(key).ToString();
}
}
}
return returnValue;
}
IJsonValue インターフェイスには読み取り専用の ValueType プロパティが含まれ、このプロパティは、オブジェクトのデータ型を表す JsonValueType 列挙型の特定の値を公開します。ValueType を参照したら、適切に型指定されたメソッドを使用してオブジェクトからデータを抽出します。
GetStringValue メソッドには、形式が正しくない JSON オブジェクトを防ぐための、ブール値と数値を識別する処理が含まれています。実装をより厳密にして解析を控えたり、想定している型向けに JSON オブジェクトの形式が厳密に指定されていない場合にエラーをスローしたりすることもできますが、この例のコードでは、解析処理を柔軟にしてエラーを防ぎます。
図 2に示す次の拡張メソッドは、ブール値を抽出するための実装です。ここでは、文字列 (たとえば、"1" または "true" は true の値など) と数値 (たとえば、"1" は true、"0" は false など) として表したブール値が GetBooleanValue メソッドでサポートされています。
図 2 GetBooleanValue 拡張メソッドの実装
public static bool? GetBooleanValue(this JsonObject jsonObject,
string key)
{
IJsonValue value;
bool? returnValue = null;
if (jsonObject.ContainsKey(key))
{
if (jsonObject.TryGetValue(key, out value))
{
if (value.ValueType == JsonValueType.String)
{
string v = jsonObject.GetNamedString(key).ToLower();
if (v == "1" || v == "true")
{
returnValue = true;
}
else if (v == "0" || v == "false")
{
returnValue = false;
}
}
else if (value.ValueType == JsonValueType.Number)
{
int v = Convert.ToInt32(jsonObject.GetNamedNumber(key));
if (v == 1)
{
returnValue = true;
}
else if (v == 0)
{
returnValue = false;
}
}
else if (value.ValueType == JsonValueType.Boolean)
{
returnValue = value.GetBoolean();
}
}
}
return returnValue;
}
数値ベースの拡張メソッドは null 許容型を返すように設定されているので、この場合、GetDoubleValue は null を許容する double を返します。この場合の修正処理では、文字列から対応する数値への変換を試みます (図 3 参照)。
図 3 GetDoubleValue 拡張メソッドの実装
public static double? GetDoubleValue(this JsonObject jsonObject,
string key)
{
IJsonValue value;
double? returnValue = null;
double parsedValue;
if (jsonObject.ContainsKey(key))
{
if (jsonObject.TryGetValue(key, out value))
{
if (value.ValueType == JsonValueType.String)
{
if (double.TryParse(jsonObject.GetNamedString(key),
out parsedValue))
{
returnValue = parsedValue;
}
}
else if (value.ValueType == JsonValueType.Number)
{
returnValue = jsonObject.GetNamedNumber(key);
}
}
}
return returnValue;
}
JsonObject クラスの数値を抽出する組み込みメソッドは double を返しますが、データ値は integer として表されることが多いため、次のコードでは、GetIntegerValue メソッドで GetDoubleValue メソッドをラップして結果を integer に変換する方法を示しています。
public static int? GetIntegerValue(this JsonObject jsonObject,
string key)
{
double? value = jsonObject.GetDoubleValue(key);
int? returnValue = null;
if (value.HasValue)
{
returnValue = Convert.ToInt32(value.Value);
}
return returnValue;
}
ファクトリのサポートを追加する
JsonObject クラスはデータをプリミティブ型として抽出するための高度なサポートを含むように拡張されたので、次の手順では、入力 JSON 文字列を取得したり処理したドメイン オブジェクトのインスタンスを返したりするファクトリ クラスで、このサポートを使用します。
次のコードは、人物をシステムでモデル化する方法を示しています。
internal class Person
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public bool? IsOnWestCoast { get; set; }
}
次のコードは、PersonFactory クラスの、文字列を受け取る Create メソッドを示しています。
public static Person Create(string jsonString)
{
JsonValue json;
Person person = new Person();
if (JsonValue.TryParse(jsonString, out json))
{
person = PersonFactory.Create(json);
}
return person;
}
図 4 は、JsonValue を受け取る Create メソッドを示しています。併用されているこれらの Create メソッドでは、未加工の文字列を取得したり、各メンバーで期待されているデータを含む Person クラスのインスタンスを返したりします。これらの Create メソッドは、次のセクションで説明する JSON 配列のサポートを提供するために、分離され、オーバーロードされています。
図 4 JsonValue を受け取る PersonFactory の Create メソッド
public static Person Create(JsonValue personValue)
{
Person person = new Person();
JsonObject jsonObject = personValue.GetObject();
int? id = jsonObject.GetIntegerValue("id");
if (id.HasValue)
{
person.Id = id.Value;
}
person.FirstName = jsonObject.GetStringValue("firstName");
person.LastName = jsonObject.GetStringValue("lastName");
bool? isOnWestCoast = jsonObject.GetBooleanValue("isOnWestCoast");
if (isOnWestCoast.HasValue)
{
person.IsOnWestCoast = isOnWestCoast.Value;
}
return person;
}
配列のサポートを追加する
データは、単なる 1 つのオブジェクトではなく、オブジェクト配列の形式で受け取る場合があります。この場合、JsonArray クラスを使用して、文字列を配列として入力文字列の解析を試みる必要があります。図 5は、入力文字列を配列として解析し、各項目を Create メソッドに渡して、最終的にモデルとして解析する方法を示しています。文字列をオブジェクト配列として解析できず、結果が空の配列になる場合に備えて、最初に Person リストの新しいインスタンスを作成していることに注意してください。このようにすると、想定外の例外を回避できます。
図 5 PersonFactory の CreateList メソッド
public static IList CreateList(string peopleJson)
{
List people = new List();
JsonArray array = new JsonArray();
if (JsonArray.TryParse(peopleJson, out array))
{
if (array.Count > 0)
{
foreach (JsonValue value in array)
{
people.Add(PersonFactory.Create(value));
}
}
}
return people;
}
サポート クラスを追加する
次の手順では、ファクトリ クラスを使用したり、モデル インスタンスの結果に関して興味深い処理を実行したりするオブジェクトを作成します。図 6 に、個別の文字列と JSON 配列文字列の両方を使用し、これらを厳密に型指定されたオブジェクトとして操作する方法を示します。
図 6 ContactsManager の実装 (非同期サポートなし)
using System.Collections.Generic;
public sealed class ContactsManager
{
private string AddContact(string personJson)
{
Person person = PersonFactory.Create(personJson);
return string.Format("{0} {1} is added to the system.",
person.FirstName,
person.LastName);
}
private string AddContacts(string personJson)
{
IList people = PersonFactory.CreateList(personJson);
return string.Format("{0} {1} and {2} {3} are added to the system.",
people[0].FirstName,
people[0].LastName,
people[1].FirstName,
people[1].LastName);
}
}
非同期対話をサポートする
JSON メッセージは予測不能なサイズに肥大化してアプリケーションで待機時間を引き起こす可能性があるので、WinRT コンポーネントのメソッドに対するこのような性質の呼び出しは、非同期に実行する必要があります。
次のコードは、AddContact メソッドへの非同期アクセスをサポートするために ContactsManager に追加されたメソッドを含んでいます。
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Windows.Foundation;
public IAsyncOperation AddContactAsync(string personJson)
{
return Task.Run(() =>
{
return this.AddContact(personJson);
}).AsAsyncOperation();
}
AddContactAsync メソッドでは、JSON 文字列を受け取って、Task を開始します。この Task で AddContact メソッドを実行します。Task が完了すると、IAsyncOperation インターフェイスのサポートによって用意されている JavaScript promise に応答が送信されます。AddContact と AddContacts の両方に対する非同期サポートを含む ContactsManager クラスの完全なソース コードは、付属のコード ダウンロードで確認できます。
JavaScript で promise を遵守する
パズルの最後のピースは、JavaScript で ContactsManager クラスを使用し、promise パターンを使用して ContactsManager クラスを呼び出すことです。この例で使用するアプローチでは、モデル化されたデータを WinRT コンポーネントに渡して応答を待つ、ビュー モデルを実装します。コンポーネントに渡すために使用するデータは図 7 のように定義されており、単一の JSON オブジェクトと配列を含んでいます。
図 7 JSON データ ソース
var _model = {
contact: {
id: 1000,
firstName: "Craig",
lastName: "Shoemaker"
},
contacts: [
{
id: 1001,
firstName: "Craig",
lastName: "Shoemaker",
isOnWestCoast: "true"
},
{
id: 1002,
firstName: "Jason",
lastName: "Beres",
isOnWestCoast: "0"
}
]
}
図 8 に示すビュー モデルには、モデルのメンバーと、WinRT コンポーネントから返されるメッセージのメンバーが含まれています。JavaScript 用 Windows ライブラリ (WinJS) のバインディング フレームワークは、HTML 要素への応答から返されたメッセージをバインドするために使用しています。完全なページ モジュールを付属のコード ダウンロードで参照できるので、すべての部分がどのように組み合わされているかを確認できます。
図 8 ContactsManager を使用するビュー モデル
var _vm = {
ViewModel: WinJS.Binding.as({
model: _model,
contactMsg: "",
contactsMsg: "",
addContact: function () {
var mgr = ParseJSON.Utility.ContactsManager();
var jsonString = JSON.stringify(_vm.ViewModel.model.contact);
mgr.addContactAsync(jsonString).done(function (response) {
_vm.ViewModel.contactMsg = response;
});
},
addContacts: function () {
var mgr = ParseJSON.Utility.ContactsManager();
var jsonString = JSON.stringify(_vm.ViewModel.model.contacts);
mgr.addContactsAsync(jsonString).done(function (response) {
_vm.ViewModel.contactsMsg = response;
});
}
})
};
データ バインド中に addContact 関数または addContacts 関数をボタンにバインドする場合は、ビュー モデルの関数に対する参照を渡して WinJS.Utilities.requireSupportedForProcessing 関数を実行する必要があります。
最後の手順では、適切な要素と属性を HTML に追加して、バインドをサポートします。div 要素は、バインド要素のメイン バインド コンテナーとして機能し、data-win-bindsource="Application.Pages.Home.ViewModel" という設定でマークされています。次に、data-win-bind 属性に適切な値を指定することで、ヘッダー要素がデータ メンバーとバインドされます。
<section aria-label="Main content" role="main">
<div data-win-bindsource=
"Application.Pages.Home.ViewModel">
<h2 data-win-bind="innerText: contactMsg"></h2>
<hr />
<h2 data-win-bind="innerText: contactsMsg"></h2>
</div>
</section>
これで完了です。JavaScript を使用した Windows ストア アプリの構築には、Web で培った既存スキルを活用してネイティブな最新 UI のアプリを作成できますが、2 つのプラットフォームを隔てる要素は多数あります。JSON データを解析する低レベルのサポートは Windows.Data.Json 名前空間を通じて利用できますが、いくつかの拡張機能を使用するもっと充実したサポートを既存のオブジェクトに追加することもできます。
Craig Shoemakerは、ソフトウェア開発者、ポッドキャスト製作者、ブログ執筆者、およびテクニカル エバンジェリストです。また、Code Magazine、MSDN、および Pluralsight でも執筆活動を行っています。余暇には、自分の貴重な "針" のコレクションを隠しておく "干し草の山" 探しを楽しんでいます。彼の連絡先は、Twitter (twitter.com/craigshoemaker、英語) です。
この記事のレビューに協力してくれた技術スタッフの Christopher Bennage (マイクロソフト)、Kraig Brockschmidt (マイクロソフト)、および Richard Fricks (マイクロソフト) に心より感謝いたします。
Christopher Bennage はマイクロソフトの Patterns & Practices チームの開発者です。彼の仕事は、開発者が利用できるプラクティスを発見、収集し、推奨することです。最近彼の興味を引いているのが JavaScript と (カジュアル) ゲーム開発です。彼のブログは dev.bennage.com(英語) です。
Kraig Brockschmidt は 1988 年からマイクロソフトに勤めており、執筆、教育、講演、および直接的な活動を通じて開発者を支援することを担当しています。彼は、Windows Ecosystem チームのシニア プログラム マネージャーとして、主要パートナーと共同で Windows ストア アプリを構築し、この経験から得た知見を幅広い開発者コミュニティに伝えています。最新の著書には Microsoft Press の無償の電子ブック『Programming Windows 8 Apps with HTML, CSS, and JavaScript (HTML、CSS、および JavaScript を使用した Windows 8 アプリのプログラミング)』があります。彼のブログは kraigbrockschmidt.com/blog(英語) です。
Richard Fricks は、過去 20 年間、開発者コミュニティと協力してきました。最近は、Media 名前空間用の Windows ランタイム API ストラテジの設計を手伝ったり、開発者コミュニティで Windows 8 の新しい機能を採用するのを支援したりしています。現在、彼は Windows シナリオ導入チームのプログラム マネージャーです。