Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to include an image in metadata for a Plug-In DLL using MEF?

C# .NET 4.0 WinForms

I started implementing MEF example code from this tutorial which describes creating a custom ExportAttribute Attribute for metadata. Everything was going swimmingly until I tried to include an image from a Resource File in the metadata. The goal being to extract the title, description, and icon for each plug-in DLL as metadata for constructing a menu of plug-ins in the main program.

I now get the compile error:

"An attribute argument must be a constant expression, typeof expression or array creation expression of an attribute parameter type"

So now I have a problem, and need to figure out either:

1) How to include an image in a attribute ?

or

2) How to include metadata in MEF without using attributes ?

Here is the code that I am using:

In the contract class:

// Metadata contract interface
public interface IPlugInMetadata
{
    string PlugInTitle { get; }
    string PlugInDescription { get; }
    Image PlugInIcon { get; }
}

// Plug-In contract interface
public interface IPlugIn
{
    void StartPlugIn(object systemObject);
    void StopPlugin();
}

The custom attribute in the Plug-In DLL:

[MetadataAttribute]
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class PluginMetadataAttribute : ExportAttribute
{
    public string title { get; set; }
    public string description { get; set; }
    public Image icon { get; set; }

    public PluginMetadataAttribute(string plugInTitle, string plugInDescription, Image plugInIcon) 
        : base(typeof(IPlugInMetadata))
    {
        title = plugInTitle;
        description = plugInDescription;
        icon = plugInIcon;
    }
}

And finally, the Program class in the Plug-In DLL:

[Export(typeof(IPlugIn))]
[PluginMetadata(ResourceFile.PlugInTitle, ResourceFile.PlugInDescription, ResourceFile.PlugInIcon24)]
public class Program : IPlugIn
{
    public void StartPlugIn(object systemObject)
    {
        Console.WriteLine("Start Plug-In: " + ResourceFile.PlugInTitle);
    }

    public void StopPlugin()
    {
        Console.WriteLine("Stop Plug-In: " + ResourceFile.PlugInTitle);
    }
}

This line is producing the error.

[PluginMetadata(ResourceFile.PlugInTitle, ResourceFile.PlugInDescription, ResourceFile.PlugInIcon24)]

Obviously the ResourceFile is not considered a constant, but then how can I use an image as metadata, or is that not possible ? ( Note that the image is set as "Embedded in .resx" )

Thanks for any help or suggestions !

like image 312
user1689175 Avatar asked Oct 21 '22 18:10

user1689175


1 Answers

Found a solution:

OK, as far as I can tell, you cannot use images or icons in MEF metadata. You can however assign an icon to the DLL file, just as you can for EXE files. The icon can then be read using a static method built into the Icon type for .NET:

Icon MyDLLIcon = Icon.ExtractAssociatedIcon(DLLFilePath);

I am still not exactly sure why you can return strings from a Resource file as MEF metadata, but not embedded Icons or Images.

Since I have been trying to piece together a functioning example program that provides a Plug-In menu with icons for a couple of days now, I figured that I would post the code in case it helps anyone else.


This is a fully functional example project with the following features:

  • Intended to be a single Solution with 5 Projects ( MainProgram, ContractInterfaces, PlugInA, PlugInB, PlugInC )

  • Post Build Events will automatically copy the DLLs from each project to a common "Plug-Ins" folder

  • The MainProgram (WinForm) project will build a catalog of the available DLL Plug-Ins and populate a ListView with the Icons and metadata Title of each Plug-In

  • Double-Clicking the ListView item will instantiate the Plug-In ( leveraging Lazy instantiation ), and start it.

  • Each Plug-In will receive a reference to the main Form at start up, create a new TextBox, and post it to the main Form to prove that it ran and can access the GUI.

  • The Title, Description, and Version metadata values of the selected Plug-In will print to the Console Window


I assigned a different Icon to each DLL ( from the old Visual Studio 6 Common Graphics Misc folder )

Screenshot

The icons were extracted from the DLL Plug-Ins to create the ListView, and the TextBoxes were created and posted by the DLLs to the GUI once they started ( after double-clicking each Plug-In item in the ListView ).


Add the following code to a fresh new C# WinForm project called "MainProgram" ( I used VS 2010 ):

For some reason the Code Sample parser is not liking the Using statements, so here they are as bullet points:

  • using System;
  • using System.Collections.Generic;
  • using System.ComponentModel.Composition;
  • using System.ComponentModel.Composition.Hosting;
  • using System.Drawing;
  • using System.IO;
  • using System.Windows.Forms;
  • using ContractInterfaces;
  • (namespace) MainProgram

public partial class Form1 : Form
{
    // Prerequisites to run:
    //      1)  Project, Add Reference, Projects, ContractInterface
    //      2)  Project, Add Reference, .NET, System.ComponentModel.Composition

    [ImportMany(typeof(IPlugIn))]
    private IEnumerable<Lazy<IPlugIn, IPlugInMetadata>> LoadedPlugIns;

    List<PlugInInfo> AvailablePlugIns = null;


    public Form1()
    {
        InitializeComponent();
    }

    private void Form1_Load(object sender, EventArgs e)
    {
        // Get a list of the available Plug-Ins
        AvailablePlugIns = GetPlugInList();

        // Prepare an ImageList to hold the DLL icons
        ImageList ImgList = new ImageList();
        ImgList.ColorDepth = ColorDepth.Depth32Bit;
        ImgList.ImageSize = new Size(32, 32);

        // Populate ImageList with Plug-In Icons
        foreach (var item in AvailablePlugIns)
        {
            ImgList.Images.Add(item.PlugInIcon.ToBitmap());
        }

        // Assign the ImageList to the ListView
        listView1.LargeImageList = ImgList;

        int imageIndex = 0;

        // Create the ListView items
        foreach (var item in AvailablePlugIns)
        {
            listView1.Items.Add(item.PlugInTitle, imageIndex);
            imageIndex++;
        }

        listView1.MouseDoubleClick += new MouseEventHandler(listView1_MouseDoubleClick);
    }

    void listView1_MouseDoubleClick(object sender, MouseEventArgs e)
    {
        // Get the Plug-In index number 
        int plugInNum = listView1.SelectedItems[0].Index;

        PlugInInfo selectedPlugIn = AvailablePlugIns[plugInNum];

        // Call the StartPlugIn method in the selected Plug-In.
        // Lazy Instantiation will fully load the Assembly here
        selectedPlugIn.PlugIn.StartPlugIn(this);

        Console.WriteLine("Plug-In Title:          {0}", selectedPlugIn.PlugInTitle);
        Console.WriteLine("Plug-In Description:    {0}", selectedPlugIn.PlugInDescription);
        Console.WriteLine("Plug-In Version:        {0}", selectedPlugIn.PlugInVersion);
        Console.WriteLine();
    }



    private List<PlugInInfo> GetPlugInList()
    {
        // Create a List to hold the info for each plug-in
        List<PlugInInfo> plugInList = new List<PlugInInfo>();

        // Set Plug-In folder path to same directory level as Solution
        string plugInFolderPath = System.IO.Path.Combine(Application.StartupPath, @"..\..\..\Plug-Ins");

        // Test if the Plug-In folder exists
        if (!Directory.Exists(plugInFolderPath))
        {
            // Plug-In Folder is missing, so try to create it
            try
            { Directory.CreateDirectory(plugInFolderPath); }
            catch
            { MessageBox.Show("Failed to create Plug-In folder", "Folder Creation Error:", MessageBoxButtons.OK, MessageBoxIcon.Exclamation); }
        }

        try
        {
            // Create a catalog of plug-ins
            var catalog = new DirectoryCatalog(plugInFolderPath, "*.dll");
            AggregateCatalog plugInCatalog = new AggregateCatalog();
            plugInCatalog.Catalogs.Add(catalog);
            CompositionContainer container = new CompositionContainer(plugInCatalog);

            // This line will fetch the metadata from each plug-in and populate LoadedPlugIns
            container.ComposeParts(this);

            // Save each Plug-Ins metadata
            foreach (var plugin in LoadedPlugIns)
            {
                PlugInInfo info = new PlugInInfo();

                info.PlugInTitle = plugin.Metadata.PlugInTitle;
                info.PlugInDescription = plugin.Metadata.PlugInDescription;
                info.PlugInVersion = plugin.Metadata.PlugInVersion;
                info.PlugIn = plugin.Value;

                plugInList.Add(info);
            }

            int index = 0;

            // Extract icons from each Plug-In DLL and store in Plug-In list
            foreach (var filePath in catalog.LoadedFiles)
            {
                plugInList[index].PlugInIcon = Icon.ExtractAssociatedIcon(filePath);
                index++;
            }
        }
        catch (FileNotFoundException fex)
        {
            Console.WriteLine("File not found exception : " + fex.Message);
        }
        catch (CompositionException cex)
        {
            Console.WriteLine("Composition exception : " + cex.Message);
        }
        catch (DirectoryNotFoundException dex)
        {
            Console.WriteLine("Directory not found exception : " + dex.Message);
        }

        return plugInList;
    }
}


public class PlugInInfo
{
    public string PlugInTitle { get; set; }
    public string PlugInDescription { get; set; }
    public string PlugInVersion { get; set; }
    public Icon PlugInIcon { get; set; }
    public IPlugIn PlugIn { get; set; }
}

Now add a ListView Control called "listView1" to the main Form, and keep it to the Right side of the Form. The dynamically created TextBoxes from the Plug-Ins will show on the left.


Next add a Class Project called "ContractInterfaces", then include this code:

  • using System.Windows.Forms;
  • (namespace) ContractInterfaces

// Prerequisites to run:
//      1)  Project, Add Reference, .NET, "System.Windows.Forms"

public interface IPlugIn
{
    void StartPlugIn(Form mainForm);
}

public interface IPlugInMetadata
{
    string PlugInTitle { get; }
    string PlugInDescription { get; }
    string PlugInVersion { get; }
}

Next add a Class Project called "PlugInA", then include this code:

  • using System;
  • using System.ComponentModel.Composition;
  • using System.Windows.Forms;
  • using ContractInterfaces;
  • (namespace) PlugInA

    // Prerequisites to run:
//      1)  Project, Add Reference, Projects, "ContractInterface"
//      2)  Project, Add Reference, .NET, "System.Windows.Forms"
//      3)  Project, Add Reference, .NET, "System.ComponentModel.Composition"
//      4)  Project, Properties, Build Events, Post-Build event command line:
//          xcopy "$(ProjectDir)$(OutDir)$(TargetFileName)"  "$(SolutionDir)Plug-Ins\" /Y
//      5)  Project, Properties, Build Events, Run the post-build event:, Always
//      6)  Project, Properties, Application, Icon and manifest, [Select an icon]

[Export(typeof(IPlugIn))]
[PluginMetadata]
public class Program : IPlugIn
{
    private Form MainForm;

    public void StartPlugIn(Form mainForm)
    {
        MainForm = mainForm;

        // Place a TextBox on the Main Form
        TextBox textBox = new TextBox();
        textBox.Text = "PlugInA";
        MainForm.Controls.Add(textBox);
        textBox.Width = 65;
        textBox.Height = 20;
        textBox.Top = 0;
        textBox.Left = 0;
    }
}

// Create a custom strong-typed Metadata Attribute for MEF
[MetadataAttribute]
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class PluginMetadataAttribute : ExportAttribute
{
    public string PlugInTitle { get; set; }
    public string PlugInDescription { get; set; }
    public object PlugInVersion { get; set; }

    public PluginMetadataAttribute()
        : base(typeof(IPlugInMetadata))
    {
        PlugInTitle = "Plug-In A";
        PlugInDescription = "This is Plug-In A";
        PlugInVersion = "1.0.0.0";
    }
}

Next add a Class Project called "PlugInB", then include this code:

  • using System;
  • using System.ComponentModel.Composition;
  • using System.Windows.Forms;
  • using ContractInterfaces;
  • (namespace) PlugInB

// Prerequisites to run:
//      1)  Project, Add Reference, Projects, "ContractInterface"
//      2)  Project, Add Reference, .NET, "System.Windows.Forms"
//      3)  Project, Add Reference, .NET, "System.ComponentModel.Composition"
//      4)  Project, Properties, Build Events, Post-Build event command line:
//          xcopy "$(ProjectDir)$(OutDir)$(TargetFileName)"  "$(SolutionDir)Plug-Ins\" /Y
//      5)  Project, Properties, Build Events, Run the post-build event:, Always
//      6)  Project, Properties, Application, Icon and manifest, [Select an icon]

[Export(typeof(IPlugIn))]
[PluginMetadata]
public class Program : IPlugIn
{
    private Form MainForm;

    public void StartPlugIn(Form mainForm)
    {
        MainForm = mainForm;

        // Place a TextBox on the Main Form
        TextBox textBox = new TextBox();
        textBox.Text = "PlugInB";
        MainForm.Controls.Add(textBox);
        textBox.Width = 65;
        textBox.Height = 20;
        textBox.Top = 30;
        textBox.Left = 0;
    }
}

// Create a custom strong-typed Metadata Attribute for MEF
[MetadataAttribute]
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class PluginMetadataAttribute : ExportAttribute
{
    public string PlugInTitle { get; set; }
    public string PlugInDescription { get; set; }
    public object PlugInVersion { get; set; }

    public PluginMetadataAttribute()
        : base(typeof(IPlugInMetadata))
    {
        PlugInTitle = "Plug-In B";
        PlugInDescription = "This is Plug-In B";
        PlugInVersion = "1.0.0.1";
    }
}

Next add a Class Project called "PlugInC", then include this code:

  • using System;
  • using System.ComponentModel.Composition;
  • using System.Windows.Forms;
  • using ContractInterfaces;
  • (namespace) PlugInC

// Prerequisites to run:
//      1)  Project, Add Reference, Projects, "ContractInterface"
//      2)  Project, Add Reference, .NET, "System.Windows.Forms"
//      3)  Project, Add Reference, .NET, "System.ComponentModel.Composition"
//      4)  Project, Properties, Build Events, Post-Build event command line:
//          xcopy "$(ProjectDir)$(OutDir)$(TargetFileName)"  "$(SolutionDir)Plug-Ins\" /Y
//      5)  Project, Properties, Build Events, Run the post-build event:, Always
//      6)  Project, Properties, Application, Icon and manifest, [Select an icon]

[Export(typeof(IPlugIn))]
[PluginMetadata]
public class Program : IPlugIn
{
    private Form MainForm;

    public void StartPlugIn(Form mainForm)
    {
        MainForm = mainForm;

        // Place a TextBox on the Main Form
        TextBox textBox = new TextBox();
        textBox.Text = "PlugInC";
        MainForm.Controls.Add(textBox);
        textBox.Width = 65;
        textBox.Height = 20;
        textBox.Top = 60;
        textBox.Left = 0;
    }
}

// Create a custom strong-typed Metadata Attribute for MEF
[MetadataAttribute]
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class PluginMetadataAttribute : ExportAttribute
{
    public string PlugInTitle { get; set; }
    public string PlugInDescription { get; set; }
    public object PlugInVersion { get; set; }

    public PluginMetadataAttribute()
        : base(typeof(IPlugInMetadata))
    {
        PlugInTitle = "Plug-In C";
        PlugInDescription = "This is Plug-In C";
        PlugInVersion = "1.0.0.2";
    }
}

The Solution should look like this:

Solution


Right-click on the Solution, and select "Project Dependencies...". Set the dependencies as follows:

  • MainProgram - depends on - ContractInterface
  • PlugInA - depends on - ContractInterface
  • PlugInB - depends on - ContractInterface
  • PlugInC - depends on - ContractInterface
  • ContractInterface - depends on - [nothing]

Right-click on the Solution, and select "Project Build Order...". The build order should be as follows:

  1. ContractInterface
  2. PlugInA
  3. PlugInB
  4. PlugInC
  5. MainProgram

Build and run the program. You should see the 3 DLL files copied to a new "Plug-Ins" folder at the same directory level as the Solution file (*.sln). If not, check the Project Build Order, Dependencies, and that you entered the Post-Build Events according to the notes in the Plug-In code above. If the files are there, then the ListView should be populated in the Form with the Plug-In entries. Double-click each ListView entry to start the Plug-In.

Have fun, I hope this helps someone....

like image 111
user1689175 Avatar answered Oct 27 '22 10:10

user1689175