(X) Hide this
    • Login
    • Join
      • Say No Bots Generate New Image
        By clicking 'Register' you accept the terms of use .

Navigating between Pages in Different Xaps (by using MEF)

(6 votes)
Pencho Popadiyn
>
Pencho Popadiyn
Joined Apr 30, 2008
Articles:   22
Comments:   97
More Articles
18 comments   /   posted on Aug 02, 2010
Categories:   Patterns and Practices , General

This article is compatible with the latest version of Silverlight.

1. The Problem

In this article I’ll show you a simple solution for navigation between pages in different Xaps, by using Managed Extensibility Framework (MEF). Recently I hit the following issue, while playing with MEF. Suppose that you have a Silverlight Navigation Application. Your application is partitioned in different modules (plugins, extensions, add-ons or whatever). Let's imagine that your application has three plugins – Orders plugin, Products plugin and Suppliers plugin, as shown on the snapshot below.

 

Each one of your plugins contains an entry page (this is the so called Entry Point for the plugin). I’ll show you some technical details about the implementation later in the article. For now let’s stick to the main problem. Your main project (the host project) has static references to two of the plugins, for example OrdersPlugin and ProductsPlugin.

 

Since both of the plugins are referenced by the main project, during compile time, they are included in the .Xap file! And once you start the application they are automatically loaded without any problems. However, the third plugin – the SuppliersPlugin is not included in the main .XAP file. It is downloaded runtime, by using the mighty DeploymentPackageCatalog (the DeploymentPackageCatalog is not part of MEF, but its implementation is widely spread in Google).

 

If you try to navigate to the Orders page or Products page, then you won’t have any problems (it’s just like creating a regular Silverlight navigation application). However, when you try to navigate to the Suppliers page, you will see the following exception.

 

What a shame, isn’t it? :). So where is the problem? By default the Frame class from the Navigation Framework uses the PageResourceContentLoader class to load the pages. The PageResourceContentLoader class has the following limitation: it is designed to load pages from the application package (the .xap file) that correspond to a given URI. However, our Suppliers page is not in the main application package, it is part of another package, downloaded runtime. That’s why the PageResourceContentLoader won’t know about the Suppliers page, and respectively won’t be able to load it.

2. The Solution

Fortunately the System.Windows.Controls.Navigation assembly provides us with the INavigationContentLoader interface. So, as you can guess, we need a custom implementation of the INavigationContentLoader.

public class MefContentLoader : INavigationContentLoader
{
    private PageResourceContentLoader pageResourceContentLoader = new PageResourceContentLoader();
    private Uri targetUri;
 
    public IAsyncResult BeginLoad( Uri targetUri, Uri currentUri, AsyncCallback userCallback, object asyncState )
    {
        throw new NotImplementedException();
    }
 
    public bool CanLoad( Uri targetUri, Uri currentUri )
    {
        throw new NotImplementedException();
    }
 
    public void CancelLoad( IAsyncResult asyncResult )
    {
        throw new NotImplementedException();
    }
 
    public LoadResult EndLoad( IAsyncResult asyncResult )
    {
        throw new NotImplementedException();
    }
}

The trick here is that our custom content loader should know about the entry points of each plugin.

public class MefContentLoader : INavigationContentLoader
{
    [ImportMany( AllowRecomposition = true )]
    public UIProviderBase[] Plugins
    {
        get;
        set;
    }
 
    //....Rest of the code
}

For our purposes we need to implement only the BeginLoad, CanLoad and EndLoad methods, like demonstrated on the code snippet below:

public class MefContentLoader : INavigationContentLoader
{
    private PageResourceContentLoader pageResourceContentLoader = new PageResourceContentLoader();
    private Uri targetUri;
 
    public MefContentLoader()
    {
        CompositionInitializer.SatisfyImports( this );
    }
 
    [ImportMany( AllowRecomposition = true )]
    public UIProviderBase[] Plugins
    {
        get;
        set;
    }
 
    public IAsyncResult BeginLoad( Uri targetUri, Uri currentUri, AsyncCallback userCallback, object asyncState )
    {
        this.targetUri = targetUri;
        return pageResourceContentLoader.BeginLoad( targetUri, currentUri, userCallback, asyncState );
    }
 
    public bool CanLoad( Uri targetUri, Uri currentUri )
    {
        return true;
    }
 
    public void CancelLoad( IAsyncResult asyncResult )
    {
        throw new NotImplementedException();
    }
 
    public LoadResult EndLoad( IAsyncResult asyncResult )
    {
        if ( this.Plugins.Length == 0 ||
            this.Plugins.Count( p => p.EntryPage != null && p.EntryPage.Metadata.NavigateUri == targetUri.ToString() ) == 0 )
        {
            return pageResourceContentLoader.EndLoad( asyncResult );
        }
 
        IView page = this.Plugins.First( p => p.EntryPage != null && p.EntryPage.Metadata.NavigateUri == this.targetUri.ToString() ).EntryPage.CreateExport().Value;
        return new LoadResult( page );
    }
}

The tricky part comes into the EndLoad method. Once we know the entry points for each plugin, and we know the target uri, we can extract the requested page and pass it to the LoadResult.

Finally, you need to set the Frame’s ContentLoader property in XAML.

<navigation:Frame x:Name="ContentFrame"
                  Style="{StaticResource ContentFrameStyle}"
                  Source="/HomeView"
                  NavigationFailed="ContentFrame_NavigationFailed">
    <navigation:Frame.ContentLoader>
        <local:MefContentLoader />
    </navigation:Frame.ContentLoader>
    <navigation:Frame.UriMapper>
        <uriMapper:UriMapper>
            <uriMapper:UriMapping Uri=""
                                  MappedUri="/Views/HomeView.xaml" />
            <uriMapper:UriMapping Uri="/{assemblyName};component/{path}"
                                  MappedUri="/{assemblyName};component/{path}" />
            <uriMapper:UriMapping Uri="/{pageName}"
                                  MappedUri="/Views/{pageName}.xaml" />
        </uriMapper:UriMapper>
    </navigation:Frame.UriMapper>
</navigation:Frame>

Check out the link at the end of the article, in case you want to download the full source code.

3. Designing Plugin Applications

Since there isn’t a lot of information in the web, I would like to say a few words about how to design (create) plugin applications. The strong definition about the plugin is a set of software components that adds specific capabilities to a large software application. We are surrounded with such kind of applications – the most famous are Office Word, Excel, Outlook, Visual Studio. Basically, each plugin has an entry point (plugin interface). The host application deals only with the plugin interfaces (not directly with the plugin). The host application operates independently of the plugins, making it possible for the end-users to add and update plugins dynamically without the need to make changes to the host application.

Prior to .NET Framework 4, we didn't have any concrete technology for creating plugin applications. Fortunately, in .NET Framework 4, Microsoft provides us with the Managed Extensibility Framework (MEF) which is available for Silverlight, too.

So let’s come back to our demo. I am using an abstract class named UIProviderBase and this is the “plugin interface” (the entry point) for the demo. The class provides a Title (the name of the plugin), an Image (associated with the plugin) and an EntryPage – this is the initial view (the main view) of the plugin. This class serves something like a contract, saying: “Hey, if you want to plug in your module in my application, you have to implement me”.

public abstract class UIProviderBase
{
    public abstract string Title
    {
        get;
    }
    public abstract string ImageUri
    {
        get;
    }
    public abstract ExportFactory<IView, IPageMetadata> EntryPage
    {
        get;
        set;
    }
}

Each plugin should provide a UIProviderBase implementation. Additionally, that implementation should be marked with the [ExportAttribute]. Below you can see the entry point for the Suppliers plugin.

[Export( typeof( UIProviderBase ) )]
public class SuppliersUIProvider : UIProviderBase
{
    public override string Title
    {
        get
        {
            return "Suppliers";
        }
    }
    public override string ImageUri
    {
        get
        {
            return "/SuppliersPlugin;component/Images/SuppliersImage.png";
        }
    }
    [Import( ViewNames.SuppliersViewName )]
    public override ExportFactory<IView, IPageMetadata> EntryPage
    {
        get;
        set;
    }
}

On the other side, the host application is expecting an array of UIProviderBase implementations. Note the [ImportManyAttribute].

[Export( ViewModelNames.HomeViewModelName, typeof( object ) )]
public class HomeViewModel : MyViewModelBase, IPartImportsSatisfiedNotification
{
    [ImportMany( AllowRecomposition = true )]
    public UIProviderBase[] Plugins
    {
        get;
        set;
    }
 
    public void OnImportsSatisfied()
    {
        this.Plugins = this.Plugins.OrderBy( p => p.Title ).ToArray();
        this.RaisePropertyChanged( "Plugins" );
    }
}

The other goodies:

  • If you download the source code (the link is at the end of the article), you will find an implementation of the DeploymentServiceCatalog, which is downloading a XAP file by a given relative uri.
  • Note the EntryPage property in the UIProviderBase class. An instance of the page is created on demand. What I am using here is a Metadata. For more information, you could read here.
  • In the HomeView.xaml I am using a PathListBox to arrange the UI.
  • One last question I should answer is how the different plugins communicate with each other. Well, in this case you need to use the so called “global events (messages)". For example, I prefer the Messanger class from the MVVM Light Toolkit, but you can use the EventAggregator with the same success.


Subscribe

Comments

  • DonovanWoodside

    RE: Navigating between Pages in Different Xaps (by using MEF)


    posted by DonovanWoodside on Aug 02, 2010 17:47

    Source Code's link is not correct. (http://www.silverlightshow.net/admin/)

  • ppopadiyn

    RE: Navigating between Pages in Different Xaps (by using MEF)


    posted by ppopadiyn on Aug 02, 2010 17:56

    Hi DonovanWoodside,

    Sorry for the inconvenience, it is fixed now. Thanks for the note.

  • -_-

    RE: Navigating between Pages in Different Xaps (by using MEF)


    posted by middle on Aug 12, 2010 11:18

    It's great article. Did you try implement this with WCF RIA service app?

    Thanks.

    Middle

  • ppopadiyn

    RE: Navigating between Pages in Different Xaps (by using MEF)


    posted by ppopadiyn on Aug 12, 2010 11:51

    Hi Middle,

    Yes, I tried it with WCF RIA service app. Furthermore, I have a completed demo, demonstrating WCF RIA, navigation framework and mef. There is nothing special in this case - each xap has a link to the service and communicate with it. However I decided not to upload it, since Brian Noyes has pretty nice series of articles about WCF RIA. In case you are interested, you could check it here.

  • -_-

    RE: Navigating between Pages in Different Xaps (by using MEF)


    posted by Anton on Aug 13, 2010 18:08

    Hi Middle,

    Great article and it has helped me plenty to find my feet with MEF.

    But for the life of me I just can't get mine working. The Link loads perfectly and from debugging the Plugins and also the DownloadCompleted in the DeploymentCatalogServices.cs all seems fine. When I click on the link is where things just falls over, I get the error The type '', specified in the x:Class of '' could not be found in any loaded assembly. I've even used the dissambler to make sure that my export tag is correct.

    Is there anything that you can think of that I might be missing. I see that you used a Silverlight Class library and then included it in a Silverlight Application for the SupplierPackage, is there any reason for doing that? If I just use a Silverlight Application will that have an effect?

     

    Thanks in advance. 

  • -_-

    RE: Navigating between Pages in Different Xaps (by using MEF)


    posted by Anton on Aug 13, 2010 18:22

    Hi Middle,

    Just realized that I can eliminate what the cause is by adding the plugin as a reference. Once I did that it worked and the error was gone, so the download of the xap doesn't seem to work.

    Not your problem though, will try to figure it out.

    Thanks again, this article was a great help.

  • -_-

    RE: Navigating between Pages in Different Xaps (by using MEF)


    posted by foobar on Aug 28, 2010 23:51
    This compiles, but doesn't run, i.e. circles rotate forever. What gives?
  • -_-

    RE: Navigating between Pages in Different Xaps (by using MEF)


    posted by foobar on Aug 29, 2010 00:27
    Dude, why be fancy? Why not keep things simple if and/or when you demostrated concepts? After I've debugged your code, I found out you didn't include a reference to Microsoft.Expression.Drawing.dll.
  • ppopadiyn

    RE: Navigating between Pages in Different Xaps (by using MEF)


    posted by ppopadiyn on Aug 29, 2010 09:44

    Hello foobar,

    I don't understand your problem, but I think you need a reference only to Microsoft.Expression.Controls.dll, which is included in the lib folder.

  • -_-

    RE: Navigating between Pages in Different Xaps (by using MEF)


    posted by revolution on Sep 08, 2010 11:14
    I dont understand how the Orders and Products plugins are Imported upon startup (Plugins property has count = 2). Why doesnt the Suppliers plugin get loaded as well (it is instead loaded upon clicking the icon).
  • -_-

    RE: Navigating between Pages in Different Xaps (by using MEF)


    posted by Sriman on Oct 26, 2010 16:45

    Getting runtime file not found error. Could not load file or assembly 'Microsoft.Expression.Drawing, Version=4.0.5.0,

    the file is in the lib folder of the download. What is wrong?

    Thanks

    Sriman 

     

  • -_-

    Do you have a solution for F5 (reload page) when SuppliersView is visible?


    posted by swo on Apr 11, 2011 18:55

    When I navigate to SuppliersView and press F5 or click the reload button tha App crasch.

    This is because the MainPage create a new MefContentLoader but the Plugins.Length = 0.  Anny suggestion ?

     

     

  • ppopadiyn

    RE: Navigating between Pages in Different Xaps (by using MEF)


    posted by ppopadiyn on Apr 12, 2011 19:56

    Hello swo,

    Unfortunately, I couldn't find any workround for the issue. Probably, what you could do is to add a direct reference to the SuppliersPlugin.dll, and to delete the code that loads the SuppliersPluginPackage.xap in the HomeViewModel class. If I find a workaround for the problem I'll write a comment here.

  • -_-

    RE: Navigating between Pages in Different Xaps (by using MEF)


    posted by Shawn on May 25, 2011 18:51
    I am attempting to use MEF for both the Plugins and to load my ViewModel's, however when I add a view model to the sample I run off the end of a cliff. Any help/guidance would be appreciated. Thanks in advance.

    Took sample:
    1) added SuppliersViewModel
        public class SuppliersViewModel
        {
            public string SupplierName { get; set; }
        }

    2) altered the SuppliersView to use the ViewModel

        [ExportPage( ViewNames.SuppliersViewName, "/SuppliersPlugin;component/Views/SuppliersView.xaml" )]
        public partial class SuppliersView : Page, IView
        {
            public SuppliersView()
            {
                InitializeComponent();
                CompositionInitializer.SatisfyImports(this);
            }

            [Import(ViewModelNames.SuppliersViewModelName)]
            public object ViewModel
            {
                set
                {
                    DataContext = value;
                }
            }

            // Executes when the user navigates to this page.
            protected override void OnNavigatedTo( NavigationEventArgs e )
            {
            }

        }


    When I run it like this the "plugin" fails to load completely. I get the following error in the Output window:


    System.ComponentModel.Composition Warning: 1 : The ComposablePartDefinition 'SuppliersPlugin.SuppliersUIProvider' has been rejected. The composition remains unchanged. The changes were rejected because of the following error(s): The composition produced a single composition error. The root cause is provided below. Review the CompositionException.Errors property for more detailed information.

    1) No valid exports were found that match the constraint '((((exportDefinition.ContractName == "System.ComponentModel.Composition.Contracts.ExportFactory") AndAlso (exportDefinition.Metadata.ContainsKey("ExportTypeIdentity") AndAlso "System.ComponentModel.Composition.Primitives.ComposablePartDefinition".Equals(exportDefinition.Metadata.get_Item("ExportTypeIdentity")))) AndAlso (exportDefinition.Metadata.ContainsKey("NavigateUri") AndAlso System.String.IsInstanceOfType(exportDefinition.Metadata.get_Item("NavigateUri")))) AndAlso (exportDefinition.Metadata.ContainsKey("ProductDefinition") AndAlso Invoke(exportDefinition => ((((exportDefinition.ContractName == "SuppliersView") AndAlso (exportDefinition.Metadata.ContainsKey("ExportTypeIdentity") AndAlso "Common.IView".Equals(exportDefinition.Metadata.get_Item("ExportTypeIdentity")))) AndAlso (exportDefinition.Metadata.ContainsKey("NavigateUri") AndAlso System.String.IsInstanceOfType(exportDefinition.Metadata.get_Item("NavigateUri")))) AndAlso ((Not(exportDefinition.Metadata.ContainsKey("System.ComponentModel.Composition.CreationPolicy")) OrElse Any.Equals(exportDefinition.Metadata.get_Item("System.ComponentModel.Composition.CreationPolicy"))) OrElse NonShared.Equals(exportDefinition.Metadata.get_Item("System.ComponentModel.Composition.CreationPolicy")))), Convert(exportDefinition.Metadata.get_Item("ProductDefinition")))))', invalid exports may have been rejected.

    Resulting in: Cannot set import 'SuppliersPlugin.SuppliersUIProvider.EntryPage (ContractName="System.ComponentModel.Composition.Contracts.ExportFactory")' on part 'SuppliersPlugin.SuppliersUIProvider'.
    Element: SuppliersPlugin.SuppliersUIProvider.EntryPage (ContractName="System.ComponentModel.Composition.Contracts.ExportFactory") -->  SuppliersPlugin.SuppliersUIProvider -->  AssemblyCatalog (Assembly="SuppliersPlugin, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")





    If I comment out the ViewModel property, I get the following error.


    System.ComponentModel.Composition.CompositionException was unhandled by user code
      Message=The composition produced a single composition error. The root cause is provided below. Review the CompositionException.Errors property for more detailed information.

    1) Cannot call SatisfyImports on a object of type 'SuppliersPlugin.SuppliersView' because it is marked with one or more ExportAttributes.
    Parameter name: part

    Resulting in: An exception occurred while trying to create an instance of type 'SuppliersPlugin.SuppliersView'.

    Resulting in: Cannot activate part 'SuppliersPlugin.SuppliersView'.
    Element: SuppliersPlugin.SuppliersView -->  SuppliersPlugin.SuppliersView -->  AssemblyCatalog (Assembly="SuppliersPlugin, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")

    Resulting in: Cannot get export 'SuppliersPlugin.SuppliersView (ContractName="SuppliersView")' from part 'SuppliersPlugin.SuppliersView'.
    Element: SuppliersPlugin.SuppliersView (ContractName="SuppliersView") -->  SuppliersPlugin.SuppliersView -->  AssemblyCatalog (Assembly="SuppliersPlugin, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")

      StackTrace:
           at System.ComponentModel.Composition.Hosting.CompositionServices.GetExportedValueFromComposedPart(ImportEngine engine, ComposablePart part, ExportDefinition definition)
           at System.ComponentModel.Composition.Hosting.CatalogExportProvider.GetExportedValue(ComposablePart part, ExportDefinition export, Boolean isSharedPart)
           at System.ComponentModel.Composition.Hosting.CatalogExportProvider.CatalogExport.GetExportedValueCore()
           at System.ComponentModel.Composition.Primitives.Export.get_Value()
           at System.ComponentModel.Composition.ExportServices.GetCastedExportedValue[T](Export export)
           at System.ComponentModel.Composition.ExportServices.GetExportLifetimeContextFromExport[T](Export export)
           at System.ComponentModel.Composition.ExportServices.<>c__DisplayClass1f`2.<CreateStronglyTypedExportFactoryOfTM>b__1e()
           at System.ComponentModel.Composition.ExportFactory`1.CreateExport()
           at MefNavigationFramework.MefContentLoader.EndLoad(IAsyncResult asyncResult)
           at System.Windows.Navigation.NavigationService.ContentLoader_BeginLoad_Callback(IAsyncResult result)
      InnerException:
  • -_-

    RE: Navigating between Pages in Different Xaps (by using MEF)


    posted by Shawn on May 26, 2011 00:57

    ok, problem solved. (I hope)

    I played with the way I was importing/exporting the ViewModel, this seems to work. . .

        [Export(typeof(SuppliersViewModel))]
        public class SuppliersViewModel
        {
            public SuppliersViewModel()
            {
                SupplierName = "Test Supplier";
            }
            public string SupplierName { getset; }
        }
     
     
        [ExportPageViewNames.SuppliersViewName, "/SuppliersPlugin;component/Views/SuppliersView.xaml" )]
        public partial class SuppliersView : PageIView
        {
            public SuppliersView()
            {
                InitializeComponent();
            }
     
            [Import(typeof(SuppliersViewModel))]
            public object ViewModel
            {
                set
                {
                    DataContext = value;
                }
            }
     
            // Executes when the user navigates to this page.
            protected override void OnNavigatedTo( NavigationEventArgs e )
            {
            }
     
        }

     

     

  • behnaz_h70

    Re: Navigating between Pages in Different Xaps (by using MEF)


    posted by behnaz_h70 on Oct 28, 2011 11:04
    hi,
    I used MEF in my project and I have a main silverlight application that my main page is in it,and my problem is that in a sub silverlight application page with different xap -that its content is loaded in my main page-I defined a style for some controls inn my page by referencing the style source file  in App.xaml in ResourceDictionary, but when I run my solution,It throws an error :
    Cannot find a Resource with the Name/Key grid1 [Line: 1500 Position: 127]
    while visual studio recognize the style I reference,but I think Mef and main page contentLoader cann't find the style resource.could you help me to solve it?
  • master777

    Re: Navigating between Pages in Different Xaps (by using MEF)


    posted by master777 on Jan 28, 2012 16:00
    ...Hi, Could you(Author) please give us a description as in step by step explanation??? Is there any source or book about this because i need to apprehend and consume as timely manner. THANKS!
  • GaneshMahajan

    Re: Navigating between Pages in Different Xaps (by using MEF)


    posted by GaneshMahajan on Aug 14, 2013 13:42

    Hi, 

    Many thanks for this great post.

    I have used this technique in my Silverlight App which is having around 13 Plugins.

    Now my problem is that one of my plugin having its own frame and all its form's navigation done within it.

    So when click on particular link from the loaded plugin it gives me error that "xyz.xaml" is not available. can u please suggest me something that how to deal with such situation where our main project having a frame for navigation and plugin also having its own frame ??

Add Comment

Login to comment:
  *      *