Using MEF Contracts to Coordinate Communication Between Extensions

Introduction

DotSpatial is an open-source project that contains controls which can be used to manipulate and display geographic information. This article explains how to allow extensions to communicate with one another. You will want to follow the introductory article, as we add additional functionality to the extension created in that article.

In many cases one class library will reference another in order to access functionality provided by the latter. Sometimes, however, these relationships are established at runtime to provide the user with more flexibility to mix and match components. In other cases, an extension is written so that it is itself extensible and will provide some service to other extensions which follow a proscribed convention, or it will “light-up” when other extensions expose the appropriate data or methods.

There are several approaches in DotSpatial for communicating between extensions. Named contracts require more documentation or inside knowledge and provide fewer guarantees about compatibility. Interface contracts are self-documenting but introduce more complexity, as the interface needs to be referenced. This article describes how to use Managed Extensibility Framework (MEF) named contracts. This article will not give you a complete understanding of MEF, but provides a simple example.

Getting Started with Exports

Open the project you created in the previous exercise. To keep things simple, we will expose the message that is being displayed in the status bar when the user clicks the Show Layer Count button. Widen the scope of the message variable, making it a field.

private string _Message;
public void ButtonClick(object sender, EventArgs e)
{
    _Message = String.Format("Number of Layers: {0}", App.Map.Layers.Count);
    App.ProgressHandler.Progress(null, 0, _Message);
}

Then, wrap the field in a method. The name of the method is not important, because we are using a named contract. If we were using an interface we would be able to use a property, instead, and expose it as part of the interface.

public string Message()
{
    return _Message;
}

The approach we will take for the string Message will work for any built-in .NET type. A similar approach will also work for collections. If you want to pass custom types between extensions, you will need to declare those in a separate assembly  and reference it in each extension project.

MEF uses the concept of Exports and Imports, where data, classes, or methods are exported by one class and imported by one or more classes. We mark our Message method for export by adding the Export attribute and specifying a contract name that will be used by all importers (“LayerCount.Message”).

[System.ComponentModel.Composition.Export("LayerCount.Message")]
public string Message()
{
    return _Message;
}

Getting Started with Imports

We will create another extension in this same project. Add a new class (Project, Add class…) named MefImport that derives from Extension. On Activate() a new SimpleActionItem should be added with the caption “Retrieve Message”. In the corresponding event handler, we retrieve the message and write it to the trace listeners. You’ll need to add the same using statements the plugin template includes.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using DotSpatial.Controls;
using DotSpatial.Controls.Header;

public class MefImport : Extension
{
    public override void Activate()
    {
        App.HeaderControl.Add(new SimpleActionItem("Retrieve Message", ButtonClick));
        base.Activate();
    }

    public override void Deactivate()
    {
        App.HeaderControl.RemoveAll();
        base.Deactivate();
    }

    public void ButtonClick(object sender, EventArgs e)
    {
        var message = GetLayerCountMessage();
        System.Diagnostics.Trace.WriteLine(message);
    }
}

We declare a property that is essentially a pointer to a function that returns a string. In C# the Func<> class represents a “pointer to a function” so our property looks like this:

public Func<string> GetLayerCountMessage
{
    get;
    set;
}

 

Lastly, we need MEF to import a contract by the name of “LayerCount.Message” so we add the import attribute.

[System.ComponentModel.Composition.Import("LayerCount.Message")]
public Func<string> GetLayerCountMessage
{
    get;
    set;
}

 

You can build and run the application. Click Show Layer Count, then Retrieve Message. Drag and drop a layer on the map, and then click Show Layer Count and Retrieve Message, again.

image

Check the output window (Debug, Windows, Output) to see that the message was properly retrieved when GetLayerCountMessage() was called.

What if the exporting extension wasn’t available?

If we expected the exporting extension to only occasionally be available, we can use a named parameter of the Import attribute to indicate that instead of throwing an exception, we would like MEF to leave our GetLayerCountMessage property with its default value (null). Of course, we have to check for null before using the property, in that case.

public void ButtonClick(object sender, EventArgs e)
{
    if (GetLayerCountMessage != null)
    {
        var message = GetLayerCountMessage();
        System.Diagnostics.Trace.WriteLine(message);
    }
}

[System.ComponentModel.Composition.Import("LayerCount.Message", AllowDefault = true)]
public Func<string> GetLayerCountMessage
{
    get;
    set;
}

 

Now we can prevent the Layer Count extension from loading by commenting it out. When running the application, we will still find the Retrieve Message menu item, and clicking it won’t cause any problem.

Points of Interest

if multiple extensions will be exporting a given contract, the importer should use the ImportMany attribute and import an IEnumerable<> of the original, expected type.

Trying to expose Message as a property under a named contract won’t work because MEF will perform the import (and export) only once, by default. We would see the original null message, and nothing else.

Fields can also be exported and imported, so as a matter of style, I used properties.

How to Sort Layers Alphabetically

Introduction

DotSpatial is an open-source project that contains controls which can be used to manipulate and display geographic information. This article explains how to create a DotSpatial extension by using the online template. The extension we are creating will allow the user to sort the layers so that they appear in the Legend alphabetically.

Getting Started

If you are not familiar with creating a simple DotSpatial-based extension, please consider the introductory article. For practical purposes, we assume you are coming to this article after having completed the previous one. This article is based on DotSpatial 1.2.

Creating a New Project

Create a new project using the DotSpatial Plugin Template. You may delete the Readme.txt and modify the name of the MyPlugin1 class (to reflect the functionality provided by the extension). I named mine SortLayersPlugin. Change the caption of the SimpleActionItem from “My Button Caption” to “Sort Layers”.

Replace the ButtonClick event handler with the following code, which is explained inline.

public void ButtonClick(object sender, EventArgs e)
{
    // Get a list of layers sorted alphabetically by LegendText.
    var newLayers = App.Map.Layers.OrderByDescending(l => l.LegendText).ToList();

    // Suspending events speeds our changes up and prevents redrawing multiple times.
    // The events will be called only once, when we call ResumeEvents().
    App.Map.Layers.SuspendEvents();

    // By default, it appears some part of the layer is disposed when it is removed from the collection.
    // If we were to use App.Map.Layers.Clear(), we would stlil need to LockDispose on each layer.
    while (App.Map.Layers.Any())
    {
        var layer = App.Map.Layers[0];
        layer.LockDispose();
        App.Map.Layers.RemoveAt(0);
    }

    // As we add each layer back in, in the correct order, we UnlockDispose so that the layer can be disposed
    // at the appropriate time.
    foreach (IMapLayer newLayer in newLayers)
    {
        App.Map.Layers.Add(newLayer);
        newLayer.UnlockDispose();
    }
    App.Map.Layers.ResumeEvents();
}

Conclusion.

Build and run the application. You can add a few layers and then use the menu item to sort them.

image

Points of Interest

You can add another button to sort the layers in reverse alphabetical order by replacing App.Map.Layers.OrderByDescending(…)… with App.Map.Layers.OrderBy(…)….