MEFを使ったXAPのオンデマンドロードとNavigation Frameworkの組み合わせ

Silverlight 4, MEF and MVVM: MEFModules, Dynamic XAP Loading and Navigation Applicationsを参考に、XAPを動的にローディングしつつも画面遷移はNavigation Frameworkをつかって簡単に別XAPのページに移動する方法を試行錯誤してみました(リンク先の例だと別XAPを動的にロードしてそこに含まれるコントロールを使っているけど、Navigation Frameworkで遷移していない)。
http://www.davidpoll.com/のブログの内容やサンプルもかなり参考になりました。

肝は、System.Windows.Navigation.INavigationContentLoaderというインタフェースですね。INavigationContentLoaderを使うと、ページがフレームにロードされるときの処理をフックできるので、ここで必要に応じてXAPを読み込みます。こんなかんじで実装してみました。メインのXAPに含まれるものについてはPageResourceContentLoaderに委譲しています。

    public class OnDemandPageResourceContentLoader : INavigationContentLoader
    {
        readonly PageResourceContentLoader _loader = new PageResourceContentLoader();

        readonly DeploymentCatalogService _service = new DeploymentCatalogService();

        public OnDemandPageResourceContentLoader()
        {
            CompositionInitializer.SatisfyImports(this);
        }

        // 別のXapに含まれるPageをインポート
        [ImportMany(AllowRecomposition = true)]
        public IEnumerable<ExportFactory<Page, IExportPageMetadata>> PageFactories { get; set; }

    // 別Xapへのリクエストか判定して、そうだったらXapの読み込み
        public IAsyncResult BeginLoad(Uri targetUri, Uri currentUri, AsyncCallback userCallback, object asyncState)
        {
            var result = CreateAsyncResult(targetUri, asyncState);
            AsyncCallback userCallbackWrapper = originalResult =>
                                                {
                                                    result.AsyncResult = originalResult;
                                                    userCallback(result);
                                                };
            if (result.IsModulePage)
            {
                _service.AddXap(result.XapName,
                                    e =>
                                    {
                                        result.AsyncResult = _loader.BeginLoad(targetUri, currentUri, userCallbackWrapper,
                                                                               asyncState);
                                        result.Exception = e.Error;
                                    }
                    );
            }
            else
            {
                result.AsyncResult = _loader.BeginLoad(targetUri, currentUri, userCallbackWrapper, asyncState);
            }
            return result;
        }

    // このクラスに対応するAsyncResultを作成。
        // Uriの文字列を解析して他のXAPへのアクセスかどうか判定できるようにする
        private OnDemandPageResourceContentLoaderAsyncResult CreateAsyncResult(Uri targetUri, object asyncState)
        {
            var result = new OnDemandPageResourceContentLoaderAsyncResult(asyncState);
            if (!targetUri.IsAbsoluteUri)
            {
                string path = targetUri.OriginalString;
                if (path != null)
                {
                    if (path.StartsWith("/"))
                    {
                        var pos = path.IndexOf(";component");
                        if (pos != -1)
                        {
                            result.IsModulePage = true;
                            result.ModuleName = path.Substring(1, pos - 1);
                            result.XapName = result.ModuleName + ".xap";
                            path = path.Substring(pos + 10, path.Length - (pos + 10));
                            var pos2 = path.IndexOf("?");
                            result.NavigationUri = pos2 != -1 ? path.Substring(pos2, pos2 - 1) : path;
                        }
                    }
                }
            }
            return result;
        }

        public void CancelLoad(IAsyncResult asyncResult)
        {
            var result = asyncResult as OnDemandPageResourceContentLoaderAsyncResult;
            if (result == null)
            {
                return;
            }
            _loader.CancelLoad(result.AsyncResult);
        }

        // 別XapへのリクエストだったらインポートされたPageを探してそれを読み込む
        public LoadResult EndLoad(IAsyncResult asyncResult)
        {
            var result = asyncResult as OnDemandPageResourceContentLoaderAsyncResult;
            if (result == null)
            {
                throw new ArgumentException("asyncResult");
            }
            if (result.Exception != null)
            {
                throw result.Exception;
            }
            if (result.IsModulePage)
            {
                var pageFactory = PageFactories.Single(factory =>
                                                           {
                                                               var metadata = factory.Metadata;
                                                               return result.ModuleName == metadata.ModuleName &&
                                                                      result.NavigationUri == metadata.NavigationUri;
                                                           });
                return new LoadResult(pageFactory.CreateExport().Value);
            }
            return _loader.EndLoad(result.AsyncResult);
        }

        public bool CanLoad(Uri targetUri, Uri currentUri)
        {
            return true;
        }

    }

別XAPのページはモジュール名とURIを指定してエクスポートしておきます。

    // ExportAttributeを継承した属性をつくってエクスポート
    [ExportPage(ModuleName = "MultiXapsModule", NavigationUri = "/Views/Sample.xaml")]
    public partial class Sample : Page
    {
         ...
    }

フレームを使うところでは、FrameのContentLoaderプロパティにINavigationContentLoaderの実装を指定します。

    <navigation:Frame x:Name="ContentFrame" Style="{StaticResource ContentFrameStyle}" 
                      Source="/Home" Navigated="ContentFrame_Navigated" NavigationFailed="ContentFrame_NavigationFailed" >
        <navigation:Frame.UriMapper>
            <uriMapper:UriMapper>
                <uriMapper:UriMapping Uri="" MappedUri="/Views/Home.xaml"/>
                <uriMapper:UriMapping Uri="/{module}Module/{page}" MappedUri="/{module}Module;component/Views/{page}.xaml" />
                <uriMapper:UriMapping Uri="/{pageName}" MappedUri="/Views/{pageName}.xaml"/>
            </uriMapper:UriMapper>
        </navigation:Frame.UriMapper>
        <navigation:Frame.ContentLoader>
            <my:OnDemandPageResourceContentLoader />
        </navigation:Frame.ContentLoader>
    </navigation:Frame>

これで、あるページから次のように呼べば別XAPのページに遷移できます。

    NavigationService.Navigage(new Uri("/MultiXapsModule/Sample"));

非常にかんたんなサンプルでしか試していませんが、うまく動きました。

メインのXAPに含まれるページについてはPageResourceContentLoaderを使いましたが、メインのXAPに含まれるページについてもMEFで管理したほうがプログラミングモデルが統一されていいかもしれないです。