2012年5月15日火曜日

wpf : リソースをC#で読み込んでxamlで使う

ちょっと前にカスタムコントロール.Resourcesが使えないという状況を発見したので、リソースをC#で読み込んでxamlで使う方法について調べてみました。 カスタムコントロール.Resourcesが使えない状況というのはこちらのネタでのお話です。

普通にやってたら気にしなくてもいいような内容ですね。 まぁ、カスタムコントロールの方は置いとくとして、リソースの読み込み方は他に役に立つこともあるかもしれないので投稿します。

xamlでResourcesプロパティに書くことができるリソースは、

  • FrameworkElement.Resources ... イミディエイト リソース
  • Application.Resources ... アプリケーション リソース

と呼ぶのだそうです。

それ以外のリソースを扱うには、

  1. 適当なクラスのResourceDictionary型のstatic変数に読み込んでおいてBinding。
  2. 適当なクラスのResourceDictionary型のインスタンスプロパティに読み込んでおいてBinding。

などをします。 例を挙げるとこんなコードになります。

<Window x:Class="ResourceDictionaryLoadTest.MainWindow"
        Name="mainWindow"
        xmlns:app="clr-namespace:ResourceDictionaryLoadTest"
        ~その他属性~
>
    <StackPanel>
        <!-- staticなResourceDictionaryにアクセス -->
        <TextBlock Style="{Binding Path=[style],Source={x:Static app:MainWindow.StaticResources}}"/>

        <!-- インスタンスプロパティのResourceDictionaryにアクセス -->
        <TextBlock Style="{Binding Path=InstanceResources[style],ElementName=mainWindow}"/>
    </StackPanel>
</Window>

MainWindow.StaticResourcesがstatic変数、MainWindow.InstanceResourcesがインスタンスプロパティです。 Bindingの記述が長くなってしまうのは仕方がないかな? 「それが嫌ならイミディエイトリソースを使う方法を考えなさい」ということでしょう。

ResourceDictionaryを読み込むコードはこうなります。

// MainWindow.xaml.cs
using System;
using System.Diagnostics;
using System.Windows;
using System.Windows.Markup;
using System.Windows.Resources;
using System.Xml;

namespace ResourceDictionaryLoadTest
{
    public partial class MainWindow : Window
    {
        private const string EnbededResourcePath = "EnbededResources.xaml";
        private const string LooseResourceFileName = "LooseResources.xaml";

        public static readonly ResourceDictionary StaticResources;

        public static readonly DependencyProperty InstanceResourcesProperty = DependencyProperty.Register(
                "InstanceResources",
                typeof(ResourceDictionary),
                typeof(MainWindow),
                new FrameworkPropertyMetadata(null)
        );

        public ResourceDictionary InstanceResources
        {
            get { return (ResourceDictionary)GetValue(InstanceResourcesProperty); }
            set { SetValue(InstanceResourcesProperty, value); }
        }

        static MainWindow()
        {
            StaticResources = LoadEmbededResources(EnbededResourcePath);
        }

        // ビルド時に埋め込んだResourceをロード。
        public static ResourceDictionary LoadEmbededResources(string path)
        {
            Uri uri = new Uri(path, UriKind.Relative);
            StreamResourceInfo info = Application.GetResourceStream(uri);
            XamlReader reader = new XamlReader();
            return reader.LoadAsync(info.Stream) as ResourceDictionary;
        }

        // ResourceAssemblyと同じフォルダにあるxamlファイルをロード。
        // ResourceAssemblyは特に設定しない場合、実行アセンブリとなる。
        public static ResourceDictionary LoadLooseResources(string fileName)
        {
            string codeBase = App.ResourceAssembly.CodeBase;
            Uri uri = new Uri(new Uri(codeBase), fileName);
            XmlReader xmlReader = XmlReader.Create(uri.LocalPath);
            return XamlReader.Load(xmlReader) as ResourceDictionary;
        }

        public MainWindow()
        {
            // InitializeComponentの前に読み込む。
            try
            {
                InstanceResources = LoadLooseResources(LooseResourceFileName);
            }
            catch (Exception exc)
            {
                Debug.Print(exc.ToString());
            }

            InitializeComponent();
        }
    }
}

static変数の方のStaticResourcesはreadonly属性を付けてみました。 静的コンストラクタで読み込んでいます。

インスタンスプロパティの方のInstanceResourcesは依存プロパティにしてみました。 普通のプロパティにしても問題なく動作します。 しかし、クラスに多くのプロパティがある場合、普通のプロパティをxamlからBindingで探すのは重い処理になってしまうそうです。 なので特に理由が無い場合は依存プロパティにしておいた方が良いでしょう。

このコードではMainWindowでリソースを使っているので、InitializeComponentでMainWindow.xamlが読み込まれる前にInstanceResourcesを読み込んでいます。 簡単なサンプルと言うことで、リソースが変更されたときのことについては考慮していません。

このサンプルにはリソースの読込先が「埋め込みリソース」の場合と「実行時に読み込む」場合の2種類書いてあります。 こっちの方も説明しておきましょう。

埋め込みリソースというのはビルド時にアセンブリに埋め込んでしまうリソースです。 ソリューションエクスプローラでリソースディクショナリを追加して、ファイルのプロパティ設定でビルドアクションをResourceにしておきます。 後は読み込みメソッドLoadEmbededResourcesは見ての通りです。

実行時に読み込むリソースは未コンパイルのxamlファイルです。 App.ResourceAssemblyを元にxamlがあるディレクトリを特定しています。 ここで「実行ファイルのパスを元にxamlファイルを探してはいけない」というちょっとした落とし穴があります。 実行ファイルのパスを元にするとxamlデザイナの表示が崩れてしまうんですよね。 これはxamlデザイナはVisual Studio(例えばvcsexpress.exe)の機能だから。 xamlデザイナがプロジェクト内で定義されたクラスをインスタンス化すると、当然そこから実行ファイルのパスをたどるとvcsexpress.exeに行き着いてしまいます。 Visual Studioのディレクトリに個別のプロジェクトのリソースなんて無いので見つからないんですね。

正直App.ResourceAssemblyの正体があまりよく分かってないので、コレを元にしていいのかイマイチ確信を持てていません。 今回のようなケースではResourceAssemblyを元にリソースを探すのは自然なことなので、そうしてもいいんじゃないかなぁと思いますが、どうでしょう?

実行時に読み込むリソースを表すLooseResourcesとかっていう単語は「Loose xaml」から取っています。 Loose xamlというのは「プログラムコードを含まずxaml単体で完結しているコード」の事らしいんでちょっと的外れな変数名かもしれないですね? まぁいいや。

最後に一応、リソースファイル2つを載せておきます。 EnbededResources.xamlは、

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Style x:Key="style" TargetType="TextBlock">
        <Setter Property="Text" Value="Enbeded resource"/>
        <Setter Property="Foreground" Value="DarkRed"/>
    </Style>
</ResourceDictionary>

LooseResources.xamlは、

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Style x:Key="style" TargetType="TextBlock">
        <Setter Property="Text" Value="Loose resource"/>
        <Setter Property="Foreground" Value="DarkBlue"/>
    </Style>
</ResourceDictionary>

文字列と色を変えただけです。