UWP の MapControl で Bing Maps の無料トークン廃止に直面し、「いま何をすれば地図機能を止めずに済むのか」「最終的にどこへ着地させるべきか」を、短期・中期・長期の観点で徹底整理しました。現実解としての Azure Maps + WebView2 を中心に、実装の具体手順、API 置き換えマッピング、セキュリティやパフォーマンス設計まで、現場でそのまま使える粒度で解説します。
背景と現状課題の整理
UWP アプリで Windows.UI.Xaml.Controls.Maps.MapControl を利用していると、Bing Maps の無料トークン廃止後に “MapServiceToken not specified” の警告が表示されます。これにより、次の意思決定が必要です。
- 短期:既存の Bing Maps Enterprise 契約があるなら延命運用を図る(ない場合は代替を即検討)。
- 中期:UWP ではネイティブの Azure Maps コントロールが提供されていないため、WebView2 で Azure Maps Web SDK(HTML/JS)をホストする構成に移行する。
- 長期:UWP 自体の将来性を鑑み、WinUI 3(Windows App SDK)への段階移行を進める。地図は引き続き WebView2 でホスト可能。
意思決定の全体像(短期・中期・長期の解決策)
| 期間 | 解決策 | 詳細 |
|---|---|---|
| 短期 | 既存の Bing Maps Enterprise トークンで延命 | 新規顧客向けの Enterprise トークン発行は原則停止。既存契約があれば既定のサポート期間まで利用可能。新規契約がない場合でも、営業経由での 短期延長(1 年単位)が成約できた事例あり(要相談)。 目の前のサービス継続を優先しつつ、すぐに中期計画を始動。 |
| 中期 | UWP + WebView2 で Azure Maps Web SDK をホスト | UWP 用のネイティブ Azure Maps コントロールは未提供。 アプリに WebView2 を組み込み、 ローカル HTML(azuremap.html)で atlas.Map() を初期化、 JS ⇄ C# の双方向通信で UI/データ連携、 REST API(検索・ルート・時空間分析)と連動。 これにより 機能差を最小化しつつリフトが可能。 |
| 長期 | WinUI 3 へ段階移行、地図は継続して WebView2 | UWP の機能追加は縮小傾向。将来的な UI/フレームワークの選好は WinUI 3。 機能単位のライブラリ化 → 新規 UI へ再利用という漸進移行を推奨。移行期間中は「UWP(WebView2+Azure Maps)」と「WinUI 3」 の併走も現実的。 |
UWP で Azure Maps を使う実装手順
前提条件と環境
- OS:Windows 10 バージョン 1809 以降(または Windows 11)
- ランタイム:Microsoft Edge(Chromium)WebView2 Runtime
- SDK:Azure Maps Web SDK(後述のとおりローカル配置を推奨)
- 鍵管理:アプリに鍵をハードコードしない。安全な読み出し(環境変数、保護ストレージ、Key Vault 経由など)を徹底。
手順 1:パッケージ導入(UWP)
Microsoft.UI.Xaml(WinUI 2)を NuGet で追加。
UWP では WinUI 2 のWebView2コントロール(Microsoft.UI.Xaml.Controls.WebView2)を使用します。- WebView2 Runtime の配布形態(Evergreen / Fixed)を設計に合わせて選定。
手順 2:XAML に WebView2 を配置
<Page
x:Class="MyApp.Views.MapPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:muxc="using:Microsoft.UI.Xaml.Controls">
<Grid>
<muxc:WebView2 x:Name="MapWebView"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" />
</Grid>
手順 3:ローカル HTML と SDK を配置
Assets/azuremaps/配下に Azure Maps Web SDK を配置(例:atlas.min.js、atlas.min.css)。Assets/azuremaps/azuremap.htmlを用意。
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/sdk/atlas.min.css">
<style>html,body,#map{height:100%;margin:0;padding:0;}</style>
</head>
<body>
<div id="map"></div>
ポイント:HTML 内では SDK を /sdk/atlas.min.js のような仮想パスで参照しています。UWP から仮想ホストとフォルダのマッピングを行うことで、ローカル資産を HTTP ドメインとして安全に提供し、Service Worker や CORS の制約を回避します。
手順 4:C# 側で仮想ホストを張り、キーを注入
using Microsoft.UI.Xaml.Controls;
using Microsoft.Web.WebView2.Core;
using Windows.Storage;
public sealed partial class MapPage : Page
{
public MapPage()
{
this.InitializeComponent();
this.Loaded += MapPage_Loaded;
}
private async void MapPage_Loaded(object sender, Windows.UI.Xaml.RoutedEventArgs e)
{
await MapWebView.EnsureCoreWebView2Async();
// ローカル Assets を仮想ホストにマップ
// https://local.azuremaps を Assets/azuremaps に対応付け
var assetsFolder = await StorageFolder.GetFolderFromPathAsync(
Windows.ApplicationModel.Package.Current.InstalledLocation.Path + @"\Assets\azuremaps");
MapWebView.CoreWebView2.SetVirtualHostNameToFolderMapping(
"local.azuremaps",
assetsFolder.Path,
CoreWebView2HostResourceAccessKind.Allow);
// /sdk, /azuremap.html のようなパスに対応
MapWebView.CoreWebView2.AddWebResourceRequestedFilter(
"https://local.azuremaps/*",
CoreWebView2WebResourceContext.All);
// HTML をナビゲート
MapWebView.Source = new Uri("https://local.azuremaps/azuremap.html");
// セキュアに取得した Azure Maps キーを注入(例:保護ストレージから読み出し)
var key = await LoadAzureMapsKeyAsync(); // 実装は後述
MapWebView.CoreWebView2.ExecuteScriptAsync($"window.__setAzureMapsKey('{key}')");
// JS→C# のメッセージ受信
MapWebView.CoreWebView2.WebMessageReceived += (s, args) =>
{
var json = args.WebMessageAsJson; // { type:'map.click', position:{lat,lon} }
// 必要に応じて座標を解析して ViewModel へ反映、逆ジオコーディングなどを実行
};
}
private async Task<string> LoadAzureMapsKeyAsync()
{
// 例:アプリ設定や PasswordVault / DataProtectionProvider を利用して安全に復号
// 実際の実装ではハードコードしないこと
return await Task.FromResult(AppSecrets.Get("AZURE_MAPS_KEY"));
}
}
手順 5:地物描画・イベントの置き換え
MapControl のプッシュピンやポリライン、ポリゴンは、Azure Maps では DataSource+Layer(Symbol/Line/Polygon)で表現します。
// JS(地図準備完了後)
const ds = new atlas.source.DataSource();
window.__map.sources.add(ds);
// プッシュピン
ds.add(new atlas.data.Point([lng, lat]));
// ライン
ds.add(new atlas.data.LineString([[lng1,lat1],[lng2,lat2],[lng3,lat3]]));
// ポリゴン
ds.add(new atlas.data.Polygon([[[lngA,latA],[lngB,latB],[lngC,latC],[lngA,latA]]]));
// レイヤ
window.__map.layers.add(new atlas.layer.SymbolLayer(ds));
window.__map.layers.add(new atlas.layer.LineLayer(ds, null, { strokeWidth: 3 }));
window.__map.layers.add(new atlas.layer.PolygonLayer(ds, null, { fillOpacity: 0.3 }));
API 置き換えマッピング(Bing Maps → Azure Maps)
| 機能(Bing Maps / MapControl) | Azure Maps(Web SDK) | 移行ポイント |
|---|---|---|
| MapIcon(Pushpin) | DataSource + SymbolLayer | プロパティは properties.title など GeoJSON 属性へ移す。カスタム画像は imageSprite または IconOptions。 |
| MapPolyline | DataSource + LineLayer | スタイルは strokeColor、strokeWidth 相当をレイヤのオプションへ。 |
| MapPolygon | DataSource + PolygonLayer | 穴(interior rings)は座標配列の二次元目で表現。 |
| カスタムタイル(TileSource) | TileLayer | URL テンプレート({x}/{y}/{z})をそのまま指定可。サブドメイン配列にも対応。 |
| タップ・クリックイベント | map.events.add('click', ...) | 地物単位なら atlas.io.feature のヒットテストか、レイヤーイベントを使用。 |
| 位置検索・逆ジオコーディング | Search REST API | /search/address、/search/address/reverse を HttpClient から呼び出す。 |
| ルーティング | Route REST API | 交通状況・最適化・マトリクスなどはエンドポイントで制御。 |
| ジオフェンス | Spatial / Geofence | ポリゴン集合と現在地の包含判定をサービス側またはクライアントで。 |
REST API 連携(UWP/C#)の実装例
Azure Maps の REST は標準の HttpClient で呼び出せます。以下は住所検索の例です。
using System.Net.Http;
using System.Text.Json;
public async Task> SearchAddressAsync(string query, string subscriptionKey)
{
using var http = new HttpClient();
var url = $"[https://atlas.microsoft.com/search/address/json](https://atlas.microsoft.com/search/address/json)" +
$"?api-version=1.0&language=ja-JP&limit=10" +
$"&subscription-key={Uri.EscapeDataString(subscriptionKey)}" +
$"&query={Uri.EscapeDataString(query)}";
using var res = await http.GetAsync(url);
res.EnsureSuccessStatusCode();
var json = await res.Content.ReadAsStringAsync();
using var doc = JsonDocument.Parse(json);
var list = new List<Place>();
foreach (var item in doc.RootElement.GetProperty("results").EnumerateArray())
{
var poi = item.TryGetProperty("poi", out var poiEl) ? poiEl : default;
var pos = item.GetProperty("position");
list.Add(new Place{
Name = poi.ValueKind != JsonValueKind.Undefined ? poi.GetProperty("name").GetString() : item.GetProperty("address").GetProperty("freeformAddress").GetString(),
Latitude = pos.GetProperty("lat").GetDouble(),
Longitude = pos.GetProperty("lon").GetDouble()
});
}
return list;
}
public class Place {
public string Name { get; set; }
public double Latitude { get; set; }
public double Longitude { get; set; }
}
キー管理とセキュリティ設計
- ハードコード禁止:リバースエンジニアリングで容易に抽出されます。
- 安全なロード:アプリ初回起動時に暗号化された設定から復号、もしくは確立済みチャネルで短寿命トークンを配布。
- 更新容易性:サーバー経由のローテーションを想定し、クライアントは再起動なしで差し替え可能に。
- 権限分離:開発・検証・本番環境でキーを分離。テナントの RBAC とアラートで監視。
パフォーマンス最適化とオフライン対応
- WebView2 でも GPU アクセラレーションは有効:レイヤ数・地物数が多い場合はクラスタリングやタイル化を検討。
- HTTP キャッシュ:仮想ホスト(例:
https://local.azuremaps/)配下で Service Worker を有効化し、SDK・スタイル・画像をキャッシュ。
動的タイルや検索レスポンスの 完全オフラインは原則不可。要件次第でタイルサーバーや独自キャッシュ層を設計。 - ストリーム描画:大量フィーチャは GeoJSON 一括より MVT(ベクトルタイル)を推奨。段階ズームでレイヤ切替。
UWP → WinUI 3 段階移行のすすめ
アプリの将来性を確保するため、UI・シェルを WinUI 3 に移し替える計画を並走させます。地図は当面 WebView2 を継続利用できます。以下の順序が実践的です。
- 機能単位で分割:ドメインサービス/ビューモデルを .NET Standard / .NET 8 ライブラリ化。
- 共通コンポーネント化:カスタムコントロールやビヘイビアを移植・再設計。
- デザイン刷新:
XamlControlsGalleryを参考に、入力サイズ・アクセシビリティ指標を達成。 - 共通 CI:UWP・WinUI 3 の両ビルドを同一パイプラインで検証。UI テストで座標・レイヤ表示の差異を検出。
サンプル:Bing MapControl API 呼び出しの「置換」チェックリスト
| 旧 API(Bing Maps) | 新 API(Azure Maps) | 検討事項 | 対応状況 |
|---|---|---|---|
MapControl.Center/ZoomLevel | map.setCamera | 初期表示とカメラアニメーションの互換 | — |
MapIcon | SymbolLayer | 吹き出し UI は Popup/HTML テンプレで代替 | — |
MapPolyline | LineLayer | スタイル・クリックヒットの調整 | — |
MapPolygon | PolygonLayer | 穴ポリゴン・塗りと枠の同時制御 | — |
| 交通情報(Traffic) | レイヤ/データ(該当機能) | 可視化と凡例設計、配色ガイドライン | — |
| 検索/逆ジオ/ルート | REST API | レートリミット・課金単位・リトライ | — |
品質保証(QA)と運用のベストプラクティス
- 座標系・測地系の差異検証:WGS84 基準での表示誤差がないか、既存実装とスクリーンショット差分を取る。
- テレメトリ:REST 呼び出しの成功率・レイテンシ・ズーム別フィーチャ数を可視化。閾値アラートを設定。
- フェイルセーフ:Azure Maps 障害時に「簡易タイルのみ」「直近キャッシュのみ」など段階的劣化で運用継続。
- キー流出対策:利用域制限・ローテーション・利用状況の日次監査。
よくある設計質問(FAQ)
Q. 新規に Bing Maps の Enterprise トークンは取得できますか?
A. 新規発行は基本停止と捉えるのが現実的です。既存契約があれば契約期間内は利用可能です。短期延長の可否は営業窓口に相談が必要です。
Q. UWP で Azure Maps をネイティブに扱う方法はありませんか?
A. 現時点の実用解は WebView2 で Web SDK をホストする方式です。レイヤ・イベント・REST 連携を組み合わせれば、多くのユースケースは移行できます。
Q. オフライン対応は可能ですか?
A. 完全オフラインは難度が高いです。要件を細分化し、タイル・スタイル・スプライトなど静的資産のキャッシュ、ルート/検索は「直近結果の再利用」や「簡易ローカル検索」で退避する戦略を取ります。業務要件によっては独自タイルサーバーや端末内データベース連携が必要です。
Q. 長期的には何を目標にすべきですか?
A. UI フレームワークの将来性・メンテ性を踏まえ、WinUI 3 へ段階移行しつつ、地図表示は WebView2 構成を継続するアーキテクチャが堅実です。
実プロジェクトでの進め方(90 日プラン例)
| 期間 | 主要タスク | 成果物 | 判断ゲート |
|---|---|---|---|
| Day 1–15 | Bing 依存点の棚卸し、Azure Maps プロトタイプ(ピン/線/検索) | PoC、差分レポート | WebView2 方針の確定、セキュリティ方式の合意 |
| Day 16–45 | API 置換の実装、REST ラッパー、UI の最小改修 | ベータ版、性能測定結果 | レイヤ構成・キャッシュ方式の確定 |
| Day 46–90 | テレメトリ・アラート、障害時劣化設計、運用 Runbook | リリース版、運用手順書 | 次期(WinUI 3)ロードマップ策定 |
実装時の技術メモ(重要ポイントの再掲)
- Azure Maps キーの安全管理:サブスクリプションから取得。アプリには埋め込まず、暗号化格納や安全なチャネルで注入。
- WebView2 の OS 前提:Windows 10 バージョン 1809 以降(または Windows 11)。
- パフォーマンス/オフライン:GPU アクセラレーションは有効。完全オフラインは不可。必要なら独自キャッシュ・タイル層を設計。
- WinUI 3 移行工数:UI 再設計は
XamlControlsGalleryを参考に。共通ビヘイビアをコンポーネント化し、UWP/WinUI 3 の両ビルドを CI で併走させるとリスクが下がります。
サンプル:仮想ホスト配下での Service Worker(任意)
SDK・画像・スタイルのキャッシュを強める場合の例です。Assets/azuremaps/sw.js を登録し、HTML から有効化します。
// azuremap.html 内
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js').catch(console.error);
}
// sw.js(簡易例)
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open('azuremaps-v1').then(cache => cache.addAll([
'/azuremap.html',
'/sdk/atlas.min.js',
'/sdk/atlas.min.css'
]))
);
});
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then(resp => resp || fetch(event.request))
);
});
注意:仮想ホスト(例:https://local.azuremaps/)で配信していること、かつ HTTPS スキームであることが前提です。
トラブルシューティング
- 空白画面になる:
EnsureCoreWebView2Asyncの完了前にナビゲートしていないか確認。仮想ホストマッピングのパスも要確認。 - “MapServiceToken not specified” が残る:旧 MapControl の初期化コードが残っていないか。Azure Maps のキー注入タイミングを HTML 側で受け取れているか検証。
- クリック座標が取得できない:イベントは
map.events.add('click')を使用。e.positionの null チェックを忘れずに。 - REST の 401/403:購買環境のキーを使っているか、参照リージョンの整合、レート制御・時刻ずれ(署名方式時)を確認。
まとめと推奨アーキテクチャ
結論:新規の Bing Maps Enterprise トークンは基本的に入手困難であり、既存契約がある場合のみ延命が現実的です。UWP で地図機能を継続する最短ルートは Azure Maps Web SDK を WebView2 でホストする方式です。
将来の保守性・採用技術の持続性を考慮すれば、WinUI 3 への段階移行を今すぐ計画・着手し、移行期間中は「UWP+WebView2(Azure Maps)」と「WinUI 3」 を併走させるのがリスク最小です。
本記事のサンプル(仮想ホスト・キー注入・API 置換マッピング)を雛形として、最初の 2 週間で PoC、90 日以内の本番切り替えを目標にするのが現実的なロードマップです。
付録:移行判断のクイックチェック
| 設問 | Yes の場合 | No の場合 |
|---|---|---|
| Bing Enterprise 契約を保持しているか? | 短期は延命、並行で Azure Maps 移行 PoC を直ちに開始。 | 即時に Azure Maps(または他サービス)へ移行計画を立案。 |
| UWP の継続運用が必須か? | WebView2 で Azure Maps をホスト。WinUI 3 併走計画を作成。 | 新規は WinUI 3 を第一候補に。UWP は保守のみに。 |
| 完全オフラインが必須か? | 要件分解の上、カスタムタイル・ローカル DB・同期戦略を設計。 | オンライン前提でキャッシュ最適化(Service Worker/HTTP キャッシュ)。 |
実務に効く Tips(覚えておきたい要点)
- HTML と SDK はローカル配布が安全・高速。仮想ホストで Service Worker を使えるようにする。
- 大量地物はサーバーで前処理(タイル化)し、クライアント負荷を軽減。
- REST はリトライ・レート制御・タイムアウトを必ず実装。ユーザー操作にフィードバック(スピナー/再試行)。
- UI はズーム段階でレイヤ切替(小縮尺では概略、多縮尺で詳細)。
- 障害時は段階的劣化(最後の正常タイル・最低限ルート等)で UX を守る。

コメント