ASP.NETの一部のコントロールはdllに埋め込まれたリソースにアクセスするために /WebResource.axd にアクセスするjavascriptやhtmlを出力します。動的なページ(axd, aspx)にアクセスするとき、組織名がリクエストパスのルートの含まれていない場合、既定の組織にアクセスしたものとみなされます。そのようなコントロールをDynamics CRM 4.0 のアドオン画面で使用していると、OnPremise環境で完全にマルチテナント対応するのは難しいことを[Dynamics CRM 4.0]ASP.NETの一部のコントロールはOnPremise環境でマルチテナント対応は無理かもしんないで記載しました。ユーザが既定の組織以外にアクセスする場合、既定の組織でユーザが無効化されているなど何らかの理由で、ユーザの既定の組織にアクセスできない場合、 /WebResource.axd にアクセスするとエラー画面にリダイレクトされ、意図通りコントロールが動作しません。リンク先で述べている通り 解決策の一つがユーザの既定の組織を変更する方法です。今回は別の方法で強引に解決してみます。 /WebResopurce.axd にアクセスするように出力されるhtmlを/組織名/WebResource.axd にアクセスするように書き換えます。

上記の問題を起こす代表的なコントロールが TreeView コントロールです。TreeViewを使用したアドオン画面でマルチテナントの問題に出くわしたため動作確認はTreeViewコントロールを使用して行っています。他のコントロールもあり得ますが未検証です。おそらく同じ方法で対応できると思います。

TreeViewは次のようなタグで使用されているものとします。PopulateNodesFromClient を true に設定していますが、実装したアドオン画面がパフォーマンスを向上させるために動的にノードを作成するようにしているためで解決策とは関係ありません。ただし、動的にノードを作成すると 動的に作成された子ノードのhtmlが、コントロールに埋め込まれた画像ファイルを取得するため /WebResouces.axd にアクセスするhtmlを含む文字列を出力します。その動作を防ぐために、ExpandImageUrl, CollapseImageUrl, NoExpandImageUrl を指定しています。指定しているパスは Dynamics CRM アプリケーションがインストールされたときに配置される画像ファイルへのUrlです。 

<!-- ツリービューの定義 -->
<asp:TreeView ID="treeView" runat="server" PopulateNodesFromClient="true"
    OnTreeNodePopulate="treeView_TreeNodePopulate" ExpandImageUrl="/_imgs/ApptBook/plus.gif" CollapseImageUrl="/_imgs/ApptBook/minus.gif" NoExpandImageUrl="/_imgs/ico_dot.gif"  >
</asp:TreeView>

1.WebResouce.axd のパスを書き換える

アドオン画面でTreeViewを使用しているときに /WebResource.axd にアクセスするhtmlやjavascriptを /組織名/WebResource.axd をアクセスするように書き換えます。方法は アドオン画面で Render メソッドをオーバーライドし、/WebResouce.axd?d= という文字列を /OrgName/WebResouce.axd?d= に書き換えます。OrgName は実際には組織名です。

// Renderメソッドをオーバーライドして/WebResource.axdのパスを書き換える
// 厳密には img,scriptタグないに含まれている場合のみ書き換える
// という処理が必要.
protected override void Render(HtmlTextWriter writer)
{
    StringBuilder builder = new StringBuilder();
    StringWriter stringWriter = new StringWriter(builder);
    HtmlTextWriter textWriter = new HtmlTextWriter(stringWriter);
    base.Render(textWriter);
    StringBuilder replaced = builder.Replace("/WebResource.axd?d=", "/" + OrgName + "/WebResource.axd?d=");
    writer.Write(replaced.ToString());
}

プログラムでは /WebResouce.axd?d= という文字列があれば、何も考えずに /組織名/WebResouce.axd?d= という文字列に置換しています。ページ内に /WebResouce.axd?d= という文字列が通常の文字列として現れる可能性がある場合は、 <script>タグ内や <img> タグ内で WebResouce.axd が現れた場合のみ置換するといったより洗練された制御が必要になるでしょう。

かなり強引ですが本対応により TreeView が出力する /WebResouce.axd へアクセスするパスを /組織名/WebResouce.axd へアクセスする html, javascript に変更できました。

2. そのほかユーティリティメソッド

今回の問題とは直接関係ないですが、備忘録としてマルチテナント対応用に作成した便利APIを記載します。IsIFDAuthentication メソッドは IFD 環境の場合 true を返すメソッドです。 PopulateIsvStaticResouceUrl は、引数で指定した静的リソースへの相対パスからOnPremise, IFD環境を考慮したパスを返すメソッドです。OnPremiseの場合、組織名を除外したパスを生成します。リクエストパスのファイルが静的リソースの場合は、動的ページ(aspx,axd)へのアクセスとは異なり、パスのルートに組織名が含まれているとリソースが見つかりません。そのため組織名を除外したパスを出力する必要があります。

/// <summary>
/// IFD認証かを調べる
/// </summary>
/// <returns></returns>
public virtual bool IsIFDAuthentication()
{
     const string ifdAuthenticationType = "CrmPostAuthentication";
     return ifdAuthenticationType.Equals(System.Web.HttpContext.Current.User.Identity.AuthenticationType, StringComparison.InvariantCultureIgnoreCase);
}
/// <summary>
/// ISV配下に配置した静的リソース(js,jpg,css等)の相対パスから、
/// 認証方式を考慮したリソースへのURLを作成する。
/// </summary>
/// <param name="relativePath">リソースの相対パス</param>
/// <returns>OnPremise,IFD認証を考慮したパス</returns>
public virtual string PopulateIsvStaticResourceUrl(string relativePath)
{
    // IFDの場合は、相対パスを変更しない
    if (IsIFDAuthentication())
    {
        return relativePath;
    }
    // OnPremiseの場合は、組織名分のパスを除く
    string path = ResolveUrl(relativePath);
    return path.Remove(0, this.OrgName.Length + 1);
}

IsIFDAuthentication を使用した別の例として、OnPremise環境で、リクエストされたパスの実際の物理パスを取得するサンプルを以下に記載します。IFD環境では、HttpRequest.PhysicalPath で物理パスを取得できます。

// 物理パスを取得する場合
if (!IsIFDAuthentication())
{
    if (Request.Url.Segments[2].TrimEnd('/').ToLower() == "isv")
    {
        requestFile = new FileInfo(MapPath(Request.Path.Substring(this.OrgName.Length + 1)));
    }
}

上記で2つめのisvチェックは助長かもしれません。OnPremise環境でもサイトマップや ISV.config での設定方法によっては、 /ISV/XX.aspx のようにアクセスするように設定できるので、念のため /組織名/isv/XXX.aspx となっているかのチェックを入れています。 

3. まとめ

今回は以上です。 /WebResouce.axd にアクセスするhtmlやjavascriptを強引に書き換えてクライアントに出力することで/組織名/WebResouce.axd にアクセスさせる方法を紹介しました。OnPremise環境でマルチテナントを対応する必要がある場合、Fiddler などを使用して 動的リソースへのリクエストパスのルートに組織名が含まれていることを確認するようにしたほうがよいみたいです。