ASP.NET WebForms の既存システムに CesiumJS を導入したところ、<asp:ScriptManager> を配置しているページだけマップにマウスを乗せた瞬間にクラッシュし、「Sys.ArgumentTypeException: Object of type ‘Number’ cannot be converted to type ‘String’」という謎のエラーで止まってしまう——そんな状況に心当たりはないでしょうか。本記事では、この現象の正体と原因、そして安全に回避するための具体的な実装手順を、再現条件からソースコードレベルまで丁寧に解説します。
現象の整理:ScriptManager があるページでだけ Cesium マップが止まる
まずは、問題となっている状況を整理します。典型的な環境は次のような構成です。
| 項目 | 値 |
|---|---|
| .NET Framework | 4.7.2 |
| アプリケーション種類 | ASP.NET WebForms(.aspx) |
| JavaScript マップライブラリ | CesiumJS 1.131 |
| 問題の有無 | <asp:ScriptManager> があるときだけ発生 |
この環境で、次のようなページを想像してください。
- WebForms ページ上に ScriptManager を配置している
- 同じページで CesiumJS を読み込み、マップを表示している
- ページ表示直後はマップが描画されている
- マウスカーソルをマップの上に乗せた瞬間に JavaScript エラーが発生し、以降の描画が止まる
ブラウザの開発者ツール(F12)でコンソールを見ると、次のエラーが出力されます。
Sys.ArgumentTypeException: Object of type 'Number' cannot be converted to type 'String'
しかし、不思議なことに <asp:ScriptManager runat=”server” /> の行をコメントアウト/削除すると、このエラーはきれいに消え、Cesium のマップは問題なく操作できます。同じ JavaScript、同じ HTML で動かしているのに、ScriptManager の有無だけで動いたり止まったりするわけです。
ポイントはこの「ScriptManager があるときだけ」という条件です。原因は、ScriptManager が裏側で読み込んでいる Microsoft Ajax Library にあります。
ScriptManager と Microsoft Ajax Library の役割
WebForms の <asp:ScriptManager> は、UpdatePanel や PageMethods などの ASP.NET AJAX 機能を提供するための中核コンポーネントです。ページに ScriptManager を追加すると、次のような処理が自動で行われます。
- MicrosoftAjax.js などの「Microsoft Ajax Library」が自動で読み込まれる
- Sys 名前空間(Sys.Application など)や PageRequestManager が初期化される
- UpdatePanel の部分更新や PageMethods の呼び出しなど、ASP.NET AJAX の仕組みが使えるようになる
便利な仕組みである一方で、Microsoft Ajax Library は古いブラウザにも対応するために、JavaScript の組み込みオブジェクトを積極的に拡張します。具体的には、次のようなことを行います。
- ブラウザに存在しないメソッドを polyfill(ポリフィル)として追加する
- 型チェックを厳格に行うヘルパー(Function._validateParams など)を通してメソッドをラップする
問題の核心は、この「組み込みメソッドの polyfill」の一つ、String.prototype.startsWith にあります。
String.prototype.startsWith の挙動差がエラーの引き金
近年のブラウザには、標準として String.prototype.startsWith が実装されています。通常、このメソッドは以下のように振る舞います。
- 第1引数は暗黙に
String()で変換される - 数値やその他の型を渡しても、とくにエラーは出さず文字列に変換して評価する
例えば、次のようなコードはネイティブ実装ではエラーになりません。
"123".startsWith(1); // true ("1" に暗黙変換される)
"abc".startsWith(123); // false
ところが ScriptManager が読み込む Microsoft Ajax の世界では事情が異なります。Microsoft Ajax は startsWith が存在しない/挙動が異なるブラウザ向けに、自前の実装を次のような流れで定義します。
String.prototype.startsWithが存在しなければ polyfill を定義する- その内部で、Function._validateParams による型チェックを行う
- 「第1引数は String 型でなければならない」といった制約を設ける
- 型が合わない場合は
Sys.ArgumentTypeExceptionをスローする
この結果、Microsoft Ajax の世界では同じようなコードが次のように扱われます。
| コード | ブラウザ標準の startsWith | Microsoft Ajax の startsWith |
|---|---|---|
"123".startsWith(1) | エラーにならず、true を返す | Sys.ArgumentTypeException をスロー |
"abc".startsWith(123) | エラーにならず、false を返す | Sys.ArgumentTypeException をスロー |
この「数値を渡すと例外になる挙動」が、CesiumJS の内部実装と噛み合わず、マウス操作時のクラッシュに繋がります。
CesiumJS のマウスイベントと startsWith の組み合わせ
CesiumJS はマウス操作やピック処理などの内部で、イベント名や識別子の判定に startsWith を利用する箇所があります。その過程で、必ずしも引数が文字列とは限らないパターンが存在します。
あくまでイメージですが、次のようなコードと似た動きをしていると考えると分かりやすいでしょう。
var prefix = 0; // 数値(ボタン番号など)
var type = "0_mousemove"; // 文字列
// ネイティブ実装なら true になるだけだが…
if (type.startsWith(prefix)) {
// 何らかの処理
}
ネイティブ実装であれば prefix が暗黙に "0" に変換されるため、単に true / false が返るだけです。しかし ScriptManager が読み込まれている環境では、ここで Sys.ArgumentTypeException がスローされてしまいます。
つまり、
- ScriptManager が読み込まれる
- Microsoft Ajax が
startsWithを厳格な実装で上書きする - CesiumJS のマウスイベント処理で非文字列の引数付き
startsWithが呼ばれる Sys.ArgumentTypeException: Object of type 'Number' cannot be converted to type 'String'が発生
という流れでクラッシュが起きている、という構図です。
解決策の全体像:3 つのアプローチ
この問題に対しては、大きく分けて次の 3 つのアプローチが考えられます。
| 解決方法 | 内容 | 長所 | 短所 |
|---|---|---|---|
| 解決策A 寛容な startsWith へ差し替え(推奨) | ScriptManager によって上書きされた厳格版 String.prototype.startsWith を、ブラウザ標準に近い「何でも文字列に変換して判定する」実装に置き換える。 | ScriptManager を残せる UpdatePanel や PageMethods など既存の ASP.NET AJAX 機能に手を入れなくてよい 影響範囲を限定しやすい | 「Microsoft Ajax の厳格な startsWith に依存しているスクリプト」がある場合、その挙動が変わる可能性 グローバルな prototype の書き換えである点に要注意 |
| 解決策B ScriptManager を外す | ページから <asp:ScriptManager> を削除し、Microsoft Ajax そのものを読み込まないようにする。 | 原因となるライブラリ自体を排除するので最もシンプル JavaScript 側での小細工が不要 | UpdatePanel や PageMethods など ASP.NET AJAX に依存している機能が一切使えなくなる 既存システムへの影響が大きいケースが多い |
| 解決策C バージョンアップ・分離構成 | Microsoft Ajax のバージョンアップや構成変更で厳格チェックを廃止する もしくは Cesium 部分を別 aspx / iframe に切り出し、ScriptManager とは同居させない | 大規模プロジェクトでは長期的な根本対策になり得る マップ周りの JavaScript をシンプルな構成にできる | 設計変更・テスト工数が大きくなりがち 既存のマスターページ構成やルーティングの影響を受ける |
以下では、現実的に採用しやすい「解決策A(startsWith の差し替え)」を中心に、具体的な実装例や注意点を解説し、その後に B / C を補足します。
解決策A:寛容な startsWith へ差し替える(推奨)
最も現場で採用しやすいのが、「Microsoft Ajax が上書きした String.prototype.startsWith を、ブラウザ標準と同じように寛容な実装に戻す」という方法です。
ポイントは次の 2 つです。
- ScriptManager によって Microsoft Ajax が既に読み込まれた「後」で実行する
- ブラウザ組み込みのネイティブ実装には触れず、「Microsoft Ajax によって上書きされている場合だけ」置き換える
サンプルコード:startsWith を標準互換の実装に戻す
ページ(もしくはマスターページ)の適切な位置に、次のようなスクリプトを追加します。
<script type="text/javascript">
(function () {
// startsWith が存在しない環境では何もしない
if (!String.prototype.startsWith) {
return;
}
// ネイティブ実装であれば toString() に "[native code]" を含むことが多い
// これを使って「Microsoft Ajax が上書きしたかどうか」の目安にする
var isNativeLike = String.prototype.startsWith.toString().indexOf('[native code]') !== -1;
if (isNativeLike) {
// すでにネイティブ実装なら何もしない
return;
}
// ここからブラウザ標準互換の startsWith を再定義する
String.prototype.startsWith = function (search, pos) {
// this を明示的に文字列化
var str = String(this);
// pos が未指定や NaN のときは 0、それ以外は 32bit 整数に丸める
pos = pos == null ? 0 : pos | 0;
if (pos < 0) {
pos = 0;
}
// 比較対象も文字列に変換
var searchStr = String(search);
return str.substring(pos, pos + searchStr.length) === searchStr;
};
})();
</script>
このコードは次のようなロジックで動きます。
String.prototype.startsWithが存在しない環境では何もしない(古すぎるブラウザ対策)toString()結果に[native code]を含む場合はネイティブと判断し、そのまま残す- ネイティブでない場合(=Microsoft Ajax によって上書きされている可能性が高い)だけ、ブラウザ標準互換の実装を上書きする
- 引数
searchをString(search)で文字列に変換するため、数値が渡されても例外は発生しない
これにより、ScriptManager が存在するページでも CesiumJS が期待通り動作し、マップのマウス操作時に Sys.ArgumentTypeException が発生しなくなります。
どこにスクリプトを置くべきか
重要なのは「Microsoft Ajax が読み込まれた後、CesiumJS が実行される前」という順序を守ることです。典型的な配置イメージは以下の通りです。
<!-- マスターページまたは対象 aspx -->
<form id="form1" runat="server">
<!-- ScriptManager: Microsoft Ajax を読み込む -->
<asp:ScriptManager ID="ScriptManager1" runat="server" EnablePageMethods="true"></asp:ScriptManager>
<!-- ★ここに startsWith を差し替えるスクリプトを記述 -->
<script type="text/javascript">
(function () {
if (!String.prototype.startsWith) return;
var isNativeLike = String.prototype.startsWith.toString().indexOf('[native code]') !== -1;
if (isNativeLike) return;
String.prototype.startsWith = function (search, pos) {
var str = String(this);
pos = pos == null ? 0 : pos | 0;
if (pos < 0) pos = 0;
var searchStr = String(search);
return str.substring(pos, pos + searchStr.length) === searchStr;
};
})();
</script>
<!-- 以降で CesiumJS やその他のライブラリを読み込む -->
<script src="Scripts/Cesium/Cesium.js"></script>
<script type="text/javascript">
// Cesium の初期化処理など
</script>
<!-- UpdatePanel 等も通常通り利用可能 -->
<asp:UpdatePanel runat="server">
<ContentTemplate>
...
</ContentTemplate>
</asp:UpdatePanel>
</form>
ScriptManager がマスターページ側にある場合は、同じマスターページにスクリプトを置くのが確実です。個別のコンテンツページに置くと、スクリプトが実行される時点で Microsoft Ajax がまだ読み込まれていない可能性があるため、マスターページ側を推奨します。
運用上の注意点
この解決策は実務で扱いやすいものですが、次の点に注意してください。
- グローバルな
String.prototypeを書き換えるため、アプリ全体に影響します - 「あえて Microsoft Ajax の厳格な startsWith に依存している」スクリプトがある場合、その挙動が変わります
- 導入前後で単体テストや E2E テストを走らせ、文字列比較ロジックに副作用が出ていないか確認すると安心です
とはいえ、多くの既存案件では startsWith をあくまで「普通の文字列メソッド」として使っているだけであり、厳格な型チェックに依存するケースは稀です。その意味で、この方法は「ScriptManager を残しつつ CesiumJS を安全に動かす」うえで現実的かつ汎用的な対策と言えます。
解決策B:ScriptManager を外して Microsoft Ajax 自体を読み込まない
もし対象ページで ASP.NET AJAX の機能を一切使っていないのであれば、思い切って <asp:ScriptManager> を削除してしまうのも有効な選択肢です。
適用しやすいケース
- UpdatePanel を使用していない
- PageMethods([WebMethod])を JavaScript から呼び出していない
- Sys.Application など Microsoft Ajax 独自の API を利用していない
- ScriptManager が「テンプレートだから何となく置いてある」程度の存在になっている
この場合、単に ScriptManager を削除するだけで、問題の原因である Microsoft Ajax の polyfill が読み込まれなくなり、CesiumJS の挙動も素のブラウザ環境と同じになります。
<!-- Before -->
<asp:ScriptManager ID="ScriptManager1" runat="server" />
<!-- After -->
<!-- ScriptManager を削除 -->
注意点と確認ポイント
ただし、この方法には次の注意点があります。
- UpdatePanel の部分更新がすべてフルポストバックに変わる
- PageMethods を利用している場合、JavaScript からのメソッド呼び出しが動かなくなる
- ScriptManagerProxy を利用しているページがある場合、それらにも影響する可能性がある
適用前に、以下のような簡単なチェックを行うと良いでしょう。
| チェック項目 | 具体的な確認方法 |
|---|---|
| UpdatePanel の有無 | ソリューション全体で <asp:UpdatePanel> を検索し、対象ページやマスターページ内に存在しないか確認する。 |
| PageMethods の利用 | JavaScript コードで「PageMethods.」という記述がないか検索する。 |
| Sys.* API の利用 | 「Sys.Application」「Sys.WebForms.PageRequestManager」などの文字列を検索する。 |
これらのいずれも使っていないのであれば、ScriptManager を外してしまうのが最もシンプルかつ副作用の少ない解決策となります。
解決策C:バージョンアップや分離構成で根本的に解決する
既存システム全体のメンテナンスやリプレースを検討できる状況であれば、より根本的なアプローチとして次のような案も考えられます。
案1:.NET Framework や Microsoft Ajax 周りをアップデートする
プロジェクトを .NET Framework 4.8 以降に更新するタイミングで、ScriptManager や JavaScript 周りの構成を見直すことも有効です。
- ScriptManager のスクリプト参照方法を整理する
- Bundling / Minification などを利用してスクリプト読み込み順序を安定させる
- 不要な Microsoft Ajax の API への依存を減らし、将来的に ScriptManager を外せる状態にしておく
ただし、このアプローチは「今すぐ CesiumJS のクラッシュを止めたい」という観点では時間がかかるため、短期的には A か B を適用し、長期的な改善策として位置づけるのが現実的です。
案2:Cesium 部分を別ページ/iframe に分離する
もう一つの方針は、CesiumJS を ScriptManager と「物理的に」分離してしまう方法です。
- マスターページや共通レイアウトから ScriptManager を外せない場合
- マップ画面だけ独立した構成にしておきたい場合
などでは、以下のような構成が取り得ます。
| 構成 | 概要 | メリット | デメリット |
|---|---|---|---|
| 専用 aspx ページ | ScriptManager を含まない専用の aspx を用意し、そのページでのみ CesiumJS を読み込む。 | マップページだけ「プレーンな HTML+JS」に近い状態にでき、ライブラリ競合リスクが減る。 | 既存のマスターページ構造やナビゲーションとの統合がやや面倒になることがある。 |
| iframe 埋め込み | マップ表示部分だけを HTML / aspx に切り出し、親ページから iframe で読み込む。 | 親ページ側がどれだけ複雑でも、iframe 内の JavaScript 環境は独立させられる。 | レスポンシブ対応やイベント連携(クリックで親ページに通知など)の実装が必要になる。 |
こちらも A / B に比べると工数は増えますが、「マップだけはモダンな JavaScript の世界」「ページ本体は WebForms+Microsoft Ajax」という役割分担を明確にできるため、中長期的にはメンテナンス性の向上にもつながります。
実装後に確認したいポイントとデバッグのコツ
いずれの解決策を採用するにせよ、実装後には次のような観点で動作確認を行うと安心です。
ブラウザコンソールで startsWith の実装を確認する
開発者ツール(F12)のコンソールで、現在の startsWith がどの実装になっているかを簡単に確認できます。
// startsWith の中身をざっくり表示
String.prototype.startsWith.toString();
ここで [native code] が含まれていればネイティブ実装、スクリプト本体が文字列で表示されれば polyfill や上書きされた実装である可能性が高い、という目安になります。
解決策Aを導入した後は、この結果が「自分が定義した実装」になっていることを確認するとよいでしょう。
マウス操作時のエラーが消えているか確認する
Cesium マップに対して、以下の操作を一通り試してみます。
- マップ上にマウスを乗せる/外す
- ドラッグしてパン操作を行う
- ホイールズームやピンチズーム(タッチ環境)を試す
- エンティティのクリック/ホバーイベントを発火させる
その際、開発者ツールのコンソールに Sys.ArgumentTypeException や Object of type 'Number' cannot be converted to type 'String' が出なくなっているかを確認します。念のため、他の JavaScript エラー(古いコードの副作用など)が増えていないかも併せてチェックしましょう。
ASP.NET AJAX 機能の回帰テスト
解決策Aで startsWith を差し替えた場合、次のような機能が従来通り動作するか簡易的なテストを行っておくと安心です。
- UpdatePanel 内のボタンクリックで部分更新が行われるか
- PageMethods を利用したサーバーサイドメソッド呼び出しが成功するか
- PageRequestManager エラーイベントなどが期待通りに動作するか
ここで問題が見つからない限り、startsWith の差し替えによる影響は限定的であると判断できます。
ライブラリ競合を防ぐための設計上のポイント
今回の問題は「ScriptManager が組み込みオブジェクトを拡張した結果、モダンな JavaScript ライブラリと競合した」ケースの一例に過ぎません。今後のトラブルを防ぐために、次のような設計指針を意識しておくとよいでしょう。
グローバルな prototype 書き換えには慎重になる
Array.prototypeやString.prototypeを書き換えるライブラリは、他ライブラリとの競合リスクが高い- 既存プロジェクトに新しいライブラリを導入するときは、「グローバル拡張」を行うかどうかを事前に確認する
- 特定のブラウザだけで polyfill が有効になる場合にも挙動差が生まれるため、テスト対象ブラウザを明確にする
WebForms とモダン JS を同じページで混在させる場合のルールを作る
- ScriptManager を置くページ/置かないページを明確に分ける
- マップやSPA風の UI を構成する画面は、可能な限り「素の HTML + モダン JS」に寄せる
- どうしても混在させる場合は、今回のような polyfill 上書き対策を「プロジェクト共通の初期化スクリプト」として用意しておく
ライブラリのバージョンと組み合わせを記録しておく
今回のような不具合は、「特定のバージョンの CesiumJS」と「特定の .NET Framework / ScriptManager」の組み合わせでだけ発生する、といったパターンも多くあります。障害対応や将来のアップデートに備え、次のような情報を README や設計書に残しておくと役立ちます。
- 使用しているライブラリのバージョン(CesiumJS, jQuery, その他)
- .NET Framework のバージョン、IIS の情報
- ScriptManager の有無や配置場所、ScriptManagerProxy の利用状況
- 今回のような対策コード(startsWith の差し替え)の存在と、その意図
こうした情報が残っていれば、数年後に別の開発者が「なぜ startsWith を上書きしているのか?」と悩む事態も防げます。
まとめ:ScriptManager と CesiumJS の共存は十分可能
ASP.NET WebForms のレガシー感と、CesiumJS のようなモダンな JavaScript ライブラリを同じページで共存させると、今回のような微妙な競合が起こることがあります。しかし、問題の構造さえ理解してしまえば、対策自体はそれほど難しくありません。
- ScriptManager が読み込む Microsoft Ajax は
String.prototype.startsWithを厳格な実装で上書きし、非文字列引数に対してSys.ArgumentTypeExceptionを投げる - CesiumJS は内部で
startsWithに数値などを渡すケースがあり、その組み合わせで「Object of type ‘Number’ cannot be converted to type ‘String’」エラーが発生する - ScriptManager を外せない場合は、「寛容な startsWith 実装に差し替える」ことでブラウザ標準に近い挙動へ戻すのが現実的な解決策
- ASP.NET AJAX 機能を使っていないページでは、ScriptManager を削除してしまうのもシンプルで有効
- 長期的には、マップ部分のページ分離や .NET Framework のアップデートも検討すると、ライブラリ競合のリスクを減らせる
既存の WebForms プロジェクトに CesiumJS を導入してマップがクラッシュしている場合は、まずは startsWith の挙動と ScriptManager の有無を確認し、本記事で紹介したいずれかの対策を試してみてください。適切に制御すれば、ScriptManager の恩恵を残したまま、CesiumJS のリッチな 3D マップ体験を安定して提供できるようになります。

コメント