Bing Maps を使って住所から地図を表示する (SharePoint サンドボックス ソリューションのネットワーク接続)


※ SharePoint 2013 以降では、サンボボックス ソリューションは非推奨です。SharePoint アドイン (SharePoint Add-ins) による開発手法を使用してください。

環境 : SharePoint 2010, Visual Studio 2010

裏 10 行でズバリ ! サンドボックス ソリューション 

本シリーズは、「10 行でズバリ ! SharePoint 2010 開発」を理解された開発者向けに、さらに踏み込んだ内容を記載しています。(ちなみに、10 行にはおさまっていません。。。)

こんにちは。

ここでは、Bing Map を例に、ネットワーク接続が必要とされる SharePoint サンドボックス ソリューションの構築方法を理解します。(この投稿のさいごに、Bing Map のようなサービスの構築方法と、サンドボックス ソリューションにおけるネットワーク接続についてまとめます。)

作成するアプリケーションと考慮点

まず、「10 行でズバリ!! SharePoint のサンドボックス ソリューションの作成」 でも説明しているように、SharePoint のサンドボックス ソリューションでは、サーバー側のサンドボックス化されたコード内から外部リソースへネットワーク接続 (Networking) をおこなうことはできません。(つまり、サーバー側の処理で、twitter などに接続することもできません。) このため、Bing Map への接続など、ネットワーク接続を伴う処理は、クライアント側の処理 (JavaScript, Silverlight など) を使用することになります。
Bing Map が提供している AJAX API などを使うと、こうしたネットワーク接続の処理をラッピングしてくれるため、プログラミングの際に内部の仕組みを理解しておく必要はありません。しかし、今回は勉強のため、敢えて、Bing Map の REST API が必要とされる場合を想定し、外部リソースへの接続方法 (回避方法) を考察したいと思います。

今回作成するアプリケーションは、テキストボックスに住所 (番地など) の情報を入力すると、その住所の位置 (座標) 情報を取得して、その位置の地図を表示する簡単な Web パーツをサンドボックス ソリューションとして作成します。
下図のような Web パーツ (WebParts) です。

注記 : 今回は、日本語環境 で使用することを想定して、アプリケーションを構築します。

このサンプルでは、地図の表示に際して、Geocode (住所の情報を解析して位置情報を取得する処理) が必要となるため、この際に Bing Maps の REST API を使用します。
この Geocode の REST API は、通常、以下のような仕様で使用します。(下記で、Bing Map の access key は、アプリケーションごとに皆さんご自身で発行してください。無料です。)

http://dev.virtualearth.net/REST/v1/Locations?countryRegion=JP&adminDistrict=[都道府県名の URL エンコード文字列]&locality=[市町村名などの URL エンコード文字列]&addressLine=[番地名などの URL エンコード文字列]&key=[Bing Maps の access key]&c=ja-jp

まず単純に、JavaScript の XMLHttpRequest などを使って上記の REST サービスを呼び出す場合を考えてみましょう。
この場合、ご存じの通り、クロスドメイン (もしくは、クロスサイト) へのネットワーク接続となり、現実的ではありません。(そもそも、IE などでクロスドメインの呼び出しをおこなうと、エラーや警告が表示されることでしょう。)
さて、困りました。

しかし、Bing Maps では、こうした接続のために、以下で説明するような仕組みが提供されています。この手法は、他のネットワーク接続が必要なアプリケーションでも応用できるので、是非、その仕組みを理解しておきましょう。

アプリケーションの実装 (構築)

早速、理屈を抜きにして、サンプル アプリケーション (Web パーツ) の実装を見て行きましょう。

では、いつものように、Visual Studio 2010 を起動して、[空の SharePoint プロジェクト] を新規作成します。(この際、[サンドボックス ソリューションとして配置する] にチェックを付けます。)

ソリューション エクスプローラーでプロジェクト名を右クリックして、[追加] – [新しい項目] メニューを選択して [Web パーツ] を追加します。

Bing Maps を使用するために、地図のフレームと、住所を入力するためのテキストボックスを表示します。
まずは、これを HTML で記述した場合のコードを以下に記載します。

. . .

<body onload="GetMap();">
    <script type="text/javascript" src="http://ecn.dev.virtualearth.net/MapControl/mapcontrol.ashx?v=6.3c"></script>
    <script type="text/javascript">
    var map = null;

    function GetMap() {
        map = new VEMap('myMap');
        map.LoadMap();
    }
    </script>

    <div id="myMap" style="position:relative; width:400px; height:400px;"></div>

    <table>
    <tr>
        <td>都道府県</td>
        <td><input type="text" id="prefTxt"/></td>
    </tr>
    <tr>
        <td>市区町村</td>
        <td><input type="text" id="cityTxt"/></td>
    </tr>
    <tr>
        <td>残りの住所</td>
        <td><input type="text" id="addressTxt"/></td>
    </tr>
    </table>
</body>

今回は Web パーツですので、上記の内容を Web パーツのプログラム コードとして実装します。
この実装をおこなったコードは、以下になります。(実は、この段階で、既に JavaScript 内部で Bing Maps へのネットワーク接続がおこなわれますが、ここでは、この内部動作の説明は省略します。)

補足 : Bing Maps で SSL (https) を使用する場合は、アドレスを下記の太字 (赤字) の通り設定してください。
<script type=”text/javascript” src=”https://ecn.dev.virtualearth.net/MapControl/mapcontrol.ashx?v=6.3c&s=1“></script>
特に Office 365 では、既定で SSL (https) を使用しており、SSL (https) を使用したサイトから http のリソースに接続すると、ブラウザ (IE) が警告を表示します。
このため、以降のサンプル コードでは、すべて、この SSL を使用した接続を使用します。

. . .

using Microsoft.SharePoint.Utilities;
. . .

protected override void CreateChildControls()
{
    base.CreateChildControls();
    this.Controls.Add(new LiteralControl(GetHTMLString()));
}

private string GetHTMLString()
{
    string htmlText = @"
<script type=""text/javascript"" src="https://ecn.dev.virtualearth.net/MapControl/mapcontrol.ashx?v=6.3c&s=1"></script>
<script type=""text/javascript"">
_spBodyOnLoadFunctionNames.push(""GetMap"");

var map = null;

function GetMap() {
  map = new VEMap('myMap');
  map.LoadMap();
}
</script>

<div id=""myMap"" style=""position:relative; width:400px; height:400px;""></div>

<table>
<tr>
  <td>都道府県</td>
  <td><input type=""text"" id=""prefTxt""/></td>
</tr>
<tr>
  <td>市区町村</td>
  <td><input type=""text"" id=""cityTxt""/></td>
</tr>
<tr>
  <td>残りの住所</td>
  <td><input type=""text"" id=""addressTxt""/></td>
</tr>
</table>
";

    return htmlText;
}

つぎに、上記で説明した GeoCode の REST API を使用して、テキストボックスのフォーカスが移るたびに地図を更新します。(ここがポイントです !)

実は、Bing Maps の REST API では、下記 (クエリ文字列の jsonp=callbackFunc) のようにコールバック関数を指定して呼び出すことで、ロケーション (位置) 情報の取得後の処理を この関数 (callbackFunc) に記述することができます。
まずは、これを HTML のコードで見てみましょう。以下の HTML のサンプルの太字部分 (追加したコード) を見てください。(先ほども述べたように、Bing Map のアクセス キーは、皆さん自身で、ちゃんと発行してください。)

. . .

<body onload="GetMap();">
    <script type="text/javascript" src="https://ecn.dev.virtualearth.net/MapControl/mapcontrol.ashx?v=6.3c&s=1"></script>
    <script type="text/javascript">
    var map = null;

    function GetMap() {
        map = new VEMap('myMap');
        map.LoadMap();
    }

    function execGeocode() {
        var requestUri = 'https://dev.virtualearth.net/REST/v1/Locations?output=json&jsonp=callbackFunc&countryRegion=JP&adminDistrict=' + encodeURI(document.getElementById('prefTxt').value) + '&locality=' + encodeURI(document.getElementById('cityTxt').value) + '&addressLine=' + encodeURI(document.getElementById('addressTxt').value) + '&key=Ao5dhnS2_AxbDpUtrmNVbpHJ6si_MLFlg5ne7lsI0K7DjjB94NU7tCufe5B4HV_o&c=ja-jp';
        appendRestReference(requestUri);
    }

    function callbackFunc(res) {
        if (res &&
            res.resourceSets &&
            res.resourceSets.length > 0 &&
            res.resourceSets[0].resources &&
            res.resourceSets[0].resources.length > 0) {

            // 返された bbox (位置情報) の内容から地図を更新
            var bbox = res.resourceSets[0].resources[0].bbox;
            map.SetMapView(new VELatLongRectangle(new VELatLong(bbox[0], bbox[1]), new VELatLong(bbox[2], bbox[3])));

            // Bing Map にプッシュピンを設定
            var lat = new VELatLong(res.resourceSets[0].resources[0].point.coordinates[0], res.resourceSets[0].resources[0].point.coordinates[1]);
            var pin = new VEShape(VEShapeType.Pushpin, lat);
            map.AddShape(pin);
        }
    }

    function appendRestReference(request) {
        var scriptElement = document.createElement('script');
        scriptElement.setAttribute('type', 'text/javascript');
        scriptElement.setAttribute('src', request);
        document.body.appendChild(scriptElement);
    }
    </script>

    <div id="myMap" style="position:relative; width:400px; height:400px;"></div>

    <table>
    <tr>
        <td>都道府県</td>
        <td><input type="text" id="prefTxt" onblur="execGeocode();"/></td>
    </tr>
    <tr>
        <td>市区町村</td>
        <td><input type="text" id="cityTxt" onblur="execGeocode();"/></td>
    </tr>
    <tr>
        <td>残りの住所</td>
        <td><input type="text" id="addressTxt" onblur="execGeocode();"/></td>
    </tr>
    </table>
</body>

通常、Bing Map の REST API は、その名の通り、Json フォーマットなどの「データ」そのものを返しますが、実は、上記のようにコールバック関数 (jsonp) をクエリ文字列に指定すると、そのコールバック関数を呼び出す JavaScript のコードが返ってきます。(つまり、これは、厳密な RESTful サービスではありません。) 上記の場合は、動的にスクリプト ソース (script タグ) を HTML に挿入することで、結果の値が格納された JavaScript の処理が動的に挿入され、その結果、指定したコールバック関数が (その結果の値を引数として) 呼び出されます。
そして、こうした script タグや img タグは、クロスドメインを制約を受けずに挿入可能であるため、上記の処理では、クロスドメインの問題も回避できているのがわかります。

この手法は、一見、クロスドメインの制約を回避したセキュリティ ホールのように思われるかもしれませんが、そうではありません。公開するサービス側で、こうした呼び出しの手段を明示的に公開していない限り、クライアント側の判断だけで勝手にこうした呼び出しをおこなうことはできません。(つまり、サービス側で「許可」されていない場合は、こうした処理をおこなうことはできません。) 今回の Bing Maps の例では、Bing Maps 自身が、こうした呼び出しのための専用の URI を公開しているからこそ、こうした呼び出しが可能なのです。

上記の HTML の処理を Web パーツとして作成すると、以下の通りになります。

. . .

using Microsoft.SharePoint.Utilities;
. . .

protected override void CreateChildControls()
{
    base.CreateChildControls();
    this.Controls.Add(new LiteralControl(GetHTMLString()));
}

private string GetHTMLString()
{
    string htmlText = @"
<script type=""text/javascript"" src=""https://ecn.dev.virtualearth.net/MapControl/mapcontrol.ashx?v=6.3c&s=1""></script>
<script type=""text/javascript"">
_spBodyOnLoadFunctionNames.push(""GetMap"");

var map = null;

function GetMap() {
  map = new VEMap('myMap');
  map.LoadMap();
}

function execGeocode() {
  var requestUri = 'https://dev.virtualearth.net/REST/v1/Locations?output=json&jsonp=callbackFunc&countryRegion=JP&adminDistrict=' + encodeURI(document.getElementById('prefTxt').value) + '&locality=' + encodeURI(document.getElementById('cityTxt').value) + '&addressLine=' + encodeURI(document.getElementById('addressTxt').value) + '&key=Ao5dhnS2_AxbDpUtrmNVbpHJ6si_MLFlg5ne7lsI0K7DjjB94NU7tCufe5B4HV_o&c=ja-jp';
  appendRestReference(requestUri);
}

function callbackFunc(res) {
  if (res &&
    res.resourceSets &&
    res.resourceSets.length > 0 &&
    res.resourceSets[0].resources &&
    res.resourceSets[0].resources.length > 0) {

    // 返された bbox (位置情報) の内容から地図を更新
    var bbox = res.resourceSets[0].resources[0].bbox;
    map.SetMapView(new VELatLongRectangle(new VELatLong(bbox[0], bbox[1]), new VELatLong(bbox[2], bbox[3])));

    // Bing Map にプッシュピンを設定
    var lat = new VELatLong(res.resourceSets[0].resources[0].point.coordinates[0], res.resourceSets[0].resources[0].point.coordinates[1]);
    var pin = new VEShape(VEShapeType.Pushpin, lat);
    map.AddShape(pin);
  }
}

function appendRestReference(request) {
  var scriptElement = document.createElement('script');
  scriptElement.setAttribute('type', 'text/javascript');
  scriptElement.setAttribute('src', request);
  document.body.appendChild(scriptElement);
}
</script>

<div id=""myMap"" style=""position:relative; width:400px; height:400px;""></div>

<table>
<tr>
  <td>都道府県</td>
  <td><input type=""text"" id=""prefTxt"" onblur=""execGeocode();""/></td>
</tr>
<tr>
  <td>市区町村</td>
  <td><input type=""text"" id=""cityTxt"" onblur=""execGeocode();""/></td>
</tr>
<tr>
  <td>残りの住所</td>
  <td><input type=""text"" id=""addressTxt"" onblur=""execGeocode();""/></td>
</tr>
</table>
";

    return htmlText;
}

 

追記 : クロス ドメインに対応したサービス (JSONP サービス) の作成方法

上記のように、callback を指定することで JavaScript の処理を返すサービスは、JSON with Padding (JSONP) という仕様に準拠しています。

.NET 4 の WCF REST サービスを使って、こうした JSONP に対応したカスタム サービスを構築することができます。WCF の REST サービスを構築し、web.config に下記太字の通り記述します。(WCF を使った REST サービスの構築方法については、こちら を参照してください。)
なお、このサービスを呼び出すクライアント側は、上記の jsonp の代わりに、http://…?callback=callbackFunc のように、クエリ文字列として callback を指定します。

. . .

<system.serviceModel>
  <behaviors>
    <endpointBehaviors>
      <behavior name="myEndpointBehavior">
        <webHttp />
      </behavior>
    </endpointBehaviors>
  </behaviors>
  <bindings>
    <webHttpBinding>
      <binding name="myBindingConfig"
                crossDomainScriptAccessEnabled="true" />
    </webHttpBinding>
  </bindings>
  <services>
    <service name="WcfService1.OrderService">
      <endpoint address="" binding="webHttpBinding"
                bindingConfiguration="myBindingConfig"
                contract="WcfService1.IOrderService"
                behaviorConfiguration="myEndpointBehavior"/>
    </service>
  </services>
</system.serviceModel>
. . .

また、WCF Data Services (旧 ADO.NET Data Services) で JSONP の仕様に準拠したサービスを実装するには、プロジェクトに、下記のリンクで提供されているクラスを追加します。 (さらに、サービス クラスのクラス属性として、このビヘイビアを追加します。)
ただし、この WCF Data Services の場合は、http://…?$format=json&$callback=callbackFunc のように、$ を使って呼び出すので注意してください。

MSDN Archive : JSONP and URL-controlled format support for ADO.NET Data Services

http://archive.msdn.microsoft.com/DataServicesJSONP

まとめ (サンドボックス ソリューションにおけるネットワーク接続)

このように、SharePoint のサンドボックス ソリューションでネットワーク リソースに接続する場合、サーバー サイドからの接続は不可能なため、以下の回避方法が考えられます。

  • コールバックの呼び出しが可能なプロキシ用のライブラリなどを接続先のサーバー上に構成し、これを経由してリモート接続をおこなう (今回使用した JSONP による方法です)
  • この他に、接続先のポリシー (crossdomain.xml など) を構成して、Silverlight アプリケーションでリモート接続をおこなうこともできます

繰り返しになりますが、いずれの方法も、接続先のサイトで必要な設定 (すなわち「許可」) が必要になります。

この方法を使って、外部リソースへのネットワーク接続をクライアント サイドで構築し、SharePoint 2010 のクライアント オブジェクト モデル (Clinet OM) を組み合わせて、SharePoint 上のデータと連携させることも可能です。

 

2011/06/24 追記 : クロス ドメイン接続の際の留意点について、以下にまとめました。

https://blogs.msdn.microsoft.com/tsmatsuz/2011/06/24/jsonp-cross-domain/

 

Comments (0)

Skip to main content