One of my early experiments with Bing Maps Silverlight Control eventually turned into a Halloween Live Gallery. This photo viewer application is based on the CircularPanel3D control from Expression Blend’s Wall3D sample that shipped with the product. It pulls geotagged Halloween photos from Flickr service and displays them in a 3D photo wall that can be rotated, zoomed in, etc. A detailed view of the photo includes a zoomable Bing Maps control, pinpointing the location of where that photo was shot.
I really like showing off this application in my Silverlight/Expression talks I’m giving, and with Halloween long gone by now, I needed to change its theme – for… let’s say… Christmas? Even better – I thought it would be great if I had a few themes always ready and packed to go so I could plug in any of them at any time, without getting too much involved with changing the existing code. And it should be easy to later create new themes and add them to the library.
The answer was clear – I needed MEF.
This article will guide you through the process of rewriting the Gallery application to be easily themable through external theme plugins. The main goal was to create a new Christmas theme that I could show off during the last weeks of the year, but also keep the Halloween theme as it was. The article is split in two parts - the first part will cover the basics, leaving some more advanced stuff for the second part.
What is MEF?
MEF (Managed Extensibility Framework) lets you build extensible applications by breaking them into smaller, discoverable parts that can be properly (re)composed on demand. It’s built into .NET Framework 4 and Silverlight 4 (SDK), but also available for earlier framework versions through a separate download from Codeplex.
There are just a few concepts one needs to be familiar with to get things going with MEF: you work with parts. On composition, exported parts are imported to expected places in application. When architecting a MEF application, these are the three core terms you need to be familiar with. Let’s review them:
- exports: parts, marked as discoverable
- imports: “placeholder” parts to be replaced with exports when composition happens
- composition: discovering exports and “satisfying imports” - replacing import “placeholders” with exports
The best thing about MEF is that for many cases, this might be all you would need to know. It’s really that simple! On the other hand, MEF allows you to dig down much deeper into the framework and solve much more complex needs your solution might require.
Where to start?
The first step after deciding which framework + MEF combo to use, is identifying parts of your existing application you want to have “pluggable”. These are best found while running your application. If you take a look at Halloween Gallery, for example, the first thing you’ll probably notice are the colors used. Black and orange may work great for Halloween, but Christmas colors have always been red and white. Then there are the photos of course – they are pulled from Flickr and are based on search terms – tags, describing those photos. If the search tag for Halloween was ‘halloween’, then it should be ‘christmas’ for Christmas gallery, right? The last thing to notice is the pumpkin-shaped pushpin icon on the Bing Maps control, marking the photo location; that one should be replaced with an icon of Santa’s hat, for example.
Just for a start, we identified the following as a definite must for making the gallery configurable through themes:
- colors
- search terms
- location icon
Now let’s take a break and actually do something about it.
Dissecting the project
Although not a requirement, it’s best to have themes separated from the main project - for better maintainability, if not other reasons. But regardless of their container, they should be discoverable through some kind of a contract that the main project would understand. MEF lets you draw the line of separation on many levels: we could, for example, put themes:
- in the main (entry) assembly,
- in a separate assembly/ies, but in the same XAP package as the main assembly,
- in an assembly, contained in a separate XAP package.
And those are just your basic options for part discovery, available from the framework (and the Silverlight Toolkit, to be fair – but more on that in the second part). For the sake of this first part of the article I decided to put all initial themes in a project called Gallery.Themes and include it in the main XAP package. Having done that, I could create additional themes later and put them in a number of separate projects (call them Gallery.Themes.Winter, etc.) – that would make no difference to the main project, which would still act as the driver, discovering the themes across all the assemblies in the package and picking one of them to load. In the second part of the article I’m going to extend the gallery to discover themes included in separate XAP packages.
But first, let’s bring some MEF into the gallery.
Each project should reference the System.ComponentModel.Composition.dll. For Silverlight 4, that’s located in \Program Files\Microsoft SDKs\Silverlight\v4.0\Libraries\Client folder. That will give you a full access to MEF API.
Having previously identified what an initial theme will consist of, we now need to define it in code. The following interface reflects the variables identified as a theme and will serve as a discoverable contract:
public interface ITheme
{
string SearchString { get; }
Color Foreground { get; }
Color Background { get; }
BitmapImage Pushpin { get; }
}
I put the interface in a separate - ‘contract’ – project, called it PhotoGallery.Extensibility and referenced it from both main and themes project. This makes maintaining projects easier while removing any need for other dependencies between the projects.
The magic of MEF
With interface in place, the project is ready for a touch of MEF magic. We need to pull all Halloween-dependent parts of code out of the main project and move them into a Themes project.
I described the three core principles of MEF before, let’s review them again, in practice:
Export
Here’s a complete HalloweenTheme class, with the Export attribute sprinkled on top. This attribute makes the class discoverable through the ITheme conract, making it a MEF export.
[Export(typeof(ITheme))]
public class HalloweenTheme : ITheme
{
public string SearchString
{
get { return "halloween"; }
}
public Color Foreground
{
get { return Color.FromArgb(255, 255, 100, 0); }
}
public Color Background
{
get { return Colors.Black; }
}
public BitmapImage Pushpin
{
get { return new BitmapImage(
new Uri("/PhotoGallery.Themes;component/Resources/Pumpkin.png",
UriKind.Relative)); }
}
}
Import
The same contract (ITheme) is used to import themes into the gallery. Because we’re expecting more than just one theme to be available for import, the property is marked with ImportMany attribute (in contrast to the Import attribute, which would require exactly one ITheme export to be found).
[ImportMany(typeof(ITheme))]
public ITheme[] Themes { get; set; }
Compose
Lastly, we need to trigger the composition by calling a single method on the PartInitializer class.
PartInitializer.SatisfyImports(this);
Where ‘this’ is the main page, exposing the importable Themes property. After calling this method, the Themes property collection should contain references to all discovered themes; now we only need to pick which one to display.
Let there be hints
Right… so when more than one theme is discovered, which one should be used? The application could just pick the first one in the collection and let the user change it later. Or…
MEF lets you attach additional metadata to exports. This metadata is not part of an export, but may provide some additional information to the imports resolver, like hints about how a part is to be imported.
Gallery themes provide the metadata about a date range, when the theme is most likely to be used. For example – the Christmas theme will have the date range of 12/1 – 1/31 (meaning whole December and January), while Halloween theme would have 10/1 – 11/30 (October and November).
The ITheme metadata is defined as:
public interface IThemeMetadata
{
int StartMonth { get; }
int StartDay { get; }
int EndMonth { get; }
int EndDay { get; }
}
The best way to expose metadata with an export is to wrap it in a custom attribute:
[MetadataAttribute]
[AttributeUsage(AttributeTargets.Class, AllowMultiple=false)]
public class ExportPeriodAttribute : ExportAttribute, IThemeMetadata
{
public ExportPeriodAttribute()
:base(typeof(ITheme))
{
}
public int StartMonth { get; set; }
public int StartDay { get; set; }
public int EndMonth { get; set; }
public int EndDay { get; set; }
}
ExportPeriodAttribute derives from the ExportAttribute thus can be used as a replacement for existing Export attribute, letting us provide the date range metadata at the same time. Here’s a complete ChristmasTheme class:
[ExportPeriod(StartMonth = 12, StartDay = 1, EndMonth = 1, EndDay = 31)]
public class ChristmasTheme : ITheme
{
public string SearchString
{
get { return "christmas"; }
}
public Color Foreground
{
get { return Colors.White; }
}
public Color Background
{
get { return Colors.Red; }
}
public BitmapImage Pushpin
{
get { return new BitmapImage( new Uri("/PhotoGallery.Themes;component/Resources/Hat.png", UriKind.Relative)); }
}
}
The part where themes get imported needs to be changed as well:
[ImportMany(typeof(ITheme))]
public Lazy<ITheme, IThemeMetadata>[] Themes { get; set; }
MEF’s Lazy<T,M> type derives from Lazy<T>, which is a new type in .NET Framework 4 and Silverlight 4, used for delayed object instantiation. This is great because we can access theme’s metadata without creating an actual theme instance. The following LINQ query with a helping function finds the right theme to load, based on provided metadata. If no suitable date range were found, the first theme still serves as the default.
public void OnImport()
{
// Found nothing to import?
if (Themes.Count() == 0)
{
return;
}
DateTime date = DateTime.Today;
var dateItem = from item in Themes
where GetIsDateInRange(date, item.Metadata)
select item;
var finalItem = dateItem.FirstOrDefault() ?? Themes[0];
// Accessing Lazy<T>'s Value creates an instance of T
ITheme theme = finalItem.Value;
// Read theme's properties and set them
}
private bool GetIsDateInRange(DateTime date, IThemeMetadata metadata)
{
int year = date.Year;
DateTime start = new DateTime(year, metadata.StartMonth, metadata.StartDay);
DateTime end = new DateTime(year, metadata.EndMonth, metadata.EndDay);
if (metadata.StartMonth > metadata.EndMonth)
{
if (DateTime.Today.Month >= metadata.StartMonth)
{
end = end.AddYears(1);
}
else
{
start = start.AddYears(-1);
}
}
return start <= date && date <= end;
}
To be continued…
This first part of this article covered the very basics of MEF. In the next part, we’ll take the gallery a bit further – we’re going to look at how MEF can help us developing with MVVM pattern, expand the themes, import more complex objects (like behaviors), load themes from separate XAPs and take a deeper look at some of MEF’s concepts.
See you in part 2 of this article…
The gallery can be viewed here [Requires Silverlight 4]