After having worked on several major projects that integrated the Managed Extensibility Framework, I thought it might be beneficial to share a retrospective on how it was used what value it provided. These are all ways MEF helped my team build modular Silverlight applications.
Unlike my earlier post that covered 10 Reasons to use the Managed Extensibility Framework, this is more of an inventory of actual, specific use cases it has been implemented in real world projects, sometimes referred to as "the wild."
Inversion of Control/Dependency Injection
This was perhaps the easiest decision to make. None of these applications had any requirements for more advanced inversion of control or dependency injection than what MEF offers out of the box. Because we were using it for other reasons, and because it is now native to the framework, it was the logical choice to solve this problem. In most cases, it was as simple as defining the interface, importing it where needed and exporting it where satisfied. Other, more specific cases are described below.
Configuration
Configuration was very important and is easy to facilitate with MEF. I included this as a sub-item under IoC because it really is a side effect of inversion of control. By defining a configuration interface, all code could then reference that interface and testing could mock the values. The main application service implemented the configuration and used a combination of compile-time constants as well as parameterized values to load these. Any consumer, application-wide, then simply needed to import the interface to have access to these variables, even from dynamically loaded modules that had no concrete references to any of the assemblies that configuration lived in.
Logging
Logging is another indirect benefit of the IoC pattern that just happened to be implemented with MEF. We created an interface for logging that was satisfied by a logger and maintained the discipline of avoiding "Debug.WriteLine" and instead writing to the log file with severity, source, and additional information. This separation of concerns allowed us to use a simple logging mechanism in the beginning (it could simply write to the debug window) but gave us the flexibility to add service calls, store logs to isolated storage, or otherwise manage the logging transaction easily from one place without changing any of the rest of the application.
Region Management
Prism 4.0 has an excellent region management framework, so I want to be clear that I have nothing against it and am not about reinventing the wheel. For this project, we only had a few specific types of regions defined, so it was more lightweight to use a more specific version.
Essentially, region management consisted of a few pieces. There was an interface for a region adapter that is scoped to a type of container (Grid, or ContentControl, etc). To implement a new region adapter was as simple as inheriting the interface, adding metadata to describe the target container it would support in a MEF export, and then writing the specific events which including registering (pointing a view to a region), activating (making the view appear in the region) and deactivating (making it go away). Some of this involved manipulation of content or child elements but much of it was driven by the Visual State Manager.
An attached property defined a container as a region, and metadata on a view defined what region that view targeted. The region manager aggregated all of these parts and coordinated between them. If I needed a view activated, the region manager would use the chain of responsibility pattern to find the region adapter that owned the view, then ask it to bring it to focus.
[RegionManagerExport(typeof(ContentControl))] public class ContentRegion : RegionBase<ContentControl>
Read MEF instead of PRISM Part 2 for a detailed explanation of building custom region management, and see Advanced Silverlight Applications using MEF for an example framework.
Service Integration
A common pattern in line of business applications is to have different types of models or business domain objects that can have some common operations performed against them, such as CRUD (create, read, update, delete). For this reason, I was able to create an abstract IServiceFor<T>
style interface and define some common functions. Implementations of the contract would export themselves explicitly, and a service router handled mapping the entities to the service handler for the entity.
What this meant is that I could build a view model for an entity and simply request a service from the service router for the entity type and perform all of my generic operations without worrying about how the service was implemented. I could easily stub the service while developing until the endpoints were actually created. Some services actually interacted with isolated storage while others carried messages over the wire: these were all abstracted from the view model and brought together with MEF.
[Import] public IServiceLocator Locator { get; set; } var service = Locator.GetServiceFor<T>();
Messaging
Messaging is a very important aspect of any system. For example, consider having a filter view model with filter data that can be applied to other view models. How do these other view models keep track of when the filter changes, or access the filter data?
In this project, we handled messaging in two ways.
View Composition
Something like a filter, even when globally applied, can be considered a part of the view model that use it. It does not make sense by itself, but when it is composed as part of another view model, it has context and use. MEF was extremely helpful with this pattern because any view model that needs access to another could simply import it:
[Import] public OtherViewModel Other { get; set; }
Of course, that still doesn't allow us to know when it changed, unless we subscribe to the property changed event. That may make sense in some circumstances, but often there are messages so important that many different parts must listen to them. For this purpose, we used the EventAggregator pattern.
Event Aggregator
The event aggregator I used was based on this post, which describes an event aggregator based on Reactive Extensions (Rx). I factored it to work with Silverlight and it worked like a charm.
One common pattern was messages that caused the application to change. For example, a new filter or a new selection item. I created a special class for carrying these messages. It held the type, the payload, and an enumeration. If I wanted to publish a selection event, for example, I could create this class and place the item selected, it's type, and the enumeration for "select" and publish that.
Any "listener" can then filter like this:
var query = from evt in EventAggregator.GetEvent<MyEntityEvent>() where evt.Type.Equals(typeof(MyWatchedEntity)) && evt.Action.Equals(Actions.Select) select evt.Cast<MyWatchedEntity>(); query.Subscribe(myEntity=>{//do something});
Dynamic XAP Routing
Dynamic XAP loading is important in Silverlight applications because it is downloaded from the web and resides on the user's machine. There are two major concerns: the first is the load time for the application, and the second is the memory footprint and impact to the user's machine. Dynamic XAP loading helps by breaking the applicatin into modules. A module is loaded only when requested. Therefore, the initial application loads faster with a smaller memory footprint and grows gradually as modules are exploited.
The challenge some frameworks have is how and when to trigger the module load. I've become fond of a concept I call "dynamic XAP routing." The concept is simple: I tag views with names, either a friendly name or the full namespace. In my core or "infrastructure" project, I have a set of global constants. While the current module may not know about a view, the constants can represent the view. Another set of constants describes the XAP files that are available.
With these two pieces of information, I can then dynamically route via a deployment service that uses deployment catalogs to load modules. A route contains a pointer to a view and the XAP file the view lives in, and is exported by whatever module may need to load that view. When the view is requested, the view manager first checks to see if a route exists. If it does, it asks the deployment service to dynamically load the XAP and defers raising the navigation event until the dependent XAP file is loaded. This makes it easy to abstract navigation from the modules, because any area in the application simply requests the view, and the framework takes care of either navigating to the view, or loading the XAP file and then navigating.
View Model Routing
A common concern with using MVVM is how to bind the view model to the view. In the frameworks I use, there is not a one-to-one mapping because some view models might service multiple views. The view model never really knows about the view(s) it is attached to, but the views depend on the view model (consider it the "binding contract" for the view). Views are exported by type or tag with a target region, and view models are also exported with a view model tag.
Next, I have a simple route object that contains two tags: a view tag, and a view model tag. Whether the view exports this tag, or a separate "routing" class does, these exports are then all managed by a centralized router. The router is able to pull views, and because everything is lazy-loaded, determine if the view needs the view model bound. Conversely, the view model can also be created for the first time or referenced.
The result is that the view and view model binding is completely decoupled - it is described elsewhere in the application. What it also means is that the routing system knows when a view is being shown for the first time, so it can call methods on the view model both the first time it is created, and each time a view is active. This allows my view models to respond to housekeeping that may be required before binding to the view again, without having a dependency on the visual tree.
[ExportAsViewModel(typeof (DialogViewModel))] public class DialogViewModel : BaseViewModel ... [ExportAsView(typeof(MyDialog),Regions.DialogRegion)] public partial class MyDialog { public MyDialog() { InitializeComponent(); } [Export] public ViewModelRoute DialogBinding { get { return ViewModelRoute.Create<DialogViewModel, MyDialog>(); } } }
Take a look at View Model binding with MEF and Yet another MVVM Locator pattern for more on this.
Decoupling Views
There are many cases when you may need to map to a type of view but don't know what the view will be before hand. For example, consider a navigation framework that depends on view types (see the next section). When the view is in a dynamic XAP file that isn't loaded, there is no way to directly reference the time. MEF was very useful in "late-binding" types, that is to say, I could import a property for a type using a tag ("MainView" or "DashboardView") and then export the tag when the view was defined somewhere else. This allowed the idea of interacting with a view to be completely decoupled from the actual view type.
Navigation
MEF provided a very flexible navigation framework. I wanted something composable, such that a "navigation event" wasn't limited to a "page" in the application, but rather a view - and views often are nested inside of other views.
The navigation works in conjunction with the view model router to synchronize changes with the view model. In the framework I developed, the MEF-enabled event aggregator simply raises a view navigation event which encapsulates the type of a view and a flag for activation or deactivation. The router, which has imported all views and view models, finds the view, talks to the region manager to swap it out, and if it is active will also call the appropriate method on the view model.
Navigation became incredibly simple because the concept of a dialog or page or even wizard step is reduced to a simple navigation event. The idea of "pop-up" is an implementation detail abstracted from the event itself, and a view model simply raises the request for a view for navigation to happen and doesn't have to worry about view models, regions, or any other nuances of the navigation itself.
Authorization
Authentication and authorization are required for most line of business applications. Typically the user has properties such as roles and credentials that are checked for access and authorization to various areas of the appplication. MEF makes it easy to carry these credentials because the user construct can be exported as either a concrete object or contract, and then imported and queried by any area of the application that requires access to the credentials and user properties.
Commands
Certain commands can be tightly coupled to the user interface, and others might exist in modules not yet referenced. For example, a "home" command or an "expand" command requires a navigation event that is aware of the target for the navigation.
Late-binding Commands
MEF helped handle this scenario by allowing commands to be exported and imported. In some cases, the view model would import the command based on a pre-defined tag (such as a "home" tag) even if the "home" view is in a different module. The module, when loaded, would export the appropriate command and it then becomes avaiable for binding from the view model even though the command was "late-bound" meaning it did not become available until the child module was loaded.
[Import(Contracts.COMMAND_GOHOME, AllowDefault = true)] public DelegateCommand<object> HomeCommand { get; set; } ... [Export(Contracts.COMMAND_GOHOME)] public DelegateCommand<object> HomeCommandExport { get { return new DelegateCommand<object>( obj => EventAggregator.Publish(typeof (HomeView).AsViewNavigationArgs())); } }
Global Commands
Some commands are global by nature and instead of creating dozens of local copies per view, make sense to be accessed as a single, global construct. Global commands be exported and then imported wherever they are needed so a global copy is used, rather than creating multiple local instances. Again, as with the "home" example, view models can expose a "home" command before having knowledge of the assembly/module that contains the command, and MEF simply imports the command when it becomes available.
Read Sharing Commands with MEF for more.
Chain of Responsibility
The chain of responsibility pattern is a powerful way to handle common messages that are processed differently based on the message signature. I used MEF to provide a "handler" mechanism that defined a standard contract that would consume certain message types. Each handler would export the contract along with metadata described what messages the handler could process. A central "router" would then import the handlers, listen to the message, and pass the message to the appropriate handlers until one indicated it had successfully processed the message. This, coupled with global commands, provides a very powerful level of extensibility to the application.
For example, consider a "print" command that is globally exposed throughout the applicaton but bound to the data element being printed. Multiple handlers are registered to the print interface that know how to process different types of data objects. When the handler that manages the target object is called, it formats the data in a printer-friendly format and sends the output to the printer. Handling a new type of data is as simple as building a new handler and then exporting the print contract.
Factories with Dependency Resolution
Many classes have multiple layers of dependencies. Consider a composable view where a list of items is presented, and each item is exposed via a bound view model that has dependencies to the event aggregator, service bus, and other contracts that are imported using MEF. MEF makes this scenario simple and straightforward to handle via the ExportFactory
keyword. When spinning through a list of data objects, a view model can be created on the fly using the export factory that features full dependency resolution, from handlers to global commands. The data is attached and then the view model bound to the child view with all dependencies resolved via MEF.
Design-Time Friendly Views
Design-time friendly views are extremely important when working with the design team in parallel. MEF made it very easy to build a design-friendly framework because the runtime view models would be bound via the MEF constructs while the design-time view models are bound directly in views using the d:Design
and d:DesignInstance
attributes. By isolating MEF-specific initialization code using the IPartImportsSatisfied
contract, the views and view models remained designer-friendly and would not execute any code reserved for the run time version of the application.
See Design-time Friendly View Models with MEF for more.
Validation Engine
Validation engines typically expose different validation rules that result in rule violations. MEF made it simple and easy to build an engine that was both flexible and extensible. Each validation rule is exported using the contract for the rule. The engine would import all rules, and then provide access via a fluent interface. Read fluent validation with MEF and PRISM to learn more.
Unit Testing
MEF was critical in the success of unit testing because all dependencies were exposed by contracts that MEF imported. For testing, the MEF initializer simply isn't called, allowing the test system to initialize the dependencies using test mocks and stubs. Take a look at using Moq with Silverlight for advanced unit tests.
Conclusion
Obviously, many of these examples could be easily handled using frameworks other than the Managed Extensibility Framework. The framework has the advantage of being included in the language run-time: it's not a third-party, out-of-band release, but a fundamental part of both the .NET 4.0 and the Silverlight 4.0 CLR. Based on my experience building very large line of business Silverlight applications, MEF not only made it easy to break those applications into modules and parts, it also accelerated development time by providing so many features "out of the box," especially the ability to tag exports with meta data and import multiple items for the same contract. Hopefully this will not only provide you with some ideas about how to use MEF in your own applications, but also demonstrates how MEF is being used effectively in production line of business applications.