Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

ProjectMIDI, Part 2: Creating a Custom ProjectMIDI Assembly

0.00/5 (No votes)
24 Jan 2006 1  
This article describes how to create your own ProjectMIDI assembly.

Image of ProjectMIDI SampleAssembly Dialog

Introduction

In my first article about ProjectMIDI, I described its basic architecture and operation. One of the strengths of ProjectMIDI is the ease with which it can be extended. In this article, I'm going to demonstrate how this is done. I'm assuming that you have already read the first ProjectMIDI article, and will be building upon the information in that article.

In this article, I will sequentially step through the process that I used to create the SampleAssembly code. The finished code is already a part of the original package. You can choose to recreate the file, or simply follow along in the source code as we go.

Quick Start

The code that I created in writing this article has already been added to the original article's download packages. Please download the code from the original article or from the ProjectMIDI website.

Background

In the first article, we learned that one of the primary objectives of ProjectMIDI is that it be easily extensible. I want to be able to quickly and easily create simple little applets to control various aspects of my MIDI equipment. In this article, I am going to show how easy it is to do this by walking you through the steps to creating the SampleAssembly code.

Create the New ProjectMIDI Assembly

OK, so here we go. The first step will be to create the basic .NET assembly, and then add the ProjectMidi interfaces to it. There are several ways that I can think of to do this. Maybe, in the future, I will create a Visual Studio Wizard to do this, but for now, we are stuck with doing it manually. The approach that I normally use is:

  1. Use the Visual Studio IDE to create a new Class Library project and add it to the existing ProjectMIDI solution.
  2. Cut and paste the required interface code from an existing assembly to the new assembly. TraceWindow is a good source for doing this, since it is a very simple assembly.

So, let's create a new assembly, and add it to the existing ProjectMIDI solution.

  1. Download and install the ProjectMIDI source code.

    Be sure to include Source code during installation.

  2. Open the ProjectMidiAll solution in Visual Studio .NET.
  3. Add a new project to the solution.

    Right-click on Solution ProjectMidiAll in the Solution Explorer, and select Add -> New Project.

  4. Select the Class Library template under Visual C# Projects.
  5. Select a name for the new assembly. In this case, I am creating the SampleAssembly assembly. Give your assembly a new, unique name.
  6. Select OK.

    A new project will be created and added to the solution. The new .CS file should now be displayed in the IDE.

  7. Change the default class1 name.

    Unfortunately, the file that the IDE creates is named "class1.cs" instead of something more meaningful like "SampleAssembly.cs". To fix this, right click on the class1.cs file name in the Solution Explorer and select Rename to give it the name of your new assembly.

  8. Likewise, edit the new file to change every occurrence of Class1 to match the name of your assembly.

    In this case, I change every occurrence of Class1 to SampleAssembly.

OK, so now, we have a new assembly added to our ProjectMIDI solution. It should look something like this:

using System;

namespace SampleAssembly
{
    /// <summary>
    /// Summary description for SampleAssembly.
    /// </summary>
    public class SampleAssembly
    {
        public SampleAssembly()
        {
            //
            // TODO: Add constructor logic here
            //
        }
    }
}

This isn't a ProjectMIDI assembly. As described in the first article, all ProjectMIDI assemblies need to derive from and implement the IProjectMidi interface. So, let's do that now.

Implementing the IProjectMidi Interface

To make cut-and-paste easier, I've opened up the TraceWindow.cs file in the IDE along with the new SampleAssembly.cs file. This way, I can easily switch back and forth between them.

If your new assembly is not going to implement a user interface, then things are a bit simpler, and you need only derive your class from IProjectMidi directly. Since SampleAssembly is going to implement a window, it needs to derive from both the System.Windows.Form.Form class as well as the IProjectMidi interface (or a derivative of it).

  1. Add the appropriate using lines.

    Copy the using ProjectMidiNS line. This will give us access to the ProjectMIDI namespace.

    Copy the using System.Windows.Forms and using System.Drawing lines, if your assembly implements a user interface window.

  2. Add references for each using statement copied in the step above.

    Right-click on References for the new project in the Solution Explorer, and select Add Reference.

    Select ProjectMidiInterfaces under the Projects tab for the using ProjectMidiNS reference.

    Select System.Windows.Forms.dll under the .NET tab for the using System.Windows.Forms reference.

    Select System.Drawing.dll under the .NET tab for the using System.Drawing reference.

  3. Give your class a ProjectMidiAssemblyAttribute.

    Copy the ProjectMidiAssemblyAttribute line to the line before your class statement. Change the name specified to match the name of your assembly. ProjectMIDI will use this name in its Assemblies list in the icon menu.

  4. Derive your class from System.Windows.Forms.Form and IProjectMidi.

    Copy the text to the right of public class TraceWindow and paste it to the right of your class name. Note, in this case, the interface is not IProjectMidi but instead IProjectMidiHideShow. This is an interface which derives from IProjectMidi, and adds a method to allow ProjectMIDI to hide and show this assembly's window via the status tray icon menu.

  5. Copy the ProjectMidiParent property code

    The ProjectMidiParent property is used to allow ProjectMidi to pass a back pointer to each assembly. This back pointer gives each assembly access to several useful properties and methods provided by the ProjectMidi core executable.

  6. Copy the projectMidiParent member variable.

    This variable is used by the ProjectMidiParent property copied in the step above.

  7. Copy the PerformStartProcessing method code.

    This method is called at strategic points during the ProjectMidi start up.

  8. Copy the IProjectMidiHideShow members section.

    The ProjectMidi core executable calls this method to toggle the visible state of each assembly.

At this point, your code should look like this, and should compile without errors:

using System;
using System.Windows.Forms;
using System.Drawing;
using ProjectMidiNS;

namespace SampleAssembly
{
    /// <summary>
    /// Summary description for SampleAssembly
    /// </summary>
    [ProjectMidiAssemblyAttribute(AssemblyType.Applet, "Sample" )]
    public class SampleAssembly : System.Windows.Forms.Form, IProjectMidiHideShow
    {
        private IProjectMidiParent projectMidiParent;

        public SampleAssembly()
        {
            //
            // TODO: Add constructor logic here
            //
        }

        #region ProjectMidiParent
        // This will happen immediately after construction
        public IProjectMidiParent ProjectMidiParent 
        {
            set 
            { 
                projectMidiParent = value; 
            }
        }
        #endregion
        #region PerformStartProcessing
        public void PerformStartProcessing( StartPhase phase )
        {
            switch(phase)
            {
                case StartPhase.FirstTimeEver:
                    break;
                case StartPhase.AfterLoading:
                    break;
                case StartPhase.AllAssembliesLoaded:
                    break;
                case StartPhase.StartConnecting:
                    break;
                case StartPhase.AllAssembliesConnected:
                    break;
                case StartPhase.Terminating:
                    break;
                default:
                    Console.WriteLine("Unknown StartPhase");
                    break;
            }
        }
        #endregion

        #region IProjectMidiHideShow Members
        // Toggle Hide/Show state of window
        [ProjectMidiActionAttribute("Hide/Show",0, 
           "","Hide or Show Trace window.")]
        public void OnHideShow(object sender, EventArgs e) { HideShow(); }
        public void HideShow()
        {
            this.Visible = !this.Visible;
        }
        #endregion
    }
}

Add the InitializeComponent Method

Because we started with the Class Library template, our class was not created as a form. To add this functionality, we need to add an InitializeComponent method and call it from our constructor.

  1. Add a call to InitializeComponent() to the constructor.

    This should be the only line in our constructor.

    #region CTOR
    public SampleAssembly()
    {
        // Required for Windows Form Designer support
        InitializeComponent();
    }
    #endregion
  2. Open the View Designer for our new file, and assign some properties.

    This will cause the View Designer to automatically create the InitializeComponent method for us. At a minimum, assign a Text value to the window, and maybe an Icon if you wish.

    Upon switching back to the code view, you should see that the InitializeComponent method has been added, and looks approximately like this:

    private void InitializeComponent()
    {
        System.Resources.ResourceManager resources = 
          new System.Resources.ResourceManager(typeof(SampleAssembly));
        // 
        // SampleAssembly
        // 
        this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);
        this.ClientSize = new System.Drawing.Size(292, 266);
        this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon")));
        this.Name = "SampleAssembly";
        this.Text = "SampleAssembly";
    }

At this point, we now have a working ProjectMidi assembly. It doesn't really do anything but display a blank window. There is some functionality provided by the core ProjectMidi executable, however:

  • The window's size and position will be saved and restored between sessions.
  • The window's name will be displayed in the system tray icon menu's Assemblies menu.

    Selecting this will alternately hide and show the window.

Note that the assembly needs to be copied to ProjectMidi's Assemblies folder in order for it to be run. The easiest way to do this is to modify the new project's properties to create the assembly DLL in the runtime assemblies folder.

  1. Right-click on the new project in the Solution Explorer.
  2. Select Properties
  3. Select Build under Configuration Properties.
  4. Change the Output Path to ..\Runtime\Assemblies.

    This assumes that you created your project at the same directory as the other ProjectMidi projects.

Now, when you build your project, it will automatically be placed into the runtime folder.

This is probably also a good time to change the properties for the ProjectMidiInterfaces reference. Visual Studio .NET will have set the Copy Local setting to True, by default. This will result in a copy of the ProjectMidiInterfaces.dll to be copied to the output directory each time you build the code. Before changing the output directory, this wasn't a problem. Now, however, it is going to put a copy of that DLL into the RunTime\Assemblies directory, which is not what we want. To change this:

  • Select ProjectMidiInterfaces under the References section in the Solution Explorer.
  • Change the Copy Local setting from True to False.

Test Run the New Blank Assembly

OK, let's give the code a test run to ensure that everything that we have done so far is working. Here're some things to check before proceeding:

  1. Delete previous ProjectMidi registry settings.

    If you have previously installed and/or run ProjectMidi, then it will have set the FoldersPath registry setting. This may be pointing to a directory other than where you are now putting your newly built assemblies. You can either change this setting, or delete it or all of the ProjectMidi settings from the Registry altogether. To do this, launch RegEdit (Start -> Run -> RedEdit) and locate the following key:

    HKEY_LOCAL_MACHINE\SOFTWARE\ProjectMidi\FoldersPath

    This key needs to point to the parent folder where your Assemblies folder is located. If it doesn't, then you will need to change or delete it.

  2. Ensure that you are working in the ProjectMidi solution.

    You can't directly execute a ProjectMidi assembly. You need to build the assembly and copy it to the Assemblies folder, as described above. Then, you need to launch ProjectMidi.exe. ProjectMidi.exe will, in turn, load and execute the assembly.

Observe that the following behaviors are working:

  1. Your new assembly's window is displayed.
  2. The name of your assembly now appears in the icon menu Assemblies menu.

    Using this menu item, you can hide or restore your window.

  3. ProjectMidi will save and restore your window's size and position.

    If you move and then close the window, upon restoring it later, it will appear at the same location and size.

By convention, I set the ShowInTaskbar setting of each ProjectMidi assembly form to false. I do this because there tend to be a lot of ProjectMidi windows showing, and I don't like cluttering up the task bar. You can set this parameter from the form's properties. These are displayed while viewing the form in the View Designer.

I also assign my standard ProjectMidi.ico icon to each window's Icon property. Maybe, someday, I'll get creative and give each window a unique icon.

Add Functionality

OK, now we get to start having some fun. Up until now, we have just been creating boiler-plate code. Now, we'll start adding real functionality.

Note: I realize that the above boiler-plate code could have been automatically created by a Visual Studio Wizard plug-in or something along those lines. That is a project that I'll put off for another time.

I'm going to add a few controls just to demonstrate how to do this. I don't expect that this will result in a logical grouping of controls, but it should be fun and give us something to play with.

Add a Button

A button can be used to trigger single value actions such as Increment Page Number.

  1. Add a button to the form.
  2. Create the button click event handler.

    Display the Events properties for the button by clicking on the little lightning bolt in the properties window for the button. Then, double-click on the Click event.

  3. Edit the new event handler code as shown:
    [ProjectMidiEventAttribute("Button",1,"SetNextSong", 
       ConnectionType.Normal,"Sent whenever the button is pressed.")]
    public event ProjectMidiEventHandler OnSampleButton;
    private void sampleButton_Click(object sender, System.EventArgs e)
    {
        try
        {
            // Fire the event (if connected).
            if(OnSampleButton!=null) OnSampleButton( this, e );
        }
        catch(Exception err)
        {
            Console.WriteLine("SampleButtonClick error: {0}",err.Message);
        }
    }

    Your code will be different depending on the name you give the button.

Add a Text Box

Add a text box to display the value of events connected to its action node.

  1. Add a text box control to the form.
  2. Create the following action code:
    // Here is a sample action. 
    // The name specified in the attribute will be displayed in the connections list. 
    // The 2nd parameter specifies how many data values it expects; 128 in this case.
    // The optional 3rd parameter can specify an auto-connect name.
    // This action will be invoked whenever any events connected to it are fired.
    // It will display the value passed in the list box.
    [ProjectMidiActionAttribute("TextBox",128,
      "GetSongNum","String or number to be displayed in the textbox.")]
    public void textBoxAction( object source, EventArgs e )
    {
        StringEventArgs eString = e as StringEventArgs;
        IntEventArgs eInt = e as IntEventArgs;
        if(eString != null)
        {
            TraceMessage( "Text Box Action: "+eString.str );
            sampleTextBox.Text = eString.str;
        }else if(eInt != null)
        {
            TraceMessage( "Text Box Action: "+eInt.data );
            sampleTextBox.Text = eInt.data.ToString();
        }
    }

Add a Trackbar

A TrackBar is interesting because it can be both an Action, receiving position information from a connection, as well as an Event, sending user selected position information to a connection.

  • Add a Trackbar control to the form.
  • Add a scroll event handler.

    Create a scroll event handler and a ProjectMidi event as shown:

    // This is the event associated with the track bar.
    // It will be fired every time the track bar is moved.
    // It is "auto-connected" to set the song #, but the user can change this.
    [ProjectMidiEventAttribute("TrackBar",16,"SetSongNum",
      ConnectionType.Normal,"Sent whenever the TrackBar is moved.")]
    public event ProjectMidiEventHandler OnTrackBar;
    private void sampleTrackBar_Scroll(object sender, System.EventArgs e)
    {
        int newvalue = sampleTrackBar.Value;
        try
        {
            IntEventArgs eInt = new IntEventArgs( newvalue );
            if(OnTrackBar!=null) OnTrackBar(this,eInt);
        }
        catch(Exception err)
        {
            Console.WriteLine("Trackbar error: {0}",err.Message);
        }
    }
  • Add a ProjectMidi Action

    Add code as shown to implement a ProjectMidi Action:

    // Here is the action associated with the TrackBar.
    // This allows having the TrackBar set by other controls.
    // It is auto-connected to Song # so that it will track it if changed elsewhere.
    [ProjectMidiActionAttribute("TrackBar",128, 
      "GetSongNum","Sets the position of the trackbar.")]
    public void trackBarAction( object source, EventArgs e )
    {
        IntEventArgs eInt = e as IntEventArgs;
        if(eInt != null)
        {
            TraceMessage( "Sample TrackBar Action: "+eInt.data );
            sampleTrackBar.Value = eInt.data;
        }
    }

Add a Context Menu

Context menus are one of my favorite items. By adding a context menu to your form, you allow the user to link to whatever actions they like. The context menu will be displayed whenever the user right-clicks on the form. The items displayed on the menu are specified by making connections from the event that we create for the context menu to any other action in any other ProjectMidi assembly! This is quite a powerful and useful feature.

As shown in the following code, implement a context menu using the following steps:

  1. Add a ContextMenu control to the form.
  2. Create a context menu popup handler method.

    Select the ContextMenu control, then double-click the Popup property. This method is called prior to displaying the context menu. We'll add code to call each action connected to our context menu event, passing them a reference to the ContextMenu object.

  3. Add a new event with ProjectMidiEventAttribute.
    private void SampleAssembly_Load(object sender, System.EventArgs e)
    {
        contextMenu.Popup += new System.EventHandler(this.contextMenu_Popup);
    }
    
    [ProjectMidiMenuEventAttribute("ContextMenu",1,"Menu",ConnectionType.Normal,
            "Displays connected action items on the Sample assmebly's context menu.")]
    public event ProjectMidiEventHandler ContextMenuEvent;
    
    // This event is fired before the icon menu is displayed
    private void contextMenu_Popup(object sender, System.EventArgs e)
    {
        Console.WriteLine("Sample FireContextMenuEvent");
        contextMenu.MenuItems.Clear();
        MenuEventArgs e2 = new MenuEventArgs( contextMenu, "Sample" );
        if(ContextMenuEvent != null) ContextMenuEvent( this, e2 );
    }

Playing with SampleAssembly

Now that the code is complete, compile it, and copy the SampleAssembly.dll to ProjectMidi's Assemblies directory. Launch ProjectMidi.exe, and verify that the SampleAssemblies window is displayed. Now, let's play with it.

Make sure that the Connections dialog, the Sample dialog, and the SongSet dialog are all visible. If not, then right-click on the Project MIDI icon in the Task Bar to bring up the icon menu. Selecting any of the names listed in the Assemblies submenu will cause the associated assembly to alternately be displayed or hidden.

First, let's establish a connection from the SongSet song number to the SampleAssembly's text box. This way, whenever anything changes the song number, it will be displayed in the SampleAssembly text box.

  1. Right click on the ID column of any connection on the Connection dialog. Select the New Connection menu item. This will create a new connection.
  2. Click on the ID column header to sort the connections list by ID number. Then, scroll down to the bottom of the list to display the newly created connection.
  3. Right-click on each of the columns of the new connection, and select the SongSet assembly, song number event, and the Sample dest assembly Text Box Action.

Test the connection by selecting different songs in the Song Set dialog. As each song is selected, its number should be displayed in the text box.

Now, let's connect the TrackBar to the song number.

  1. Right click on the ID column of any connection on the Connection dialog. Select the New Connection menu item. This will create a new connection.
  2. Click on the ID column header to sort the connections list by ID number. Then, scroll down to the bottom of the list to display the newly created connection.
  3. Right-click on each of the columns of the new connection, and select the SongSet assembly, song number event, and the Sample dest assembly Track Bar Action.
  4. Using the same procedure, create another new connection, and connect the Sample assembly TrackBar event to the SongSet assembly Song Number Action.

Now, the TrackBar is connected as both an Event and an Action to the Song Number.

Menu Connections

Let's create some Sample Assembly context menu entries to select the next or previous songs.

Menu items can be added directly to a menu. Alternatively, a submenu can be created, and menu items added to the submenu.

For this example, we will create a "Song" submenu, and add items to select the next or previous song.

  1. Create a new connection.
  2. Set the source of this connection in the Sample assembly ContextMenu event.
  3. Now, make this connection a submenu by right-clicking on the dest assembly and selecting SubMenu.
  4. Right-click on this new submenu connection item, and select "Edit Context Menu". This will allow you to edit the name of this item. Change the name to "Song".
  5. Close the menu editor.
  6. Now, create new connections from the Sample assembly Song event. This event is, in fact, the submenu you just created. Create connections from it to the SongSet assembly Prev Song and Next Song actions.

Image of Sample Context Menu

Now, when you right-click on the Sample Assembly dialog, a context menu will be displayed with two menu items: Set next song and Set previous song in a "Song" submenu.

Points of Interest

I'm hoping that this article has shown you how easy it can be to extend ProjectMIDI to suite your own unique needs. I hope many of you will feel motivated to create your own ProjectMidi assemblies to take control of your MIDI equipment.

History

  • 21 Jan 2006 - Initial document creation.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here