XAMLはWPFアプリケーションの開発において、UIのデザインや複数のコントロールなどで利用する背景デザインなどをまとめたResourceDictionaryを記述す際に利用するマークアップ言語です。
本記事では、通常、クラスライブラリには追加できないResourceDictionaryなどのXAML定義ファイルを追加し、またこれをUserControlのXAMLから利用したり、C#のコード上でResourceDictionaryをロードする方法を紹介致します。


クラスライブラリでXAMLを記述する

Visual Studioでプロジェクトを作成する際、クラスライブラリを選択すると追加項目の一覧に「ResourceDictionary」の項目が表示されません。

イメージ 1
図1 クラスライブラリで追加可能なWPF関連項目

クラスライブラリとして作成したプロジェクトとは別に「WPFアプリ」としてソリューションを作成し、その中で作成したResourceDictionaryのXAMLファイルをコピーする方法などもあるかと思います。
本記事では、テキストファイルとしてプロジェクトに追加した項目をResourceDictionaryなどを定義可能なXAMLの項目へ変更する方法をご紹介致します。

(1)参照の追加

ResourceDictionary型をはじめとするWPF関連の型が定義されているアセンブリや、XAMLを扱う上で必要になる各種機能を提供してくれるアセンブリを参照に追加する必要があります。
以下の項目をプロジェクトの参照に追加して下さい。
  • PresentationCore
  • PresentationFramework
  • System.Windows
  • System.Xaml
  • WindowsBase

(2)ファイルの追加


イメージ 2
図2 テキストファイルの追加

まず、クラスライブラリのプロジェクトへ「テキストファイル」を追加します。
その際、ファイル名を「***.xaml」という形で拡張子を「xaml」にします。

イメージ 3
図3 無効なマークアップ

追加すると、「無効なマークアップ」というエラーがデザイン画面に表示されます。

拡張子を「txt」のまま追加した場合、通常のテキストエディタモードでファイルが開かれてしまいます。
その際は、拡張子を修正した後、項目のプロパティを以下に示した図のように変更して下さい。

イメージ 4
図4 XAML項目のプロパティ

表1 XAMLファイルとして扱わせるためのファイルのプロパティ値
項目名
設定値
カスタム ツール
XamlIntelliSenseFileGenerator
カスタム ツールの名前空間
変更不要
ビルド アクション
Page
出力ディレクトリにコピー
コピーしない(変更不要)

こうすることで,プロジェクトに追加したファイルは,XAMLとして扱われるようになります.
具体的には,Visual Studioで編集する際は,XAML専用のモードで開かれ,また,ビルド時にはXAMLの定義データとして正しくビルドされるようになります.

(4)XAMLの記述

あとは、通常通りResourceDictionaryを定義していきます。

イメージ 5
図5 XAMLによるResourceDictionaryの記述


【解説】ビルド アクション「Page」

図4に示したとおり、XAMLファイルのビルドアクションは「Page」に設定する必要があります。
このPageについて解説をば。

XAMLの記述によって定義されたデータは、ビルド時に特殊な処理が行われます。

イメージ 6
図6 XAMLの扱い

XAMLファイルは通常のC#コードとは違い、ILコードには変換されません。出力されたアセンブリをildasmで定義したResourceDictionaryなどを探してみると、クラスとして扱われていないのがわかるかと思います。
※UserControlやWindowなどのXAML定義はロード用・コード記述用のクラスが作成されます。

XAMLはビルド時に「BAML」というXAMLを読み込みやすくバイナリデータ化したものに変換され、出力されるアセンブリに格納されます。
その際変換元となったXAMLファイルに基づき、BAMLデータにも識別名が振られるようです。これは実行時にBAMLからインスタンスを作成する際に利用しますが、詳細については後述します。

どうやら、その「BAMLへ変換,並びにBAMLデータのアセンブリへの埋め込み」という指示を表しているのがビルドアクション「Page」のようです。
やや検証不足気味ですが。。

やや余談ですが、XAMLとBAML、実行時に生成されるインスタンスは次のような処理関係になります。

イメージ 7
図7 XAML、BAML、インスタンスの関係

XAMLデータをファイルなどのStreamから読み込んでインスタンスを生成する「XamlReader」というクラスが実は存在しております.
しかし後でも触れますが,XAMLなどをはじめとするXMLデータのパース及びロードには実行コストが発生します.

ビルド時に予め,XAMLを読み込みやすいBAMLに変換することで,実行時に余計なロードコストが発生することを防いでいるようです.
XAMLをC#コードに変換して,そのあとに通常のコードと一緒にILにコンパイルするような仕組みを取っていると思っていたのですが,そうではなかったんですね...

XAMLを利用する(XAML)

UserControlなどのXAMLで、定義したResourceDictionaryを利用する場合は図7のように記述します。

イメージ 8
図8 ResourceDictionaryの参照

ResourceDictionaryのプロパティ「Source」にXAMLのUriを指定することで参照することが可能です。
XAMLのUriについては、後述の「XAMLを参照する際のUriについて」をご覧ください。
ソリューションをビルドするまでは、ResourceDictionaryの定義内容がUserControlのデザイナに反映されない場合がございます。

すでにUserControl内で定義したResourceDictionaryと合わせて利用したい場合は、新しくResourceDictionaryを作成し、MergedDictionariesで追加してあげると良いでしょう。

イメージ 9
図9 UserControlで定義したResourceDictionaryと合わせて利用する

XAMLを利用する(C#コード)

XAMLで定義したResourceDictionaryなどをC#のコード上で利用したい場合は、まずロードしてインスタンスを作成する必要があります。
ロードは、 System.Windows.Application 名前空間の LoadComponent メソッドを利用します。

イメージ 10
図10 LoadComponentの利用

Application.LoadComponent(new Uri("SampleClassLibrary;component/sampleresources.xaml", UriKind.Relative));

LoadComponentメソッドは指定したUriのXAMLをロードしてインスタンス化してくれるメソッドです。
戻り値はObject型になっておりますが、例えば、ResourceDictionaryを定義したXAMLをロードした場合は、そのままResourceDictionaryにキャストすることが可能です。

このとき指定するUriについては、後述の「XAMLを参照する際のUriについて」をご覧ください。

【解説】XAMLを参照する際のUriについて

XAMLの定義を利用するためには、XAMLのUriを指定して、ロードする必要があります。
Uriは通常Web上のコンテンツやローカルファイルのパスなどを指定する際に用いる型ですが、どうもWPFではアセンブリ内に格納された特殊なリソースを参照する際にも利用可能なようです。

その際のUriは文字列から初期化しますが、構文は次のとおり。
[アセンブリ名];component/[プロジェクト内でのパス(すべて小文字で)]

アセンブリ名は、恐らく出力アセンブリファイル名かプロジェクト名がこれに該当するのですが、通常はこの2つは一致するため、どちらが正解かは未検証です。
また、プロジェクト内でのパスも実際のcsprojファイルからの相対パスになるのか、あるいはcsproj内での論理パスになるのかは未検証です。既存のXAMLファイルをリンクで取り込んだ際は注意が必要です。

Uriの例

前述のサンプル「SampleClassLibrary」ではプロジェクトのルートに「SampleResources.xaml」を追加しました。
これを参照する際のUriは
SampleClassLibrary;component/sampleresources.xaml
になります。

また、プロジェクト内で「SampleDir」というディレクトリを作成し、その中に「Hoge.xaml」を定義した場合のUriは
SampleClassLibrary;component/sampledir/hoge.xaml
になります。

この辺は、通常のUserControlに対してVisualStudioが自動生成したラッパクラスの中身を見ただけなので、もっと深く調べる必要がありそうですね。

余談「ControlTemplateをXAMLで定義してみた」

コードから参照して利用する場合、ResourceDictionary以外でもXAMLに定義することが可能です。

イメージ 11
図11 ControlTemplateを定義してみた例

ControlTemplateは、XAMLで記述する場合、XAMLからのロードの都合もあり、通常はResourceDictionary内で定義しますが、ControlTemplateをコードから呼び出して利用したい状況が発生しましたので、ResourceDictionaryを省略して直接定義してみました。
正攻法とは言えないでしょうが、ControlTemplateを直接XAMLで定義すると、デザイナが仕事をしてくれるんですね。便利かも(?)

ちなみに、サンプルイメージの配色がTheドイツ国旗って感じですが、実験用にわかりやすい配色がないか考えたら偶然ドイツ国旗が頭をよぎっただけです。
ドイツ国旗シンプルでかっこいいですけど、それをイカしたUIに活用するだけのデザイン力は僕にはありません。
あ、ちなみにドイツ国旗の3色のうち一番下の部分って黄色じゃなくて正確には金色みたいですね。

余談「Application.LoadComponentとXamlReader」

ぶっちゃけ頑張ってBAMLとしてアセンブリに挿入させることにこだわらずとも、リソースとしてぶちこんだXAMLファイルを実行時にXamlReaderでLoadしちゃうなんて手もなくはないです。
というか、この方法を思いつかなかった時は、そうしていました。

しかし、通常のリソースとしてXAMLデータを添付した場合、若干ちゃんとBAMLに変換させたときよりも時間がかかるようです。
そらそうですよね。
BAMLってのはビルド時にXAMLを読み込んで最適化した結果なのですから。

あとファイルサイズが変わります。
BAMLのほうがファイルサイズ小さいです。
リソースとしてXAMLファイルを取り込むとXAMLのソースがそのまま出力ファイル内に取り込まれます。
テキストエディタでバイナリを眺めるという意味不明な習性を持っている僕と致しましては、圧倒的にBAMLのほうがかっこいいバイナリになります(意味不明)


では。