Thursday, April 14, 2011

Creating a Markup Extension for MEF with Silverlight 5

One exciting new feature of Silverlight 5 is the ability to create custom markup extensions. Markup extensions are a part of XAML. Whenever you see a notation that starts and ends with a brace, you are viewing a markup extension. Markup extensions exist to pass information to properties in the XAML that the XAML parser can use when generating the object graph.

A commonly used example of markup extensions is the data-binding syntax. In the following XAML snippet, the data-binding uses a markup extension to specify that a binding is being defined.

<TextBlock Text="{Binding Path=Name,Mode=TwoWay}"/>

The brace begins the extension, followed by the name of the class that will handle the extension, followed by any parameters you wish to pass. You can do that same thing in code behind by creating a Binding object directly and setting it's properties.

So how are markup extensions useful? They provide a hook to perform functionality that would be impossible or overly complicated using traditional approaches such as attached properties or behaviors. One popular use of markup extensions is to provide access to static classes that otherwise are not available in XAML. A common reason to do that would be to access a service locator - for example, to generate your view model.

To illustrate how to create your own, I decided to use the Managed Extensibility Framework as an example. It is often challenging to insert classes that are managed by MEF into XAML because they require a step known as "composition" to wire dependencies. With a custom markup extension, however, that limitation goes away because the extension can perform the composition call!

Because MEF does not work the same way in the designer as it does in the runtime, I also wanted to provide a fallback mechanism for design-time views. The markup extension will take a component type (a fully qualified type name) and an optional design type. In the designer, if the design type is specified, it creates an instance and returns it. If not, it creates an instance of the component and returns it. During runtime, when not in the designer tool, the extension takes the additional step of calling MEF to compose the dependencies on the target component.

While the following example view model could be created in XAML, the properties would never get set because they are imported from somewhere else:

public class MainViewModel
{        
    [Import("Name")]
    public string Name { get; set; }

    [Import("Email")]
    public string Email { get; set; }
        
}

The values are exported in another class (just for the sake of this example):

public class Exports
{
    [Export("Name")]
    public string Name
    {
        get { return "Jeremy Likness"; }
    }

    [Export("Email")]
    public string Email
    {
        get { return "[email protected]"; }
    }
}

The design-time view model returns some sample data:

public class DesignViewModel
    {
        public string Name { get { return "Joe Designer"; } }
        public string Email { get { return "[email protected]"; } }
    }

Now for the custom markup extension. It should inherit from MarkupExtension and the name should end with Extension (similar to the way dependency property definitions end with the word Property). There is just one method to implement and you can provide your own properties. Here is the MEF composition extension:

public class MefComposerExtension : MarkupExtension 
{
    public string Component { get; set; }
    public string Designer { get; set; }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {            
        if (Component == null)
        {
            throw new Exception("Component required.");
        }
            
        if (DesignerProperties.IsInDesignTool)
        {
            return Activator.CreateInstance(Type.GetType(string.IsNullOrEmpty(Designer) ? Component : Designer));
        }

        var instance = Activator.CreateInstance(Type.GetType(Component));
        CompositionInitializer.SatisfyImports(instance);
        return instance;
    }
}

Notice that the first check is whether it is in the design tool. If so, it will return an instance of either the design parameter or the component. If not in the design tool, it will create an instance of the component, and then use MEF to satisfy the imports. To use the custom markup extension, you simply add a reference to the namespace and then reference the name of the extension inside braces. Here is the XAML for the namespace:

<UserControl ... xmlns:local="clr-namespace:TypeConverter" .../>
And here is the XAML for the main page that uses the markup extension to set the data context:
<Grid x:Name="LayoutRoot" Background="White" 
        DataContext="{local:MefComposer Component='TypeConverter.MainViewModel',Designer='TypeConverter.DesignViewModel'}">
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="Auto"/>            
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto"/>
        <ColumnDefinition Width="Auto"/>
    </Grid.ColumnDefinitions>
    <TextBlock Grid.Row="0" Grid.Column="0" Text="Name: "/>
    <TextBlock Grid.Row="1" Grid.Column="0" Text="Email: "/>
    <TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding Name}"/>
    <TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding Email}"/>        
</Grid>

As you can see, it is fairly straightforward - simply the namespace and the name of the extension, wherever the returned value should be inserted. This works fine in the preview version of Blend. You can see the grid's datacontext property correctly resolve the design view model:

And the design-time data in on the design surface:

Of course, the most important step is to run this and see it in action. Sure enough, at runtime it will not only create the view model instance, but compose the parts through MEF and find the imported properties:

Obviously this feature is extremely powerful. While it has existed for some time in WPF, it is now available for Silverlight developers and promises to create many new possibilities when integrated in code moving forward.

Grab the source code for this project here.

Jeremy Likness