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:
- Use the Visual Studio IDE to create a new Class Library project and add it to the existing ProjectMIDI solution.
- 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.
- Download and install the ProjectMIDI source code.
Be sure to include Source code during installation.
- Open the ProjectMidiAll solution in Visual Studio .NET.
- Add a new project to the solution.
Right-click on Solution ProjectMidiAll in the Solution Explorer, and select Add -> New Project.
- Select the Class Library template under Visual C# Projects.
- Select a name for the new assembly. In this case, I am creating the SampleAssembly assembly. Give your assembly a new, unique name.
- Select OK.
A new project will be created and added to the solution. The new .CS file should now be displayed in the IDE.
- 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.
- 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
{
public class SampleAssembly
{
public SampleAssembly()
{
}
}
}
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).
- 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.
- 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.
- 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.
- 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.
- 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.
- Copy the
projectMidiParent
member variable.
This variable is used by the ProjectMidiParent
property copied in the step above.
- Copy the
PerformStartProcessing
method code.
This method is called at strategic points during the ProjectMidi start up.
- 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
{
[ProjectMidiAssemblyAttribute(AssemblyType.Applet, "Sample" )]
public class SampleAssembly : System.Windows.Forms.Form, IProjectMidiHideShow
{
private IProjectMidiParent projectMidiParent;
public SampleAssembly()
{
}
#region ProjectMidiParent
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
[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.
- Add a call to
InitializeComponent()
to the constructor.
This should be the only line in our constructor.
#region CTOR
public SampleAssembly()
{
InitializeComponent();
}
#endregion
- 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));
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:
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.
- Right-click on the new project in the Solution Explorer.
- Select Properties
- Select Build under Configuration Properties.
- 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:
- 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.
- 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:
- Your new assembly's window is displayed.
- The name of your assembly now appears in the icon menu Assemblies menu.
Using this menu item, you can hide or restore your window.
- 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.
- Add a button to the form.
- 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.
- 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
{
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.
- Add a text box control to the form.
- Create the following action code:
[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:
[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:
[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:
- Add a
ContextMenu
control to the form.
- 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.
- 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;
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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- Create a new connection.
- Set the source of this connection in the Sample assembly ContextMenu event.
- Now, make this connection a submenu by right-clicking on the dest assembly and selecting SubMenu.
- 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".
- Close the menu editor.
- 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.
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.