The C#Builder Open Tools API by Erik Berry

By: Erik Berry

Abstract: The C#Builder Open Tools API (OTA) allows developers to add functionality to the IDE using any .NET language. This article introduces the major changes in the new .NET OTA and provides several usage examples and practical hints.

The C#Builder Open Tools API

The Open Tools API (OTA) provides developers with the ability to extend and enhance the functionality of the C#Builder IDE. Because the C#Builder OTA closely resembles the interfaces provided with the Delphi and C++Builder products, developers who are familiar with those tools should feel at home quickly when creating addins for C#Builder. This article will introduce seasoned developers to the major changes and additions to the new .NET-based OTA as well as providing an overview of the interfaces and development process for new OTA developers. The sample code for this article is available for download in CodeCentral.

The OTA is contained in the Borland.Studio.ToolsAPI namespace inside the assembly Borland.Studio.ToolsAPI.dll. C#Builder includes basic documentation on the contents of the assembly, but you can also use .NET Reflector or the Borland Reflection.exe tool to inspect the assembly directly. For all of the OTA addin assemblies you create, you must add a reference to the Borland.Studio.ToolsAPI.dll assembly located in the BDS\1.0\Bin directory.

"Hello World"

Every introductory lesson needs a basic "Hello World" example, so here comes one for C#Builder. Though these examples are written in C#, any .NET language can be used to create OTA addins. The class below implements IOTAMenuWizard, which is a basic type of addin that adds a menu item to the Help menu and can execute some code when the menu item is selected. Create a new .NET assembly by selecting File, New, Other, C# Projects, Class Library. Add a reference to the Borland.Studio.ToolsAPI.dll assembly by choosing Projects, Add Reference and clicking the Browse button. Also add a reference to the System.Windows.Forms.dll assembly so the MessageBox class is available. Then edit the class code to look like this:

using System;
using Borland.Studio.ToolsAPI;
using System.Windows.Forms;

namespace ToolsAPI.Introduction
{
public class HelloWorldWizard : IOTAMenuWizard
{
public static void IDERegister()
{
IOTAWizardService wizServ = (IOTAWizardService)
BorlandIDE.GetService(typeof(IOTAWizardService));
wizServ.AddWizard(new HelloWorldWizard());
}
public string IDString { get { return "EB.HelloWizard"; } }
public void Execute() { MessageBox.Show("Hello World!"); }
public string Name { get { return "Hello World Wizard"; } }
public void Destroyed() { /* nothing */ }
public string MenuText { get { return "Hello World Wizard"; } }
public bool Checked { get { return false; } }
public bool Enabled { get { return true; } }
}
}

The example illustrates both of the major kinds of interfaces you will find in the OTA. The IDE implements many interfaces (including all of those with "Service" or "Manager" in their name) for you that you will never have to implement yourself. IOTAWizardService is one such interface that allows adding and removing wizards (the OTA term for a generic addin) inside the IDE. The other major interface type is one that you must implement so the IDE can interface with your code. For example, to add a menu item to the IDE using IOTAMenuWizard, the IDE needs to know things like the menu item's text, whether the menu item is enabled, and a unique ID for your wizard.

After you compile your assembly, to get it loaded into the IDE, you can add a new string entry to the registry here:

HKCU\Software\Borland\BDS\1.0\Known IDE Assemblies

The entry name should be the full path and file name of the compiled assembly dll. The entry data can be any string, but some value is required for the assembly to be loaded into the IDE:

C:\OTAIntro\OTAIntro.dll = OTAIntro

Registry Screenshot

After adding the registry entry above, the IDE will load your assembly at startup. The IDE will try to locate a public static void function with the name IDERegister. If such a method is found, it is executed, and this is where you should initialize and register your addin with the IDE. After the IDE loads, your new menu item will appear in the Help menu.

Hello World Screenshot

IDE Services

The IDERegister function above shows a common pattern you should become comfortable with:

IOTAWizardService wizServ = (IOTAWizardService)
BorlandIDE.GetService(typeof(IOTAWizardService));

Many of the IDE-provided interfaces can be obtained by calling GetService on the BorlandIDE class. If the requested interface is present, it will be will be returned. Note that usage of the as operator to typecast BorlandIDE to a specific interface (which was common in previous IDEs) is no longer supported, so you must use GetService. Some of the interfaces that can be obtained using BorlandIDE.GetService are:

Interface New for .NET? Interface Usage
IOTAAboutBoxService Yes Add product information to the IDE about box (detailed below)
IOTAActionService No Open, close, save, and reload files and projects
IOTAAddInService No Load new addin assemblies into the IDE
IOTAAddReferenceDialog Yes Shows the Add Reference dialog for the current project
IOTAAssemblySearchPathService Yes List, add, and remove items from the assembly search paths
IOTAAssemblyUnloadedService Yes Determine when assemblies are unloaded from the IDE
IOTABitmapService Yes Load a bitmap from a Windows resource inside an executable file
IOTAComponentInstallService Yes List, add, and remove components from the toolbox
IOTADotNetObjectInspectorService Yes Allows "selecting" arbitrary .NET objects and showing their properties in the IDE object inspector.
IOTAGalleryCategoryManager Yes List, add, and remove gallery categories. Gallery categories are the tree nodes in the File, New, Other (Repository) dialog.
IOTAIdleNotifier Yes Receive notification when the IDE is idle
IOTAMainMenuService Yes Iterate through, add, remove, and execute main menu items
IOTAMessageService No Add or remove messages and message groups to the message view
IOTAModuleServices No Get references to the current project group, project, file group (module). Open, close, and create new modules.
IOTAPersistenceManager Yes Provides in-memory access to the XML document that stores the default and user-specific IDE settings (ApplicationSettings.xml).
IOTAService No General information such as the location of the settings in the registry and on disk, the IDE installation directory, and the environment options interface. Also provides notifications for files being opened/closed, installation/removal of packages, and compilation events.
IOTASplashScreenService Yes Add product information to the IDE splash screen
IOTAWizardService No Add or remove wizards from the IDE. Wizards can be generic addins, menu wizards, repository items, etc.

From Notifiers to .NET Events

Previous Borland IDEs used notifier interfaces to provide callbacks when certain events occurred. The new OTA uses .NET events for this purpose. This means that instead of registering a notifier that has to be able to receive all 7 events on the old IOTAIDENotifier interface, you can now register to receive a single event, such as the FileNotification callback. For example, this code snippet shows how to receive FileNotification callbacks when files are opened, closed, etc.:

public static void IDERegister()
{
IOTAService ideService = (IOTAService)
BorlandIDE.GetService(typeof(IOTAService));
ideService.FileNotification +=
new FileNotificationHandler(FileEvent);
}
private static void FileEvent(object sender, FileNotificationEventArgs args)
{
LogMessage(String.Format("{0} notification for: {1}",
args.NotifyCode, args.FileName));
}

Adding a Main Menu Item

Unlike IOTAMenuWizard, which always places the new menu item in the Help menu, the IOTAMainMenuService interface allows addins to place new menu items anywhere in the main menu and allows customizing the menu item bitmap, shortcut, etc. The sample code below shows how to do this:

protected static void AddMainMenuItem(
{
    ((Bitmap)form.MenuItemBitmap.Image).MakeTransparent();

    IOTAMainMenuService menuServ = OTAUtils.MainMenuService;
    menuItem = menuServ.AddMenuItem(OTAUtils.IDEViewDebugItemName,
        OTAMenuItemLocation.otamlBefore, MenuItemName, "OTA Intro Form",
        (Bitmap)form.MenuItemBitmap.Image).GetHbitmap());

    menuItem.Executed += new EventHandler(MenuItemExecuted);

    int shortcut = Convert.ToInt32(Keys.Z) | OTAUtils.Shift |
        OTAUtils.Control | OTAUtils.Alt;
    menuItem.Shortcut = shortcut;
    OTAUtils.StartUpdatingMenuShortcut(menuItem, shortcut);
}

Because the OTA interfaces expect a Windows HBITMAP value to set the menu item bitmap, we must cast the Image to a Bitmap and pass in the result of GetHbitmap(). The call to Bitmap.MakeTransparent takes the corner pixel of the bitmap and treats it as the transparent color for the image. In the sample code, the menu item image is loaded from a PictureBox component on the form, but you could also use IOTABitmapService to load it from a resource. The first parameter to AddMenuItem is a reference menu item name. The new menu item is placed relative to the reference item. The names of all of the existing IDE menu items can be obtained using IOTAMainMenuService.GetFirstMenuItem and the various methods to iterate over the menu items, such as IOTAMenuItem.ChildMenuItem.

Each IOTAMenuItem can have a shortcut, but the value to pass in to set the shortcut isn't obvious. At first glance, it might seem logical that the type would be a .NET Shortcut enumeration, but that type does not support the majority of the shortcuts that the IDE menu items support. Instead, the shortcut is treated as a word where the low byte is a Windows virtual key code value and the high byte encodes the state of the three shift keys in the high 3 bits. You can use the or operator on a System.Windows.Forms.Keys key code combined with any of the Shift, Control, and Alt modifiers defined in OTAUtils to generate an appropriate shortcut value. Since the .NET OTA does not yet expose the keybinding functionality from previous IDEs, your menu item shortcuts might occasionally disappear when packages are loaded or unloaded and the IDE reinitializes the main menu. To work around this, you can use the two utility methods provided in OTAUtils.cs called StartUpdatingMenuShortcut and StopUpdatingMenuShortcut. This workaround automatically resets the menu item's shortcut when packages load or unload. The result of the menu item code is shown here:

Menu Item Screenshot

If one of your menu event handlers throws an uncaught exception, the only indication the user will see is the somewhat unhelpful message "Exception has been thrown by the target of an invocation". This is because native code deep inside the IDE is making calls into your .NET object to signal the menu item being selected and the .NET event invocation framework is catching .NET exceptions and re-throwing them with the above message. For this reason, it might be prudent to wrap your menu event handlers in a try/catch block, show the real exception message to the user, and then handle the exception as appropriate.

Project Groups, Projects, Modules, Editors, and Views

New OTA developers are sometimes confused by the terminology used in the OTA to describe files in the IDE. At the top-level is a project group (.bdsgroup) that is a collection of project files, such as .bdsproj files. Within a project is any number of modules. Modules are groups of 1 or more files that are treated as a unit. For example, an ASP.NET module might contain both an .aspx html template and a .cs file for the code behind implementation. Within a module, there can be many IOTAEditors, which generally correspond to a single physical file on disk. These may be IOTASourceEditors for source text files or some other type of editor. Source editors can in turn have one or more IOTAEditViews into their contents. Though the ability to create multiple edit views for the same source editor is not present in C#Builder 1.0, the interfaces to iterate over edit views are still present in the OTA, since the feature will likely reappear in a future C#Builder release. The sample code for this article contains an example of how to use IOTAModuleServices to obtain the current project group, project, module, editor, and editor source. Install the OTAIntro.dll assembly as described above and then choose the View, OTA Intro Form menu item. Clicking the Refresh button will gather the current module data as shown here:

Modules Tab

Components

C#Builder replaces the component palette with a multi-purpose Tool Palette. The OTA provides a way to determine the categories and components present in the Tool Palette to add to form designers. The Components tab of the sample code form shows how to list the component categories, list the components in the category, and dynamically create components from your OTA addins. Here you can see the component list and a button having just been created:

Component Creation

Because the C#Builder IDE uses the .NET designer built into the .NET 1.1 SDK, all of the designer functionality documented for IDesignerHost and the related interfaces is available to you through the OTA. You can get an IDesignerHost interface using IOTADotNetModule.DesignerHost. IDesignerHost allows creating transactions of atomic/undoable modifications and implements many of the interfaces in System.ComponentModel.Design, including IComponentChangeService, ITypeResolutionService, ISelectionService, INameCreationService, IUIService, IMenuCommandService, etc. The sample code shows how to use IDesignerHost to get the list of selected components and how to create a component using IToolboxUser.ToolPicked.

public static void CreateToolboxItemOnDesigner(ToolboxItem item,
    IDesignerHost designer)
{
ISelectionService selection = (ISelectionService)
designer.GetService(typeof(ISelectionService));
IComponent selComp = (IComponent)selection.PrimarySelection;
IDesigner des = null;
if (selComp is IToolboxUser)
des = designer.GetDesigner(selComp);
else
des = designer.GetDesigner(designer.RootComponent);
if (des is IToolboxUser)
{
((IToolboxUser)des).ToolPicked(item);
}
}

About Box Customization

The new IDE was designed to be pluggable both for third-party addins and new compilers, such as the upcoming Delphi 8. There are increased customization opportunities available for addin developers such as placing their product details in the splash screen and about box. The sample code below shows how to add a logo and related informational text to the IDE's about box. Similar to the procedure for menu items, note that the Bitmap object in the image is made transparent and passed in as a Windows HBITMAP value. Note that you should save the value returned from AddPluginInfo and use it later to remove your plugin information using RemovePluginInfo when your addin unloads.

private void AddAbout_Click(object sender, System.EventArgs e)
{
    if (AboutPluginIndex > -1)
        throw new ApplicationException("You can't add the plugin twice");
    IOTAAboutBoxService aboutServ = OTAUtils.AboutBoxService;
    ((Bitmap)AboutImage.Image).MakeTransparent();
    AboutPluginIndex = aboutServ.AddPluginInfo(AboutTitle.Text,
        AboutDescription.Text, ((Bitmap)AboutImage.Image).GetHbitmap(),
        AboutUnregistered.Checked, AboutLicenseStatus.Text,
        AboutSkuName.Text);
}

After running the above code, the about box looks something like this:

About Box Customization

Note that similar customization of the splash screen is possible using IOTASplashScreenService.

The sample code also shows how to execute arbitrary IDE menu items using the provided OTAUtils.ExecuteMainMenuItemByName method. When you click the "Show About Box" button on the About Box tab, the IDE menu item named "HelpAboutItem" is located and executed. A similar method will work for any menu item in the IDE once you find its name using the relevant IOTAMainMenuService and IOTAMenuItem interfaces.

The Message View

The sample form also has a Message View tab. The code invoked there shows how to clear the message view, add messages and message groups, and add parent/child nested message types that open files when clicked, etc. The functionality is illustrated below:

Message View

General OTA Utilities

The sample code also includes many static utility functions in the OTAUtils class in OTAUtils.cs that make OTA development easier. You can, for example, safely and easily retrieve many of the IDE services, get the current project interface, execute arbitrary IDE menu items, etc. Feel free to use this unit in your own projects and add new methods and send them to me for inclusion in the utility package.

More New Features

Other than the new interfaces described above, seasoned OTA developers will also find that the following interfaces are new to the .NET OTA:

  • IOTACodeDomProvider - IOTAModule.GetService can return this interface when it is supported. It allows access to the CodeDom for a file open in the IDE. The CodeDom is an instance of the System.CodeDom.CodeObject class and provides a structured view of the file including the declared types, methods, method implementations, etc. This is an enormously useful addition to the IDE and should make many OTA developers happy.
  • IOTAElideActions - IOTAEditView implements this interface to allow folding and unfolding code sections in the editor. For this specific interface, you can use the as operator to cast IOTAEditView to IOTAElideActions.
  • IOTADotNetModule - Obtained using IOTAModule.GetService. Allows access to .NET specific module properties and helper functions such as showing a specific part of the code in the module, showing the .NET designer, setting the code generator options that map from the CodeDom to the actual source, and getting information about and modifying a designer using the IDesignerHost interface. The component creation example described above uses this interface.
  • IOTADotNetProject - Obtained using IOTAProject.GetService. Allows access to .NET-specific properties of a project such as the external references using IOTAReferences and adding controls to the .licx (component license) file using IOTAProjectLicenseProvider.
  • IOTAFileReader and IOTAFileWriter - These replace IOTAEditReader and IOTAEditWriter in the old OTA and allow directly reading and writing the bytes in a source file. You can obtain a file reader or writer using an IOTASourceEditor's CreateReader or CreateWriter methods, or via the IOTAVirtualFile interface. These two interfaces operate almost identically to the older ones they replace.
  • IOTAReferences - Allows you to list, add, and remove assembly, COM server, and other references for a project. This interface is obtained from IOTADotNetProject.References.

Another bonus (at least until C#Builder 2!) of the .NET OTA is that the complex interface hierarchies that had developed due to successive IDE editions adding more features to interfaces were flattened into a single level. For example, there is no longer an IOTAServices interface inheriting from IOTAServices60 which inherits from IOTAServices50. There is now a single IOTAService interface implementing all of the old IOTAServices methods and any additions for .NET.

Debugging Addins

Debugging OTA addins is not as easy as it could be, because C#Builder 1.0 does not directly support debugging into a .NET assembly when the assembly was loaded by a native Windows executable like the C#Builder IDE (trying to do so results in the exception "Unable to scan program's header"). Workarounds include debugging by attaching to an already running C#Builder instance using the Run, Attach to Process menu item (see the Readme.txt for details), debugging using a log file, or messages sent using IOTAMessageService, etc. To debug the initialization code for your addin, you can place a MessageBox.Show call as the first line in your IDERegister procedure, and attach to the new IDE process while the IDE is waiting for the MessageBox dialog to be confirmed.

OTA Features That Are Not Currently Implemented

There are also several things supported by the older unmanaged OTA or that would be helpful in .NET but are not currently supported by the .NET OTA including:

  • There is no support for creating dockable IDE windows using the internal DockForm.TDockableForm class. Normal modal or non-modal WinForms can be created and shown as expected.
  • All of the INTA-type services such as INTACustomDrawMessage, INTAFormEditor, INTAServices, INTAComponent, etc. are not present, since you can not access native VCL objects using pure .NET code.
  • IOTAFormEditor and IOTAComponent are not present, but the .NET framework defined IDesignerHost property of an IOTADotNetModule provides much of the same functionality to list, add, and delete designer components (see above for more details).
  • IOTAKeyboardServices is not supported, but you can do some basic keystroke handling by adding a menu item with a shortcut.
  • The debugger interfaces (IOTADebuggerServices, IOTAProcess, IOTAThread, IOTABreakpoint, etc.) are not implemented
  • Some other rarely used interfaces are not present such as IOTACodeInsightServices, IOTAFileSystem, IOTASpeedSetting, IOTAToDoServices, etc.
  • Some of the interfaces do not properly return/handle multi-byte UNICODE characters in the code editor.

The sample code for this article is available in CodeCentral as submission ID #20287.

Erik Berry is an independent software developer and consultant working from Saint Louis, MO and with Oasis Digital Solutions. He is the project leader for the GExperts IDE toolkit for Delphi and C++Builder and helped design and implement the Open Tools API in C#Builder 1.0. He also maintains an Open Tools API FAQ.


Server Response from: ETNASC02