In-Depth

Using MEF to Expose Interfaces in Your Silverlight MVVM Apps

Managed Extensibility Framework (MEF) lets you expose interfaces and classes in your Silverlight apps without having to expose the real implementation. See how it works.

 

While many developers may think of Silverlight as a Web-centric technology, in practice it has become a great platform for building any type of application. Silverlight has built-in support for concepts such as data binding, value converters, navigation, out-of-browser operation and COM Interop, making it relatively straightforward to create all kinds of apps. And when I say all kinds, I also mean enterprise apps.

Creating a Silverlight application with the Model-View-ViewModel (MVVM) pattern gives you, in addition to the features already in Silverlight, the advantages of greater maintainability, testability and separation of your UI from the logic behind it. And, of course, you don't have to figure all this out on your own. There's a wealth of information and tools out there to help you get started. For example, the MVVM Light Toolkit is a lightweight framework for implementing MVVM with Silverlight and Windows Presentation Foundation (WPF), and WCF RIA Services helps you easily access Windows Communication Foundation (WCF) services and databases thanks to code generation.

You can take your Silverlight application a step further with the Managed Extensibility Framework, also known as MEF. This framework provides the plumbing to create extensible applications using components and composition.

In the rest of the article I'll show you how to use MEF to get centralized management of the View and ViewModel creation. Once you have this, you can go much further than just putting a ViewModel in the DataContext of the View. All this will be done by customizing the built-in Silverlight navigation. When the user navigates to a given URL, MEF intercepts this request, looks at the route (a bit like ASP.NET MVC), finds a matching View and ViewModel, notifies the ViewModel of what's happening and displays the View.

Getting Started with MEF
Because MEF is the engine that will connect all the parts of this example, it's best to start with it. If you're not familiar with MEF already, start by reading Glenn Block's article, "Building Composable Apps in .NET 4 with the Managed Extensibility Framework," in the February 2010 issue of MSDN Magazine.

First you need to correctly configure MEF when the application starts by handling the Startup event of the App class:

private void OnStart(object sender, StartupEventArgs e) {
  // Initialize the container using a deployment catalog.
  var catalog = new DeploymentCatalog();
  var container = CompositionHost.Initialize(catalog);
  // Export the container as singleton.
  container.ComposeExportedValue<CompositionContainer>(container);
  // Make sure the MainView is imported.
  CompositionInitializer.SatisfyImports(this);
}

The deployment catalog makes sure all assemblies are scanned for exports and is then used to create a CompositionContainer.

Because the navigation will require this container to do some work later on, it's important to register the instance of this container as an exported value. This will allow the same container to be imported whenever required. Another option would be to store the container as a static object, but this would create tight coupling between the classes, which isn't suggested.

Extending Silverlight Navigation
Silverlight Navigation Application is a Visual Studio template that enables you to quickly create applications that support navigation using a Frame that hosts the content. The great thing about Frames is that they integrate with the Back and Forward buttons of your browser and they support deep linking. Look at the following:

<navigation:Frame x:Name="ContentFrame" 
  Style="{StaticResource ContentFrameStyle}" 
  Source="Customers" 
  NavigationFailed="OnNavigationFailed">
  <i:Interaction.Behaviors>
    <fw:CompositionNavigationBehavior />
  </i:Interaction.Behaviors>
</navigation:Frame>

This is just a regular frame that starts by navigating to Customers. As you can see, this Frame doesn't contain a UriMapper (where you could link Customers to a XAML file, such as /Views/Customers.aspx).

The only thing it contains is my custom behavior, CompositionNavigationBehavior. A behavior (from the System.Windows.Interactivity assembly) allows you to extend existing controls, such as a Frame in this case.

Listing 1 shows the behavior. Let's take a look at what this CompositionNavigationBehavior does. The first thing you can see is that the behavior wants a CompositionContainer and a CompositionNavigationLoader because of the Import attributes. The constructor then forces the Import using the SatisfyImports method on the CompositionInitializer. Note that you should only use this method when you don't have another choice, as it actually couples your code to MEF.

When the Frame is attached, a NavigationService is created and wrapped around the Frame. Using ComposeExportedValue, the instance of this wrapper is registered in the container.

When the container was created, the instance of this container was also registered in itself. As a result, an Import of Composition-Container will always give you the same object; this is why I used ComposeExportedValue in the Startup event of the App class. Now the CompositionNavigationBehavior asks for a CompositionContainer using the Import attribute and will get it after SatisfyImports runs.

When registering the instance of INavigationService the same thing happens. It's now possible from anywhere in the application to ask for an INavigationService (that wraps around a Frame). Without having to couple your ViewModels to a frame you get access to the following:

public interface INavigationService {
  void Navigate(string path);
  void Navigate(string path, params object[] args);
}

Now, let's assume you have a ViewModel showing all of your customers and this ViewModel should be able to open a specific customer. This could be achieved using the following code:

[Import]
public INavigationService NavigationService { 
  get; set; 
}

private void OnOpenCustomer() {
  NavigationService.Navigate(
    "Customer/{0}", SelectedCustomer.Id);
}

But before jumping ahead, let's discuss the SetContentLoader method in the CompositionNavigationBehavior. It changes the ContentLoader of the Frame. This is a perfect example of the support for extensibility in Silverlight. You can provide your own

ContentLoader (that implements the INavigationContentLoader interface) to really provide something to show in the Frame.

Now that you can see how things start falling into place, the following topic -- extending MEF -- will become clear.

Back to Extending MEF
The goal here is that you can navigate to a certain path (be it from the ViewModel or your browser address bar) and the Composition-NavigationLoader does the rest. It should parse the URI, find a matching ViewModel and a matching View, and combine them.

Normally you'd write something like this:

[Export(typeof(IMainViewModel))]
public class MainViewModel

In this case it would be interesting to use the Export attribute with some extra configuration, referred to as metadata (Listing 2 shows an example). In addition to the ViewModel interface, this attribute allows you to define a navigation path such as Customer/{Id}. Then it will process this path using Customer as Key and {Id} as one of the arguments. Here's an example of how this attribute is used:

[ViewModelExport(typeof(ICustomerDetailViewModel), 
  "Customer/{id}")]
public class CustomerDetailViewModel 
  : ICustomerDetailViewModel

Before continuing, there are a few important things to note. First, your attribute should be decorated with the [MetadataAttribute] to work correctly. Second, your attribute should implement an interface with the values you'll want to expose as metadata. And finally, mind the constructor of the attribute -- it passes a type to the base constructor. The class that's decorated with this attribute will be exposed using this type. In the case of my example, this would be IViewModel.

That's it for exporting the ViewModels. If you want to import them somewhere, you should be writing something like this:

[ImportMany(typeof(IViewModel))]
public List<Lazy<IViewModel, IViewModelMetadata>> ViewModels { 
  get; 
  set; 
}

This will give you a list that contains all exported ViewModels with their respective metadata, allowing you to enumerate the list and maybe pick out only the ones of interest to you (based on the metadata). In fact, the Lazy object will make sure that only the ones of interest are actually instantiated. The View will need something similar:

[MetadataAttribute]
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class ViewExportAttribute : 
  ExportAttribute, IViewMetadata {

  public Type ViewModelContract { get; set; }
  public ViewExportAttribute() : base(typeof(IView)) {
  }
}

This attribute allows you to set the contract of the ViewModel to which the View should be linked.

Here's an example of AboutView:

[ViewExport(ViewModelContract = typeof(IAboutViewModel))]
public partial class AboutView : Page, IView {
  public AboutView() {
    InitializeComponent();
  }
}

A Custom INavigationContentLoader
Now that the overall architecture has been set up, let's take a look at controlling what's loaded when a user navigates. To create a custom content loader, the following interface needs to be implemented:

public interface INavigationContentLoader {
  IAsyncResult BeginLoad(Uri targetUri, Uri currentUri, 
    AsyncCallback userCallback, object asyncState);
  void CancelLoad(IAsyncResult asyncResult);
  bool CanLoad(Uri targetUri, Uri currentUri);
  LoadResult EndLoad(IAsyncResult asyncResult);
}

The most important part of the interface is the BeginLoad method, because this method should return an AsyncResult containing the item that will be displayed in the Frame. Listing 3 shows the implementation of the custom INavigationContentLoader.

As you can see, a lot happens in this class -- but it's actually simple. The first thing to notice is the Export attribute. This is required to be able to import this class in the CompositionNavigationBehavior.

The most important parts of this class are the ViewExports and ViewModelExports properties. These enumerations contain all exports for the Views and the ViewModels, including their metadata.

Instead of using a Lazy object I'm using an ExportFactory. This is a huge difference! Both classes will only instantiate the object when required, but the difference is that with the Lazy class you can only create a single instance of the object. The ExportFactory (named after the Factory pattern) is a class that allows you to request a new instance of the type of object whenever you feel like it.

Finally, there's the BeginLoad method. This is where the magic happens. This is the method that will provide the Frame with the content to display after navigating to a given URI.

Creating and Processing Objects
Let's say you tell the frame to navigate to Customers. This will be what you'll find in the targetUri argument of the BeginLoad method.

Once you have this you can get to work.

The first thing to do is find the correct ViewModel. The View-Model-Exports property is an enumeration that contains all the exports with their metadata. Using a lambda expression you can find the correct ViewModel based on its key. Remember the following:

[ViewModelExport(typeof(ICustomersViewModel), "Customers")]
public class CustomersViewModel : 
  ContosoViewModelBase, ICustomersViewModel

Well, imagine you navigate to Customers. Then the following code will find the right ViewModel:

var viewModelMapping = ViewModelExports.
  FirstOrDefault(o => o.Metadata.Key.Equals("Customers", 
  StringComparison.OrdinalIgnoreCase));

Once the ExportFactory is located, the same thing should happen for the View. However, instead of looking for the navigation key, you look for the ViewModelContract as defined in both the ViewModelExportAttribute and the ViewModelAttribute:

[ViewExport(ViewModelContract = typeof(IAboutViewModel))
public partial class AboutView : Page

Once both ExportFactories are found, the hard part is over. Now the CreateExport method allows you to create a new instance of the View and the ViewModel:

var viewFactory = viewMapping.CreateExport(); 
var view = viewFactory.Value as Control; 
var viewModelFactory = viewModelMapping.CreateExport(); 
var viewModel = viewModelFactory.Value as IViewModel;

After both the View and the ViewModel have been created, the ViewModel is stored in the DataContext of the View, starting the required data bindings. And the OnLoaded method of the ViewModel is called to notify the ViewModel that all the heavy lifting has been done, and also that all Imports -- if there are any -- have been imported.

You shouldn't underestimate this last step when you're using the Import and ImportMany attributes. In many cases you'll want to do something when creating a ViewModel, but only when everything has been loaded correctly. If you're using an ImportingConstructor you definitely know when all Imports were imported (that would be when the constructor is called). But when working with the Import/ImportMany attributes, you should start writing code in all your properties to set flags in order to know when all properties have been imported. In this case the OnLoaded method solves this issue for you.

Passing Arguments to the ViewModel
Take a look at the IViewModel interface, and pay attention to the OnNavigated method:

public interface IViewModel {
  void OnLoaded();
  void OnNavigated(NavigationArguments args);
  string GetTitle();
}

When you navigate to Customers/1, for example, this path is parsed and the arguments are combined in the NavigationArguments class (this is just a Dictionary with extra methods like GetInt, GetString and so on). Because it's mandatory that each ViewModel implements the IViewModel interface, it's possible to call the OnNavigated method after resolving the ViewModel:

// Get navigation values. 
var values = viewModelMapping.Metadata.GetArgumentValues(targetUri); 
   viewModel.OnNavigated(values);

When the CustomersViewModel wants to open a Customer-Detail-ViewModel, the following happens:

NavigationService.Navigate("Customer/{0}", SelectedCustomer.Id);

These arguments then arrive in the CustomerDetailViewModel and can be used to pass to the DataService, for example:

public override void OnNavigated(NavigationArguments args) {
  var id = args.GetInt("Id");
  if (id.HasValue) {
    Customer = DataService.GetCustomerById(id.Value);
  }
}

The Final Chores
If the View is a Page or a ChildWindow, the title of this control will also be extracted from the IViewModel object. This allows you to dynamically set the titles of your Pages and Child-Windows based on the current customer.

After all these great things, there's one last step. If the View is a ChildWindow the window should be displayed. But if the ViewModel implements IClosableViewModel, the CloseView event of this ViewModel should be linked to the Close method on the ChildWindow.

The IClosableViewModel interface is simple:

public interface IClosableViewModel : IViewModel {
  event EventHandler CloseView;
}

Processing the ChildWindow is also trivial. When the ViewModel raises the CloseView event, the Close method of the ChildWindow gets called. This allows you to indirectly connect the ViewModel to the View:

// Close the ChildWindow if the ViewModel requests it.
var closableViewModel = viewModel as IClosableViewModel;
if (closableViewModel != null) {
  closableViewModel.CloseView += (s, e) => { 
    window.Close(); 
  };
}

// Show the window.
window.Show();

If the View isn't a ChildWindow, then it should simply be made available in the IAsyncResult. This will show the View in the Frame. There. Now you've seen the whole process of how the View and ViewModel are constructed.

Using the Example Code
The code download contains an MVVM application using this type of custom navigation with MEF. This article should've provided a good idea of how the sample works. For a deeper understanding, play with the code and customize it for your own applications. You'll see how powerful and flexible MEF can be.

comments powered by Disqus
Most   Popular
Upcoming Events

.NET Insight

Sign up for our newsletter.

Terms and Privacy Policy consent

I agree to this site's Privacy Policy.