Wednesday, December 2, 2009

Silverlight MEF: Custom ExportProvider for Attached XAML Exports

I've been enjoying learning more about the Managed Extensibility Framework (MEF) and exploring various ways to integrate it with applications. After toying with MEF on Silverlight for awhile, I began to wonder about using it to export/import UI pieces or even define data elements in XAML. After a tweet to @gblock confirmed there was not native support, I set out to see what I could do. (If you're not familiar with MEF, take a look at my 10 minute walkthrough.)

The result is by no means ideal and is certainly a contrived "proof of concept" solution, but I believe it will help expose some of the custom extension points available with MEF and just how flexible it truly can be.

Download the Source

My goal was to minimize code behind for a common scenario of having pages with groups of widgets. Widgets will most likely be defined as user controls, but we've already seen plenty of examples there. I wanted to be able to define them as resources in pure XAML, no code behind, then import them into something like a StackPanel as child elements.

The Attachable View Model

The first thing I created was an attachable view model. It would get a list of FrameworkElement for binding to a control. To make it even more interesting, I wired in an attached property. This way I can simply attach the view model to a control and it will automatically pull in the parts and populate the children.

The initial shell looked like this:


public class ViewModel
{
    private static List<FrameworkElement> _uiParts = new List<FrameworkElement>();
    
    [ImportMany("UIPart", AllowRecomposition = true)]
    public List<FrameworkElement> UIParts
    {
        get { return ViewModel._uiParts; }
        set { ViewModel._uiParts = value; }
    }

    public ViewModel()
    {
        var catalog = new AssemblyCatalog(Assembly.GetExecutingAssembly());
        var container = new CompositionContainer(catalog);
        container.ComposeParts(this);
    }

    static ViewModel()
    {
        new ViewModel();
    }

    public static readonly DependencyProperty ViewBinderProperty = DependencyProperty.RegisterAttached(
        "ViewBinder", typeof(bool), typeof(ViewModel), new PropertyMetadata(new PropertyChangedCallback(OnViewBound)));

    public static void OnViewBound(object sender, DependencyPropertyChangedEventArgs args)
    {
        // make sure we want this ...
        if (args.NewValue != null && args.NewValue.GetType().Equals(typeof(bool)) && (bool)args.NewValue)
        {
            Panel panel = sender as Panel;
            if (panel != null)
            {
                // iterate the parts
                foreach (FrameworkElement element in _uiParts)
                {
                    // clone it
                    FrameworkElement newElement = Clone(element);
                    
                    // give it a unique name
                    newElement.SetValue(FrameworkElement.NameProperty, Guid.NewGuid().ToString());
                    
                    // inject it
                    panel.Children.Add(Clone(newElement));
                }
            }
        }
    }

    public static bool GetViewBinder(DependencyObject obj)
    {
        return (bool)obj.GetValue(ViewBinderProperty);
    }

    public static void SetViewBinder(DependencyObject obj, bool value)
    {
        obj.SetValue(ViewBinderProperty, value);
    }
}

So far it feels pretty much like a standard class hosting a dependency property. You'll notice it's a bit strange, however, as the class itself is not static. This is because MEF is expecting an instance to compose the parts in. To facilitate that, I have an instance constructor that follows the general MEF pattern ... throw the catalog into the container and compose the parts. The "instance" getter and setter is really a facade that populates the static list of parts, in this case I'm just asking for any type of FrameworkElement. Obviously, those will have to come from somewhere else.

Because my intention is to use a resource dictionary, I must be able to share the elements. Silverlight won't let you take an element that is already the child of another element and then move it somewhere else. I have to assume I may reuse some of these elements as well (for example, this could be extended with a filter to choose the types of elements). So, instead of adding the elements directly to the parent, I am cloning them.

Thanks here goes to Justin Angel for his very functional clone method that I found here. I left it out of the code snippet above to keep it simple, but it is included in the source.

Now we need to tackle the task of exporting the framework elements. Ideally, I just want to put them into a resource dictionary, tag them somehow, and have the magic work. Fortunately, MEF gives us a nice ExportProvider we can derive from to do just that. There's an excellent article I started with by the "Code Junkie" you can access here.

It may have made more sense to split out the dependency properties and the provider to separate classes, but I figured, why not? So here is a custom export provider that also hosts some attached properties to facilitate exporting XAML:


public class XAMLProvider : ExportProvider
{
    private static readonly Dictionary<ExportDefinition, List<Export>> _exports = new Dictionary<ExportDefinition, List<Export>>();

    private static readonly XAMLProvider _provider = new XAMLProvider();

    public static DependencyProperty MEFExportProperty = DependencyProperty.RegisterAttached("MEFExport", typeof(string), typeof(XAMLProvider),
        new PropertyMetadata(new PropertyChangedCallback(OnMEFExport)));

    public static void OnMEFExport(object sender, DependencyPropertyChangedEventArgs args)
    {
        if (args.NewValue != null && !string.IsNullOrEmpty(args.NewValue.ToString()))
        {
            string contract = args.NewValue.ToString();
            _provider.AddExport(contract, sender);
        }
    }

    public static string GetMEFExport(DependencyObject obj)
    {
        return (obj).GetValue(MEFExportProperty).ToString();
    }

    public static void SetMEFExport(DependencyObject obj, string value)
    {
        obj.SetValue(MEFExportProperty, value); 
    }

    private static readonly object _sync = new object();

    public static XAMLProvider GetXAMLExportProvider()
    {
        return _provider;
    }

    public void AddExport(string contractName, object export)
    {
        lock (_sync)
        {
            var found =
                from e in _exports
                where string.Compare(e.Key.ContractName, contractName, StringComparison.OrdinalIgnoreCase) == 0
                select e;

            if (found.Count() == 0)
            {
                ExportDefinition definition =
                    new ExportDefinition(contractName, new Dictionary());

                _exports.Add(definition, new List<Export>());
            }

            Export wrapper =
                new Export(contractName, () => export);

            found.First().Value.Add(wrapper);
        }
    }

    protected override IEnumerable<Export> GetExportsCore(ImportDefinition definition, AtomicComposition atomicComposition)
    {
        var contractDefinition = definition as ContractBasedImportDefinition;
        IEnumerable<Export> retVal = Enumerable.Empty<Export>();

        if (contractDefinition != null)
        {
            string contractName =
                contractDefinition.ContractName;

            if (!string.IsNullOrEmpty(contractName))
            {
                var exports =
                    from e in _exports
                    where string.Compare(e.Key.ContractName, contractName, StringComparison.OrdinalIgnoreCase) == 0
                    select e.Value;

                if (exports.Count() > 0)
                {
                    retVal = exports.First();
                }
            }
        }

        return retVal; 
    }
}

The only override provided is to actually get the exports for a provided input definition. Everything else is left to us.

The first thing I did was create a dictionary. The key is the contract name (in MEF, the contract name is just a string ... if you use a type or an interface, then the fully qualified name becomes the string for the contract). The value is a list of all exports attached to that contract.

The dependency property allows me to attach "MEFExport" to any XAML object and give it a contract name. When this property is attached, it will look for the name in the dictionary and then either create a new entry or add the element to the existing list. It's as simple as that!

When the GetExportsCore is called, we are given an import definition. For a more complex example, we could look at attributes and other extensions. In this case, I'm simply extracting the contract name, finding the key in the dictionary and then sending off any exports that were loaded with the key.

Now I can add a resource dictionary and tag my elements for export. Mine looks like this:


<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:MEFExtension">
    <TextBlock FontSize="32" Text="This is a text block." x:Key="ui1" local:XAMLProvider.MEFExport="UIPart"/>
    <Rectangle Width="100" Height="100" Fill="Red" x:Key="ui2" local:XAMLProvider.MEFExport="UIPart"/>
    <TextBlock FontSize="16" Text="This won't get imported." x:Key="ui3"/>
    <Ellipse HorizontalAlignment="Left" Fill="Blue" Stroke="Black" Width="100" Height="50" x:Key="ui4" local:XAMLProvider.MEFExport="UIPart"/>
</ResourceDictionary> 

I threw in a dummy text block without tagging it to show that it won't get exported because the property is not attached. So now when that resource dictionary is pulled in, we can parse the XAML and export the elements. We need to let MEF know about our custom provider. Going back to my view model, I'll change the container creation to this:


var container = new CompositionContainer(catalog, XAMLProvider.GetXAMLExportProvider());
            

Notice I now passed in our custom export provider.

Now all of the elements are in place. The custom export provider knows how to scan XAML and load up elements for export, we've added a resource dictionary with some elements to export, and the view model is asking for imports and has its own attached property so that we can push the elements to any type of panel.

Last step: I go into my main page, pull in the resource dictionary and bind the view model:


<UserControl x:Class="MEFExtension.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
    mc:Ignorable="d" d:DesignWidth="640" d:DesignHeight="480"
    xmlns:local="clr-namespace:MEFExtension">
    <UserControl.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="Resources.xaml"/>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </UserControl.Resources>
    <StackPanel Orientation="Vertical" Width="Auto" local:ViewModel.ViewBinder="true"/>
</UserControl>

So the resource dictionary will get pulled in. As the keys are processed, it will fire the attached properties and load them into exports. I gave them the "UIPart" contract. When the view model is attached, it will new an instance that fires the composition container for MEF. This will use the custom provider. Because we have the list of elements marked "UIPart", the exports will match and get loaded. The attach event will clone these objects and add them as children to the stack panel.

The result?

Silverlight MEF ExportProvider

There you have it, an example of extending the, ah, Managed Extensibility Framework, to support exporting using attached properties in XAML (I did this in Silverlight 3, so I had to add the reference to the System.ComponentModel.Composition from the download ... it is included in Silverlight 4).

Download the Source

Jeremy Likness