使ってみよう! Bing API/SDK

第5回Hello, Bing Map App!─⁠─Silverlightで作るBing Mapsアプリケーション(5)

はじめに

今回はネットワークアクセスを行うMap Appを作ってみましょう。Silverlightアプリケーションで画像やWebページダウンロードなどのWebアクセスは、.NET Frameworkと同様にSystem.Net.WebClientクラスを使用すると簡単にできますが、Map App SDKには、ネットワークアクセス用の機能が提供されています。クラスが用意されているだけでドキュメント類は皆無なのですが、今回はこれを使用してネットワークアクセスに挑戦してみましょう。

Webアクセス

NetworkManagerContract

ネットワークアクセスにはNetworkManagerContractを使用します。このContractで提供される機能を使用すると、WebClientに似たPrioritizedWebClientクラスを使えます。特徴は、名前にあるようにWebアクセスのとき優先順位を設定可能になっています。地図の隠れている周辺部分の情報を低優先で先行して情報を取得しておくなどの用途が想定されているのではないかと思われます。また、このContractを使用するとネットワークアクセスが発生していないアイドル状態を知ることができます。

コードは、これまでと同様にContractの機能を使用するため、次のようにプロパティを宣言します。

[ImportSingle("Microsoft/NetworkManagerContract", ImportLoadPolicy.Synchronous)]
public NetworkManagerContract NetworkManagerContract { get; set; }

プラグインクラス

上記のプロパティの宣言を含む、今回作成する プラグインの元となるコードは次のようになります。これまでの内容を参考にプラグインクラスとレイヤークラスを作成してください。

NetworkSamplePlugin.cs:
namespace NetworkSampleMapApp
{
    using Microsoft.Maps.Core;
    using Microsoft.Maps.Plugins;
    using Microsoft.Maps.Network;
    using Microsoft.Maps.MapControl;
    using System;
    using System.Xml;
    using System.Windows.Controls;

    public class NetworkSamplePlugin : Plugin
    {
        [ImportSingle("Microsoft/MapContract", ImportLoadPolicy.Synchronous)]
        public MapContract DefaultMap { get; set; }

        [ImportSingle("Microsoft/LayerManagerContract", ImportLoadPolicy.Synchronous)]
        public LayerManagerContract LayerManagerContract { get; set; }

        [ImportSingle("Microsoft/PushpinFactoryContract", ImportLoadPolicy.Synchronous)]
        public PushpinFactoryContract PushpinFactoryContract { get; set; }

        [ImportSingle("Microsoft/ModalDialogContract", ImportLoadPolicy.Synchronous)]
        public ModalDialogContract ModalDialogContract { get; set; }

        [ImportSingle("Microsoft/NetworkManagerContract", ImportLoadPolicy.Synchronous)]
        public NetworkManagerContract NetworkManagerContract { get; set; }

        private CityLayer mainLayer;

        public override void Initialize()
        {
            base.Initialize();
            this.mainLayer = new CityLayer(this.Token, this);
        }

        public override void Activate(System.Collections.Generic.IDictionary<string, string> activationParameters)
        {
            if (LayerManagerContract.ContainsLayer(this.mainLayer))
            {
                LayerManagerContract.BringToFront(this.mainLayer);
            }
            else
            {
                LayerManagerContract.AddLayer(this.mainLayer);
            }
        }
    }
}
CityLayer.cs:
namespace NetworkSampleMapApp
{
    using Microsoft.Maps.Core;
    using Microsoft.Maps.Plugins;
    using System;

    public class CityLayer : Layer
    {
        private NetworkSamplePlugin plugin;

        public CityLayer(PluginToken pluginToken, NetworkSamplePlugin plugin)
            : base(pluginToken)
        {
            this.plugin = plugin;
            this.Title = "Network sample";
        }
    }
}

以上のコードを元に、追記していきます。

PrioritizedWebClient

それでは、PrioritizedWebClientクラスを使ってWebアクセスしてみましょう。PrioritizedWebClientクラスは直接インスタンスを生成するのではなく、CreatePrioritizedWebClientメソッドによって取得します。

var client = NetworkManagerContract.CreatePrioritizedWebClient();

Webページを文字列としてダウンロードするには、PrioritizedWebClientクラスのDownloadStringAsyncメソッドを使います。優先順位を指定する場合、Priorityプロパティを使用します。

client.Priority = NetworkPriority.Low;
client.DownloadStringCompleted += new EventHandler<PrioritizedDownloadStringCompletedEventArgs>(client_DownloadStringCompleted);
client.DownloadStringAsync(new Uri("http://gihyo.jp/"));

非同期メソッドのため、ダウンロードが完了したときのメソッドを用意する必要があります。ここでは、前回に紹介したダイアログを使ってダウンロードした文字列を表示するようにしています。

void client_DownloadStringCompleted(object sender, PrioritizedDownloadStringCompletedEventArgs e)
{
    if (e.Error != null)
    {
        ModalDialogContract.Open("Error", new TextBox() { Text = e.Error.ToString() });
    }
    else
    {
        ModalDialogContract.Open("Success", new TextBox() { Text = e.Result });
    }
}

Webページダウンロード部分をメソッド化して、以上のコードをまとめると以下のようになります。

private void DownloadString()
{
    var client = NetworkManagerContract.CreatePrioritizedWebClient();
    client.Priority = NetworkPriority.Low;
    client.DownloadStringCompleted += new EventHandler<PrioritizedDownloadStringCompletedEventArgs>(client_DownloadStringCompleted);
    client.DownloadStringAsync(new Uri("http://gihyo.jp/"));
}

void client_DownloadStringCompleted(object sender, PrioritizedDownloadStringCompletedEventArgs e)
{
    if (e.Error != null)
    {
        ModalDialogContract.Open("Error", new TextBox() { Text = e.Error.ToString() });
    }
    else
    {
        ModalDialogContract.Open("Success", new TextBox() { Text = e.Result });
    }
}

作成したDownloadStringメソッドを、Activateメソッド内で呼んでみましょう

public override void Activate(System.Collections.Generic.IDictionary<string, string> activationParameters)
{
    if (LayerManagerContract.ContainsLayer(this.mainLayer))
    {
        LayerManagerContract.BringToFront(this.mainLayer);
    }
    else
    {
        LayerManagerContract.AddLayer(this.mainLayer);
        DownloadString(); // ← 追加
    }
}

そして、プラグインを実行してみます。結果は図1のようになったのではないでしょうか。

図1 実行結果(エラー)
図1 実行結果(エラー)

エラーが発生し例外の内容が表示されてしまったのではないかと思います。Silverlightアプリケーションでは、アプリケーションとは異なるドメインへ(ここでは、アプリケーションはwww.bing.com、アクセスしようとした先は、gihyo.jp)のアクセスは、セキュリティのため許可がない場合できません。

異なるドメインへのアクセス

異なるドメイン間でSilverlightアプリケーションからWebサービスなどのコンテンツにアクセスできるようにするためには、アクセス先のサーバー側で許可する必要があります。つまりMap Appのプラグインの作りをどうこうして可能なものではありません。接続先のWebサービスのサーバーがアクセス許可されているかの確認、または、自分でMap Appと連携するサービスを作成する場合に、ここでの作業が必要になります。

アクセス許可の方法は、clientaccesspolicy.xmlというファイルをサーバー側に配置します。すべてのクライアント(Silverlightアプリケーション)からアクセスを許可する場合のファイルの内容は次のようになります。

<?xml version="1.0" encoding="utf-8"?>
<access-policy>
  <cross-domain-access>
    <policy>
      <allow-from http-request-headers="*">
        <domain uri="*"/>
      </allow-from>
      <grant-to>
        <resource path="/" include-subpaths="true"/>
      </grant-to>
    </policy>
  </cross-domain-access>
</access-policy>

このclientaccesspolicy.xmlファイルを、クライアントからのアクセス先のドメインのルート、たとえばgihyo.jpの場合は、http://gihyo.jp/clientaccesspolicy.xmlでアクセスできる場所に配置します。実際にhttp://gihyo.jpにあるファイルを操作できませんので、独自ドメインの使用できるレンタルサーバーなどを用意して、ファイルを配置し試してみてください。

Silverlightアプリケーションは、ネットワークアクセスが発生したとき、このファイルの存在を確認し、その内容に基づいて許可されているアクセスを行います。先の例では、gihyo.jpには、ファイル自体が存在しない、またはアクセス許可されていなかったということですね。

残念ながらRSSフィードやWeb APIなどを提供しているサーバーなどでも、すべてのSilverlightアプリケーションに対してアクセス許可がされているサーバーはほとんどないのが現状です。Map Appから公開されているWeb APIなどを利用するには中継するサーバーを自前で用意する必要がありそうです。

さて、Bing MapsのWeb APIのアクセス先であるhttp://dev.virtualearth.netではclientaccesspolicy.xmlがあり、アクセス可能です。先ほどのコードのアクセス先をhttp://dev.virtualearth.netに変更して実行すると、正しくWebページの内容が取得できています図2

図2 実行結果(成功)
図2 実行結果(成功)

Adobe Flashでも同様の仕組みがあり[1]⁠、その方法もSilverlightでは対応しています。その場合、crossdomain.xmlというファイルを使用します。

外部XMLファイルからプッシュピンの追加

第2回では、県庁所在地にプッシュピンを追加するMap App図3を作成しましたが、そのとき県庁所在地の情報はXMLファイルをプラグインのアセンブリにリソースとして埋め込み参照していました。

図3 県庁所在地 Bing Map App
図3 県庁所在地 Bing Map App

今回紹介した方法で、XMLファイルを外部のWebサーバーに配置して、プラグインではファイルをダウンロードしプッシュピンを配置するようにしてみましょう。コードは最初に示したものを編集します。第2回ではパネルも使用していましたが今回は省略しています。

県庁所在地の情報サーバー

Webサーバー側に必要な作業は次の通りです。

  • 独自ドメインのWebサーバーの用意
  • clientaccesspolicy.xmlファイルの配置
  • 県庁所在地情報のXMLファイルの配置

サーバー側はドメインのルートにファイルを配置できる必要があります。そして、clientaccesspolicy.xmlファイルを参照できるようにしておきましょう。

県庁所在地の情報を書いたXMLファイルは以前と同じ内容のものです。下記のような情報を記述したファイルを、同じドメインの任意のディレクトリに配置し、HTTPで参照できるようにしましょう。


<?xml version="1.0" encoding="utf-8" ?>
<Pushpins>
  <Pushpin Name="札幌市" Latitude="43.0646147" Longitude="141.3468074" />
  <Pushpin Name="青森市" Latitude="40.8243077" Longitude="140.7399984" />
  <Pushpin Name="盛岡市" Latitude="39.7036194" Longitude="141.1526839" />
(省略)
  <Pushpin Name="鹿児島市" Latitude="31.5610825" Longitude="130.5577279" />
  <Pushpin Name="那覇市" Latitude="26.2124013" Longitude="127.6809317" />
</Pushpins>

以上でWebサーバー側の準備はできました。今回は静的なXMLファイルにアクセスしていますが、PHPやASP.NETなどを使用してURLにパラメーターを指定して動的な結果を返すことができれば、ユーザーの表示している場所や操作によって表示内容を変えるMap Appも作成可能ですね。

プラグインの変更

プラグイン側を、用意したWeb上のXMLファイルにアクセスするように変更します。

先ほどはPrioritizedWebClient クラスのDownloadStringAsyncメソッドを使用して文字列としてダウンロードしていました。これでも問題ありませんが、次はストリームを取得するOpenReadAsyncメソッドを使って書いてみましょう。

private void OpenRead()
{
    var client = NetworkManagerContract.CreatePrioritizedWebClient();
    client.OpenReadCompleted += new EventHandler<PrioritizedOpenReadCompletedEventArgs>(client_OpenReadCompleted);
    client.OpenReadAsync(new Uri("http://katamari.jp/Cities.xml")); // Web サーバー上の XML ファイルを指定
}

特に難しいところはありませんね。実際にストリームを処理するメソッドは次のようになります。

void client_OpenReadCompleted(object sender, PrioritizedOpenReadCompletedEventArgs e)
{
    if (e.Error != null)
    {
        return;
    }

    using (var reader = XmlReader.Create(e.Result, new XmlReaderSettings()))
    {
        while (reader.Read())
        {
            if (reader.NodeType == XmlNodeType.Element &&
                reader.LocalName == "Pushpin")
            {
                // Entity の作成
                var entity = new Entity();

                // 県庁所在地名
                reader.MoveToAttribute("Name");
                var name = reader.Value;

                // 経緯度
                var location = new Location();
                reader.MoveToAttribute("Latitude");
                location.Latitude = double.Parse(reader.Value);
                reader.MoveToAttribute("Longitude");
                location.Longitude = double.Parse(reader.Value);

                // プッシュピンの追加
                entity.Primitive = this.PushpinFactoryContract.CreateStandardPushpin(location, name[0].ToString());

                // レイヤーに追加
                this.mainLayer.Entities.Add(entity);
            }
        }
    }
}

最後に作成したOpenReadメソッドを呼ぶようにActivateメソッド部分を書きかえて、プラグインを実行してみましょう。結果は図4のようになります。きちんと動作したでしょうか?

図4 県庁所在地の表示
図4 県庁所在地の表示

おわりに

今回はMap AppからWebアクセスについて紹介しました。実際にMap Appを作成する場合、天気予報など時間によって変化する情報を扱う場合は、外部にMap AppがアクセスするWebサーバーを用意して、Webアクセスが必要になってくると思います。また、サードパーティの情報取得に使用するだけでなく、経緯度から住所や画像などを取得するBing Mapsで提供されているWeb APIを利用する場合にもこのようにアクセスして使います。このWeb APIについてはいずれ連載で機会があれば紹介したいと思いますが、次回は、Map Appの予定です。

おすすめ記事

記事・ニュース一覧