Friday, March 12, 2010

Custom Export Providers with Custom Metadata for Region Management

Over the past few weeks I've been exploring the concept of region management using the Managed Extensibility Framework, and for a good reason. I'm working on a project that has several different regions and controls that must be managed effectively and across the boundaries of dynamic XAP files in Silverlight 3.

Sound like a mouthful? In previous posts I demonstrated some methods for handling region management using MEF. This post is an advanced post and I'm assuming you have the fundamentals of MEF down. If not, I would come back to this post at a later time. You'll also want to be familiar with my previous experiments detailed in this article and another version in this video tutorial.

So the ultimate goal for me is to tag a view with a type, tag a region with a type, then route views to regions and vice versa. This way I can easily add controls (just tag 'em) and regions (again, tag 'em) and have a routing table that handles swapping them in and out for me.

One of the issues in the past has been having to explicitly export the region. In other words, I might have a ContentControl tagged for a particular region. I have to give it an x:Name in the XAML, like this:

...
<ContentControl x:Name="MyName"/>
...

And then export it with my custom attributes like this:

...
[TargetRegion(Region=ViewRegion.MainRegion)]
public FrameworkElement MainRegionExport 
{
   get { return MyName; }
}
...

This assumes I created a custom export attribute with a Region parameter and a base type of FrameworkElement. Where I want to go is here: eliminate the code behind, and tag my regions like this:

...
<ContentControl mef:TargetRegion.Region="MainRegion"/>
...

In the XAML, with no code behind. Can we get there? Of course, this is MEF!

The first step for me is to create to a custom export provider. I spoke briefly about this in an old post: Custom Export Provider for Attached Exports, but that was for static controls. Here, I want to export regions with meta data. The export provider looks like this:

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

    private static readonly RegionExportProvider _provider = new RegionExportProvider();

    public static DependencyProperty RegionProperty = 
        DependencyProperty.RegisterAttached(
        "Region", 
        typeof(ViewRegion), 
        typeof(RegionExportProvider),
        null);       

    public static ViewRegion GetRegion(DependencyObject obj)
    {
        return (ViewRegion) obj.GetValue(RegionProperty);
    }

    public static void SetRegion(DependencyObject obj, ViewRegion value)
    {
        obj.SetValue(RegionProperty, value);
        GetRegionExportProvider().AddExport(value, obj);
    }

    private static readonly object _sync = new object();

    public static RegionExportProvider GetRegionExportProvider()
    {
        return _provider;
    }

    public void AddExport(ViewRegion region, object export)
    {
        lock (_sync)
        {
            string contractName = typeof (FrameworkElement).FullName;

            var metadata = new Dictionary<string, object>
                               {
                                   {"ExportTypeIdentity", typeof (FrameworkElement).FullName},
                               }; 

            var found =
                from e in _exports
                where string.Compare(e.Key.ContractName, contractName, StringComparison.OrdinalIgnoreCase) == 0
                select e;
            
            if (found.Count() == 0)
            {                    
                var definition =
                    new ExportDefinition(contractName, metadata);

                Debug.WriteLine("Region Export Provider: Add custom export: " + region);

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

            metadata.Add("Region", region); 

            var wrapper =
                new Export(contractName, 
                    metadata, 
                    () => 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;
            
            Debug.WriteLine("GetExportsCore: Request for contract: " + contractName);

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

                if (exports.Count() > 0)
                {
                    Debug.WriteLine("Export satisfied.");
                    retVal = exports.First();
                }
                else
                {
                    Debug.WriteLine("Export was not satisfied.");
                }
            }
        }

        return retVal;
    }
}

Let's step through it. First, I need to keep a catalog of my exports. These are unique type definitions and contract names that we are going to satisfied with our custom export provider. I use the singleton pattern because I'm going to double this class as an attached behavior. Probably violating the single responsibility principle here, and could break it out, but let's run with it for now. Sometimes it makes sense to keep the code together for readability and maintainability.

The attached property just takes the type of the region. Because we type it to the enumeration, intellisense will work in the XAML when adding the behavior, which is nice, because it makes it less of a "magic string" for us to use.

The key to note is the setter. When we set the value, we add the export. The export itself is going to be the contract name and the type identity. This is why we add the first bit of meta data, the ExportTypeIdentity, and then see if we've already made an entry or not. If not, we add a new entry with an empty list of the actual exports.

Adding the exports is easy. Metadata on an export is really just a dictionary of labels and values. We know this particular export has one label ("Region") and that it is the enum of the region type. Therefore, once we find or create our export definition, we go ahead and add the export with the region itself (a content control, items control, or other container) and meta data for that region (in this case, the "Region" tag and the enumeration value).

Now we just need to be prepared to supply the exports when our custom provider is queried. This is the GetExportsCore. Fortunately, we get it easy. Instead of having to keep track of all of the rules, the import definition comes with a IsConstraintSatisfiedBy method that calls the Constraint provided to filter the export values. We simply find the definition that matches the contract and the constraint, then provide all of the exports we have. These will then have their meta data parsed and if a capabilities interface is provided, it will be populated with the values so that the Lazy<Type,ITypeCapabilities> target can be populated. Notice I added some debug lines to help troubleshoot.

Now I can tag my export the way I wanted to:

...
<ContentControl mef:RegionExportProvider.Region="MainRegion" .../>
...

There is only one more step I need to take in order for this all to work. Remember back when I showed you how to override the container so we could continue to compose new DeploymentCatalogs into an AggregateCatalog as they became available? When we create that special container, we need to add our secret sauce. We do it like this:

...
var container = new CompositionContainer(catalogService.GetCatalog(), RegionExportProvider.GetRegionExportProvider());
...

Now we've told the container to query our own custom export provider when trying to satisfy parts.

If you recall from my prior posts, I used a routing class to map view types to regions, and loaded the regions like this:

[ImportMany(AllowRecomposition = true)]
public Lazy<FrameworkElement, ITargetRegionCapabilities>[] RegionImports { get; set; }

Our new provider kindly exports the tagged regions, which are then imported with the routing class and used to swap views in and out of the display.

Jeremy Likness