Click here to Skip to main content
15,886,362 members
Articles / Web Development / HTML

Silverlight MEF: Custom ExportProvider for Attached XAML Exports

Rate me:
Please Sign up or sign in to vote.
5.00/5 (7 votes)
2 Dec 2009CPOL5 min read 12.3K   42   11  
Use the Managed Extensibility Framework to export dictionary resources using custom ExportProvider in Silverlight

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 a while, 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.

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:

C#
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:

C#
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<string,>());

                _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; 
    }
}</string,>

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:

XML
<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:

C#
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:

XML
<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?

45916/mefextension.PNG

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).

You can download the source code from here.

Jeremy Likness

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Program Manager Microsoft
United States United States
Note: articles posted here are independently written and do not represent endorsements nor reflect the views of my employer.

I am a Program Manager for .NET Data at Microsoft. I have been building enterprise software with a focus on line of business web applications for more than two decades. I'm the author of several (now historical) technical books including Designing Silverlight Business Applications and Programming the Windows Runtime by Example. I use the Silverlight book everyday! It props up my monitor to the correct ergonomic height. I have delivered hundreds of technical presentations in dozens of countries around the world and love mentoring other developers. I am co-host of the Microsoft Channel 9 "On .NET" show. In my free time, I maintain a 95% plant-based diet, exercise regularly, hike in the Cascades and thrash Beat Saber levels.

I was diagnosed with young onset Parkinson's Disease in February of 2020. I maintain a blog about my personal journey with the disease at https://strengthwithparkinsons.com/.


Comments and Discussions

 
-- There are no messages in this forum --