かずきのBlog@hatena

日本マイクロソフトに勤めています。XAML + C#の組み合わせをメインに、たまにASP.NETやJavaなどの.NET系以外のことも書いています。掲載内容は個人の見解であり、所属する企業を代表するものではありません。

NavigationFrameworkとMEFの連携

Silverlight 4 + MEF + NavigationFrameworkのお話。
MEFを使うと、動的に読み込んだXAPからクラスのインスタンスを取得したり出来るけど、これがNavigationFrameworkと、すこぶる相性が悪いです。どう悪いかって、例外が出て動かないくらい悪いです。

ということで、Pageクラスを複数のXAPに配置しておいて、それをナビゲーションするという事は諦めたほうがよさげという結論に至ります。ついでにいうと、NavigationFrameworkにPageクラスのインスタンスの生成を任せると、MEFによるImportとかExportの範疇の外での出来事なので連携しづらいことこの上ないです。
PageクラスのコンストラクタあたりでCompositionInitializer.SatisfyImports(this);するとかしないといけません。

ということで、今回考えたのはPageは静的に読み込めるクラスにしておいて、パラメータでMEFの管理するUserControlの名前渡してやって、それを表示するようにすればいいんじゃないかということです!
文章だけでの説明は疲れるので、コードをのせつつ解説していきます。

UserControlにつける属性

ExportAttributeを拡張して、NavigateExportAttributeというものを作ります。

[MetadataAttribute]
[AttributeUsage(AttributeTargets.Class)]
public class NavigateExportAttribute : ExportAttribute
{
    // コントラクト名
    public const string Contract = "##NaviNavi##";
    // NavigationFrameworkのパラメータで渡される名前
    public string Name { get; set; }
    // ページタイトル
    public string Title { get; set; }

    public NavigateExportAttribute(string name, string title) : base(Contract, typeof(UserControl))
    {
        this.Name = name;
        this.Title = title;
    }
}

こいつのメタデータをタイプセーフに読み取るためのインターフェースも作成します。

public interface INavigateMetadata
{
    string Name { get; }
    string Title { get; }
}

この属性をUserControlを継承したクラスにつけることで、コントラクト名が##NaviNavi##で型がUserControlでExportされるようになります。

MEFのコンテンツをホストするページの作成

次に、MEFのコンテナが管理するUserControlを表示するためのPageを作成します。MEFHostPageという名前で作ってみました。XAML側は、ContentControlを1つもつだけの単純なつくりです。このContentControlにUserControlをはめ込んで見た目をとっかえたりします。

<navigation:Page x:Class="MEFSL.Commons.Navigations.MEFHostPage" 
           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"
           xmlns:navigation="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Navigation"
           d:DesignWidth="640" d:DesignHeight="480">
    <ContentControl x:Name="host" />
</navigation:Page>

コードビハインドも作成していきます。OnNavigatedToメソッドで、MEFから画面遷移する対象のUserControlを探し当てて表示しています。nameという名前のパラメータにMEFで管理されているUserControlの名前が渡ってくるという前提で作っています。

using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Linq;
using System.Windows.Controls;
using System.Windows.Navigation;

namespace MEFSL.Commons.Navigations
{
    public partial class MEFHostPage : Page
    {
        // NavigateExportされたUserControlをImportする
        [ImportMany(NavigateExportAttribute.Contract, AllowRecomposition=true)]
        public IEnumerable<Lazy<UserControl, INavigateMetadata>> Pages { get; set; }

        public MEFHostPage()
        {
            InitializeComponent();
            // MEFにImportしてもらう
            CompositionInitializer.SatisfyImports(this);
        }

        protected override void OnNavigatedTo(NavigationEventArgs e)
        {
            // パラメータから名前を取得して
            var name = this.NavigationContext.QueryString["name"];
            // メタデータのNameとパラメータのnameが一致するものを抜き出して
            var target = Pages.Single(lazy => lazy.Metadata.Name == name);
            // タイトルを設定して
            this.Title = target.Metadata.Title;
            // ContentControlにUserControlを差し込む
            host.Content = target.Value;
        }

    }
}

これで、下準備完了です。

使い方

適当にUserControlを作成してクラスに、NavigateExport属性とPartCreationPolicy属性をつけます。HomeViewという名前のクラスを作ってHomeという名前でHome Pageというタイトルを指定する場合は以下のようになります。

using System.ComponentModel.Composition;
using System.Windows.Controls;
using MEFSL.Commons.Navigations;

namespace MEFSL.Views
{
    // 名前はHomeでタイトルはHome Page
    [NavigateExport("Home", "Home Page")]
    // インスタンスは毎回作らないと同じ画面に2回遷移できないので
    // インスタンスはNonSharedに設定する
    [PartCreationPolicy(CreationPolicy.NonShared)]
    public partial class HomeView : UserControl
    {
        public HomeView()
        {
            InitializeComponent();
        }
    }
}

このUserControlの画面に遷移しようとした場合は、以下のようなURIをHyperlinkButtonのNavigateUriに設定すればいけます。

/MEFSL.Commons;component/Navigations/MEFHostPage.xaml?name=Home

毎回指定するのはだるいので、FrameのUriMapperに以下のようなマッピングを登録しておけば楽ちんです。

<!-- /MEF/Home のようにアクセスが出来るようになる -->
<uriMapper:UriMapping Uri="/MEF/{name}" MappedUri="/MEFSL.Commons;component/Navigations/MEFHostPage.xaml?name={name}" />

UserControl内でコードから画面遷移したいんだけども・・・

この方法をとると、UserControl内でコードから画面遷移するというのが、ちょっと困りものです。
苦し紛れですが、以下のようなクラスを作ってお茶を濁すことが出来ます。

using System;
using System.ComponentModel.Composition;
using System.Windows.Controls;

namespace MEFSL.Commons.Navigations
{
    // 画面遷移処理
    public interface INavigator
    {
        void Navigate(Uri uri);
        void Navigate(string uri);
    }

    [Export(typeof(INavigator))]
    public class Navigator : INavigator
    {
        // どこかでExportされたFrameをImportする
        [Import]
        public Frame Frame { get; set; }

        // ImportされたFrameを対象に画面遷移する
        public void Navigate(Uri uri)
        {
            Frame.Navigate(uri);
        }

        public void Navigate(string uri)
        {
            Navigate(new Uri(uri, UriKind.Relative));
        }
    }
}

このクラスを使うために、Frameを持ってる画面でExport属性をつけたFrame型のプロパティを定義する必要があります。

// frameという名前でFrameコントロールが定義されているとする
[Export]
public Frame Frame { get { return frame; } }

このプロパティが定義されているクラスもMEFで管理されてる必要があるため全体としては以下のようになります。

// こいつは唯一のインスタンスになるようにNonSharedしない
// じゃないとFrameのExportも毎回違うインスタンスが渡されるため
// 悲しい結果になる
[Export]
public partial class Shell : UserControl
{
    [Export]
    public Frame Frame { get { return frame; } }
}

INavigatorを使うUserControl側では単純にImportしてNavigateメソッドを呼ぶだけでOKです。

[Import(typeof(INavigator))]
public INavigator Navigator { get; set; }

// Biz1Menuという名前でExportされているUserControlに遷移する場合
private void button1_Click(object sender, RoutedEventArgs e)
{
    Navigator.Navigate("/MEF/Biz1Menu");
}


複数のXAPに分割しつつNavigationFrameworkのメリットであるURLがページ毎に作られるという点もばっちりなので割と満足です。