ページ

2010年8月25日水曜日

[Silverlight] MEF を使って XAP を動的に読み込む その2

[Silverlight] MEF を使って XAP を動的に読み込む その1」 の続きです。

今回は XAP を動的に読み込んでみる例です。

なお、Visual Studio 2010 のソリューションを http://cid-ca42d76a68f54d16.office.live.com/self.aspx/Public/Sample/MefSample.zip に置いておきます。
ZIP ファイル内の MefSample02 フォルダが以下の例です。
この例では、本体である MefSample02 が Page1.xap、Page2.xap を読み込んでいます。そしてボタンクリックで Page1.xap や Page2.xap に入っているコントロールを表示しています。

まずは本体側の XAML と C# コードです。

<UserControl x:Class="MefSample02.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    d:DesignHeight="300" d:DesignWidth="400"
    Loaded="OnLoaded">

    <StackPanel x:Name="LayoutRoot" Background="White">
        <StackPanel Orientation="Horizontal">
            <Button x:Name="button1" Content="Page1" Click="button1_Click"/>
            <Button x:Name="button2" Content="Page2" Click="button2_Click"/>
        </StackPanel>
        <StackPanel x:Name="panel1"/>
    </StackPanel>
</UserControl>
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;
using System.Windows;
using System.Windows.Controls;

namespace MefSample02
{
    public partial class MainPage : UserControl
    {
        [ImportMany("Page", AllowRecomposition = true)]
        public List<FrameworkElement> controls = new List<FrameworkElement>();

        public MainPage()
        {
            InitializeComponent();
        }

        private void OnLoaded(object sender, RoutedEventArgs e)
        {
            var catalog = new AggregateCatalog();
            catalog.Catalogs.Add(CreateCatalog("Page1.xap"));
            catalog.Catalogs.Add(CreateCatalog("Page2.xap"));
            var container = new CompositionContainer(catalog);
            container.ComposeParts(this);
        }

        private DeploymentCatalog CreateCatalog(string uri)
        {
            var deploymentCatalog = new DeploymentCatalog(uri);
            deploymentCatalog.DownloadCompleted += (s, e) => { /*ダウンロード完了イベント*/ };
            deploymentCatalog.DownloadProgressChanged += (s, e) => { /*ダウンロード進行中*/ };
            deploymentCatalog.DownloadAsync();
            return deploymentCatalog;
        }

        private void button1_Click(object sender, RoutedEventArgs e)
        {
            NavigatePage("Page1");
        }

        private void button2_Click(object sender, RoutedEventArgs e)
        {
            NavigatePage("Page2");
        }

        private void NavigatePage(string pageName)
        {
            this.panel1.Children.Clear();
            foreach (var page in this.controls)
            {
                if (page.Name == pageName)
                {
                    this.panel1.Children.Add(page);
                    break;
                }
            }
        }
    }
}

controls フィールドに ImportMany 属性を付けて、ここにインポートされるようにしています。前回と違って controls は FrameworkElement のコレクションなので Import では無く ImportMany 属性を使っています。
そして、インポートの解決をしているのが OnLoaded イベントの

var catalog = new AggregateCatalog();
catalog.Catalogs.Add(CreateCatalog("Page1.xap"));
catalog.Catalogs.Add(CreateCatalog("Page2.xap"));
var container = new CompositionContainer(catalog);
container.ComposeParts(this);

の部分です。
カタログは CreateCatalog() メソッドで作っています。DeploymentCatalog クラスという、XAP を読み込んでインポート元として使えるようにしてくれるそのものズバリなクラスが用意されていますからそれを使っています。なお、DeploymentCatalog クラスは今のところ Silverlight 4 にしかありません。(MEF は .NET Framework 4 にも入ってますが DeploymentCatalog クラスは .NET Framework 4 には無いみたいです)
AggregateCatalog は複数のカタログをひとつに束ねるカタログです。
そしてコンテナを作り、this へのインポートを実行を依頼しています。

前回紹介したようにデフォルトのコンテナを使うこともできます。
その場合は以下のようになります。

var catalog = new AggregateCatalog();
catalog.Catalogs.Add(CreateCatalog("Page1.xap"));
catalog.Catalogs.Add(CreateCatalog("Page2.xap"));
CompositionHost.Initialize(catalog);
CompositionInitializer.SatisfyImports(this);

実は CompositionHost.Initialize() メソッドには複数のカタログを渡せるようになっているので、それを使うと AggregateCatalog で束ねてやる必要も無くなります。以下のような感じ。

CompositionHost.Initialize(
    CreateCatalog("Page1.xap"),
    CreateCatalog("Page2.xap"));
CompositionInitializer.SatisfyImports(this);

ソースの説明に戻って、残りの部分はボタンクリックイベントでコントロールを表示しているだけです。
button1_Click()、button2_Click() イベントで、MEF が controls フィールドに格納してくれた Page1.xap、Page2.xap の中のコントロールを名前で探して panel1 に配置しています。(名前で探せるように Page1.xap、Page2.xap の中の UserControl には x:Name=”Page1” のように名前を付けてあります)

本体側でやっているのは以上です。
続いてエクスポートする Page1、Page2 側。

Page1 という名前の Silverlight アプリケーションのプロジェクトを作ります。これをビルドすると Page1.xap という名前の XAP ファイルができあがることになります。
その Page1 の XAML とコードは以下のような感じです。

<UserControl x:Class="Page1.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    d:DesignHeight="300" d:DesignWidth="400"
    x:Name="Page1">

    <Grid x:Name="LayoutRoot" Background="Blue">
        <TextBlock>Page1</TextBlock>
    </Grid>
</UserControl>
using System.ComponentModel.Composition;
using System.Windows;
using System.Windows.Controls;

namespace Page1
{
    [Export("Page", typeof(FrameworkElement))]
    public partial class MainPage : UserControl
    {
        public MainPage()
        {
            InitializeComponent();
        }
    }
}

見ての通りかなり簡単なものです。
MEF のためにやっていることと言えば MainPage クラスに Export 属性を付けていることだけです。
Export 属性に typeof(FrameworkElement) と書いていますが、これには意味があります。MEF では名前と型でインポート・エクスポートの解決を行うようです。Import の側が List<FrameworkElement> と FrameworkElement のコレクションになっていますので、Export する側でも typeof(FrameworkElement) として FrameworkElement 型でエクスポートすることを明示してやらないといけません。これがないと MainPage 型としてエクスポートすることになってしまい、インポートする側と型が一致しないのでインポートしてくれなくなっちゃいます。
あと、ボタンクリック時に名前で探せるように XAML の UserControl に x:Name=”Page1” を追加してあります。

Page2.xap も内容的には同じようなものです。

こんな風にすると XAP を読み込んでその中のクラスやプロパティをインポートすることができます。
今回は UserControl をエクスポートしていますが、MEF は単に 「クラスやプロパティをエクスポート・インポートする仕組み」 ですから使い方次第でいろんなことに応用できるんじゃないかと思います。

ちょっと注意
DeploymentCatalog は非同期で XAP をダウンロードします。
なので、上記のサンプルで言うと container.ComposeParts(this) を実行したからと言ってその直後に controls フィールドが更新されるとは限りません。XAP のダウンロードに時間がかかった場合には遅れて非同期で controls フィールドが更新されます。また、順序もダウンロードが速く終わった順になるようです。なので controls フィールドの内容は Page1、Page2 の順かもしれませんし、Page2、Page1 の順かもしれません。
必要であれば、DeploymentCatalog の DownloadCompleted イベントなんかを使えばダウンロード完了を知ることができます。(サンプルでは何もしない空のハンドラを指定してるだけですが)

なお、サンプルでは名前でコントロールを探すようにしているのでダウンロードに時間がかかってまだ該当するコントロールが読み込まれていない場合にもエラーにはならず単に何も表示されないだけとなります。また、名前で探すので controls の格納順も問題になりません。
ダウンロードに時間がかかるさまをテストしたい場合は Page1 プロジェクトなどに何でもいいのでサイズの大きいファイルを追加して、そのファイルのビルドアクションを 「コンテンツ」 にすればいいんじゃないかと思います。こうすると XAP ファイルにそのファイルが含まれるので XAP ファイルのサイズを簡単に大きくすることができます。

ん?あれ?
よくよく考えたら、これって非同期で controls フィールドの内容が更新されるってことだよな。
ということは何も考えずに controls フィールドにアクセスしちゃまずいってことになるな。
MEF って controls フィールドを更新するときに SyncRoot で look してくれてるのかな?そうじゃないとどうしようも無くなってしまうような気がする。うーん、どうなんだろ?

最後に
と、MEF の基本的なところと XAP を動的にダウンロードして読み込むサンプルを示してみました。
ざっくりしたものではありますが、MEF がどんなものなのかはわかって頂けるんじゃないかと。
私もよくわかってませんが、他にもいろいろとできたりします。今回は Import 属性、Export 属性を使うやり方にしましたがリフレクションベースで指定する方法もあるようです。あとは、Lazy<T> を活用するとより柔軟なインポートができるとかなんとか。。。
あっ、そうそう、メソッドを Export することもできます。この場合の Import の型は Action、要するにデリゲートで受け取ることになります。使い方によってはおもしろいかもしれません。

0 件のコメント:

コメントを投稿