2010年01月08日

WPF 上の pack URI

Generic.xamlの中のImageのSourceを、相対パス指定する方法について で私が自分自身で触れたにも関わらず実際には良く理解していなかった pack スキームについての調査。

資料はコレ、Windows Presentation Foundation におけるパッケージの URI ですね。そのもととなる Open Packaging Conventions の仕様をまず見ろと言う話かも知れませんが、知りたいのはプログラム上の話ですし。

なお、Open Packaging Conventions の仕様は ISO/IEC 29500-2 Open Packaging Conventions (OPC, 2008)(zip ファイル)に存在しているみたいです。

pack スキームは本来パッケージ及びその内部のパーツを識別するための URI 記述法であり、WPF はそれを流用してリソースの場所を指定するための URI として利用しています。正直、なぜ敢えて pack スキームを使ったのか分かりません。拡張しやすかったからでしょうかね。

WPF で pack URI を使用する手段などは一通り上記記事に出ていて、特に付け足すこともありません。個人的にはコンテンツファイルを解決する際にどうやるのか結構悩んだんですが、記事をよく読むとちゃんと記述されていました(AssemblyAssociatedContentFile 属性)。ですのでそれ以外の豆知識をいくつかご紹介しましょう。

pack URI 形式は、上記記事には「pack://application:,,,/Content.xaml」などと記述されています。しかし、実行ファイルのエントリポイント Main メソッド冒頭でいきなりこの文字列を引数にした Uri オブジェクトを作ろうとすると、実行時例外 UriFormatException(「無効な URI: コロン (':') があるためポートが予期されていましたが、ポートを解析できませんでした」)になります。Open Package Conventions では証明機関内の : はパーセントエンコード対象の文字であるので %3a に置き換えてみても、メッセージこそ変わるもののやはり UriFormatException(「無効な URI: ホスト名を解析できませんでした」)です。

これらの問題を解決するのに一番簡単なのは、System.IO.Packaging 名前空間に存在する PackUriHelper クラスの Create 静的メソッドを使用することです。引数が二つのものを使用し、一つ目の引数 packageUri に "application:///" を、二つ目の引数 partUri にコンテンツのパス "/Content.xaml"(こちらは UriKind.Relative を使って作成)を指定すれば、問題なく pack URI 形式で証明機関に application:/// を使った URI が作成できます。

この辺のことは、Open Packaging Conventions のアドレス指定モデル に記述されています。

URI を記述することはできました。次はこの URI に存在するリソースにアクセスする方法です。「Open Packaging Conventions のアドレス指定モデル」のページにも紹介されていますが、pack スキームに対応する WebRequest 派生クラス、PackWebRequest クラスが System.IO.Packaging 名前空間に用意されています。WebRequest を作成する作法として、スキームに対応する各派生クラスは直接コンストラクタを提供せず、WebRequest.Create を通じてインスタンスを作成することになっていますが、残念ながら pack プリフィクスは認識されず、実行時例外 NotSupportedException(「URI プレフィックスが認識されません」)が発生してしまいます。

URI のプリフィクスと対応する WebRequest を関連づけるのには、WebRequest.RegisterPrefix を使用します。第二引数の IWebRequestCreate の pack スキームにおける実装は PackWebRequestFactory です。いちいち WebRequest.RegisterPrefix を呼び出す代わりに、app.config の configuration / system.net / webRequestModules 配下に add 要素を記述する方法もあります。

直接 PackWebRequestFactory を使って WebRequest を直接作ってしまう方法もあります。ただし Create メソッドが明示的な実装になっているので、PackWebRequestFactory インスタンスを IWebRequestCreate に一旦キャストしなければなりません。WPF ではこの方法を採っているようです。当然ながら WebRequest にプリフィクスを登録していない場合はネスト型パッケージには対応できませんが、まあ普通は application 及び siteoforigin しか使わないしネストなんて考えるだけ無駄だろうとは思います。

この WebRequest に対して GetResponse すると、再び実行時例外 NotSupportedException(「URI プレフィックスが認識されません」)が発生しまいます。pack URI の中身は実際のパッケージの URI(“証明機関”部分)とそのパッケージ内のパート名で成り立っています。これは逆に言うと、まず証明機関の URI を使ってパッケージを取得し、その中から対象のパートを取り出すという処理になります。

証明機関が application:/// のとき、証明機関の URI はそのまま "application:///" です。つまり、WebRequest.Create("application:///") という操作が発生するわけです。application プリフィクスなど WebRequest には登録していないのでこの操作は失敗します。

ところで、パッケージを毎回サーバに要求するのは非効率なので、パッケージの URI に対してパッケージをキャッシュする機能が用意されています。それが PackageStore です。application:/// の場合、System.Windows.Application クラスの静的コンストラクタが登録を行っているようです(どこかの掲示板か何かで見かけたんですが URL 失念)。厳密には PackageStore そのものではなくその類似機構みたいですが。つまり、Application を何か使用(new したり、Current を参照したり)することで、application:/// 証明機関を使用可能になります。

これでリソースやコンテンツファイル(こちらの場合は上の方で言ったとおり、AssemblyAssociatedContentFile 属性が必要ですが)にアクセスすることが可能になりました。

ちなみに、application:/// の代わりに http:// を使えばウェブ上のファイルをソースにできますし、Open Package Conventions 仕様に合致したパッケージ内のパートを使用することもできます。pack がネストする「コンテンツファイルのパッケージのパート」は、スタックトレースを見るに、PackWebResponse のコンストラクタで BeginGetResponse を呼び出していて、それが PackWebResponse.BeginGetResponse を呼び出すことになり(ネストしてなかったら HttpWebResponse.BeginGetResponse とかになるわけですが)、PackWebResponse は非同期アクセスを実装していないっていうことで取れない(実行時例外)みたいですが。

最後に、逆の視点、すなわち普通にアセンブリのリソースとして取得する場合どうするのか? ということを考えましょう。

WPF のリソースは、Assembly.GetManifestResourceStream で取得できる「ビルドアクション:埋め込まれたリソース」やResources クラス内の明示的に型指定されたプロパティでアクセスできる「プロジェクトのプロパティ>リソース」とは違って、「ビルドアクション:Resource」を使用します。XAML からなら直接相対パスを記述すれば参照できますし、コードからなら今までさんざん述べてきたように pack URI と PackWebRequest を使って取得できます。

ではこの実体はどうなっているのか?

結論から言うと、ResourceManager を使ってアクセスできるリソースとして定義されています。obj フォルダの中に「アセンブリ名.g.en-US.resources」などという名前になっているのがアセンブリに埋め込む前のリソースです。カルチャ名が含まれるかどうか、含まれるとして en-US 固定なのかどうかは未調査ですが。

上記ファイル名を見れば分かるとおりリソース名は「アセンブリ名.g」になります。ResourceManager コンストラクタには、このリソース名とアセンブリを渡します。

あとは ResourceManager の GetStream メソッドや GetObject メソッド、あるいは ResourceSet を作ってそちらの GetObject メソッドを使えば MemoryStream として取得できます。

posted by Hongliang at 21:51| Comment(0) | TrackBack(0) | .NET | このブログの読者になる | 更新情報をチェックする
この記事へのコメント
コメントを書く
お名前:

メールアドレス:

ホームページアドレス:

コメント:

認証コード: [必須入力]


※画像の中の文字を半角で入力してください。

この記事へのトラックバック

ここ(hongliang.seesaa.net)で公開しているものについて、利用は自由に行って頂いて構いません。改変、再頒布もお好きになさって下さい。利用に対しこちらが何かを要求することはありません。

ただし、公開するものを使用、または参考したことによって何らかの損害等が生じた場合でも、私はいかなる責任も負いません。

あ、こんなのに使ったってコメントを頂ければ嬉しいです。

×

この広告は1年以上新しい記事の投稿がないブログに表示されております。