Dynamic Compilation, Reflection, & Customizable Apps

David and Eric show how to develop apps that offer user-created buttons and menu items that launch custom features.


October 01, 2004
URL:http://www.drdobbs.com/windows/dynamic-compilation-reflection-customiz/184405851

October, 2004: Dynamic Compilation, Reflection, & Customizable Apps

Giving users what they want, when they want

Between them, David and Eric have developed everything from Windows apps for travel agents to electronic publishing software. They can be contacted at scofielddaveqwest.net and ericterrellcomcast.net, respectively.


To Learn More


You can satisfy some of your users all of the time, and all of your users some of the time. But how can you satisfy all of your users all of the time? A given user often has unique needs that cannot be accommodated by a single application developed for a large user community. And it's usually not feasible to create custom versions of your product for individual users. But how can you accommodate your users' unique needs in your application? Well, the answer is by letting them create the custom features that they require. In this article, we show how to develop applications that offer user-created toolbar buttons and menu items that launch custom features written in dynamically compiled C# or Visual Basic.NET. In the process, we present a sample application called "Sample App" (available electronically; see "Resource Center," page 5) that demonstrates all of the techniques covered in this article.

The Sample Application

The sample application is a .NET Windows Forms program written in C#. It requires the .NET 1.1 Framework and Windows 2000/XP or later. Build it in Visual Studio .NET 2003 (by opening SampleApp\SampleApp.sln) or install it by running the setup.exe program in the Setup folder. In any case, launch Sample App and try it out.

Sample App's custom toolbar buttons and menu items are generated from custom code (Figure 1). Press Edit Code to see the custom code (Figure 2). When you make changes to the custom code and press OK, the code is recompiled and the custom toolbar buttons and menu items are regenerated.

Press the Edit Code button and add Example 1 as the first class inside of the NS namespace declaration. Now press the OK button. The custom code is compiled and the toolbar redrawn with a new "Hello World" button. The HelloToolbarButton constructor specifies the button's caption and tool-tip. The GetImage method specifies the button's graphic. DoAction contains the code that is called when the user presses the toolbar button.

Select Options/Settings to configure Sample App. The Assemblies tab specifies the assemblies that your custom code references. For example, since the HelloToolbarButton class uses the Windows Forms MessageBox class, the Assemblies tab must include the system.windows.forms.dll assembly. Use the Misc. tab to specify compilation options (debugging/tracing, optimization, warning level, and so on).

Dynamic Compilation

The sample application's InMemoryCompiler class takes care of all the details of dynamic C# code compilation. It compiles C# code to an in-memory assembly (DLL) and checks for compiler errors; see Listing One.

Because compilation takes time, it's performed only if the source code has changed since the previous compilation. The first step is to instantiate a CSharpCodeProvider object. Next, an ICodeCompiler object is created by calling CreateCompiler(). Then a CompilerParameter object is created by calling CreateCompilerParameters. This CompilerParameter object specifies that the source code will be compiled into an in-memory assembly. It also includes the settings specified in the Options/Settings Assemblies and Misc. tabs.

Calling CompileAssemblyFromSource takes the source code and CompilerParameter object and creates an in-memory assembly, provided there are no errors. If there are no errors, Compile returns a value of true. Otherwise, it returns false and the compilerError parameter holds the first error.

To modify Sample App to compile VB.NET code, make the following changes:

  1. In InMemoryCompiler.Compile, change the CSharpCodeProvider to a VBCodeProvider.
  2. In EditCodeForm.UpdateClassesComboBox, change the regular expression pattern to: String pattern = "[\s] +Class[\s]+" + type.Name;.
  3. Convert the custom code to VB.NET.

Creating Custom GUIs

The custom toolbar buttons and menu items are dynamically generated by inspecting the compiled assembly and looking for classes that implement specific interfaces.

A custom toolbar button is created for each class that implements the ICustomToolBarButton interface. The button has the appearance and behavior specified by that class. Listing Two presents the ICustomToolBarButton interface and CustomToolBarButton class.

The ICustomMenuItem interface declares the properties and methods that are implemented by each custom menu object (Listing Three). This interface provides menu text and shortcuts as well as overrideables for the Select and Click menu methods. One additional interesting feature of the ICustomMenuItem is that it has properties and methods that let the menu item be owner-drawn. Sample App's Textbox Settings menu contains owner-drawn child menu items.

Finding Classes in an Assembly with Reflection

To create custom toolbar buttons and menu items, you need to search the in-memory assembly for classes that implement the ICustomToolBarButton and ICustomMenuItem interfaces. The .NET Reflection API is perfect for this purpose. There are two ways to get type information from an assembly—Assembly.GetTypes and Assembly.GetExportedTypes. Since we're only interested in public classes, we use GetExportedTypes. This method returns an array containing all of the assembly's public types. We then inspect each type to see if it implements the desired interface. This is done by calling Type.GetInterface.

Sample App's UpdateProgrammableToolbarButtons method populates the toolbar with custom buttons (Listing Four). It first removes all existing toolbar buttons. Then it calls inMemoryCompiler.GetTypes, which in turn calls GetExportedTypes to construct an array of all the assembly's public classes. It creates a custom toolbar button for each class that implements the ICustomToolBarButton interface. Then it needs to instantiate an ICustomToolBarButton object and call that object's SaveAPI and GetImage methods, and access the object's Text and Style properties. A handy class called Activator creates an instance of this class by using the CreateInstance method.

Unlike a toolbar, which is just a linear list of toolbar buttons, menus can contain cascading menu items. For example, Sample App's Custom Menu Items menu contains a Textbox Settings cascading menu, which in turn contains several menu items (Red, Blue, and so on). Because menus can contain nested submenus, Sample App uses nested classes to represent custom menu items. For example, the TextBoxSettingsMenuItem class is not a nested class, but it contains nested classes for each menu item nested below the Textbox Settings menu item (RedMenuItem, BlueMenuItem, and the like).

Sample App needs to determine if a class is nested to generate custom menus. Consider a nonnested ParentClass that contains a nested ChildClass. The Type.FullName property of ParentClass is simply "ParentClass." The Type.FullName property of ChildClass is "ParentClass+ChildClass." Consequently, Sample App knows that a class is nested if its Type.FullName property contains at least one "+" character.

Listing Five shows how Sample App creates its custom menu items. The UpdateProgrammableMenus method first clears the custom main menu. Then it creates an ArrayList of ICustomMenuItem objects that are not nested. Finally, the array is passed to the method GetChildMenus, which creates each menu item and recursively creates any subordinate menu items.

Developing Custom Code

When developing custom code, there needs to be a two-way calling capability. The application needs to be able to call custom code, and the custom code needs to be able to call methods and properties in the application. The IAPI interface contains all of the methods and properties in Sample App that custom code can access. When each custom toolbar button and menu item object is created, the object's SaveAPI method is called. SaveAPI saves a reference to the IAPI interface in the object. The object's custom code can call the application's API by using its iapi member. For example, a custom toolbar button could launch Sample App's about box by calling iapi.DisplayAboutBox.

The standard .NET debugging APIs are available to custom code. Select Options/Settings and check "Allow debugging features" in the Misc. tab to enable Debug.Assert. Check "Allow tracing features" to enable Trace.WriteLine calls in custom code. The Sample App's trace feature uses a TraceListener to intercept Trace calls and display them in the View/Trace Window dialog box. See the code for TraceUtil (available electronically) for the details. Since custom code can throw exceptions, Sample App has a handler for the Application.ThreadException event. MainForm.Application_ThreadException inspects its exception argument. If the exception was thrown from custom code, it displays the exception information and a stack trace in a RuntimeErrorForm. Otherwise, if the exception was thrown by the application itself, it rethrows the exception and relies on standard exception processing.

Conclusion

If your users are demanding custom features that aren't feasible to include in your product, use the techniques we've discussed to make your application customizable. Dynamic compilation and Reflection make it easy to implement custom features. What are you waiting for?

DDJ



Listing One

// Compile the specified source code to an in-memory assembly.
public bool Compile(String sourceCode, out CompilerError compilerError)
{
  bool result = true;
  compilerError = null;
  // Only compile if source code has changed since previous compilation.
  if (sourceCode != compiledSourceCode)
  {
    CSharpCodeProvider cSharpProvider = new CSharpCodeProvider();
    ICodeCompiler codeCompiler = cSharpProvider.CreateCompiler();

    // Create a CompilerParameters object that specifies assemblies referenced
    //  by the source code and the compiler options chosen by the user.
    CompilerParameters compilerParameters =
      CreateCompilerParameters(
        SerializeConfiguration.Settings.assemblies);
    compiledSourceCode = null;
    try
    {
      // Compile the source code.
      compilerResults =
      codeCompiler.CompileAssemblyFromSource(compilerParameters, sourceCode);
      // Check for errors.
      if (compilerResults.Errors.Count > 0)
      {
        compilerError = compilerResults.Errors[0];
        result        = false;
      }
      else
      {
        // Keep track of the source code that was compiled.
        compiledSourceCode = sourceCode;
      }
    }
    catch (Exception)
    {
      MessageBox.Show("Cannot compile source code", Application.ProductName, 
                      MessageBoxButtons.OK, MessageBoxIcon.Error);
      result = false;
    }
  }
  return result;
}
Back to article


Listing Two
public interface ICustomToolBarButton : IAPIUser
{
 ...
}
public class CustomToolBarButton : ICustomToolBarButton
{
  // Reference to the program's API.
  protected IAPI iapi;
  // The code that creates a CustomToolBarButton will call this method 
  // and pass in an object that implements the IAPI interface.
  public void SaveAPI(IAPI iapi)
  {
    this.iapi = iapi;
  }
  // Button's caption.
  private String text;
  public String Text
  {
    get
    {
      return text;
    }
    set
    {
      text = value;
    }
  }
  // Button's Tool-tip text
  private String toolTipText;
  public String ToolTipText
  {
    get
    {
      return toolTipText;
    }
    set
    {
      toolTipText = value;
    }
  }
  // Button's style
  public virtual ToolBarButtonStyle Style
  {
    get
    {
      return ToolBarButtonStyle.PushButton;
    }
  }
  // Button's graphic, no graphic by default.
  public virtual Image GetImage()
  {
    return null;
  }
  // Do the action to be performed when the button is pressed.
  public virtual void DoAction(object sender, System.EventArgs e)
  {
    MessageBox.Show(iapi.TheMainForm, text);
  }
}
Back to article


Listing Three
public interface ICustomMenuItem : IAPIUser
{
 ...
}
public class CustomMenuItem : ICustomMenuItem
{
  // Reference to the program's API.
  protected IAPI iapi;

  // The code that creates a CustomMenuItem will call this method and 
  // pass in an object that implements the IAPI interface.
  public void SaveAPI(IAPI iapi)
  {
    this.iapi = iapi;
  }
  // Menu's text
  private String text;
  public String Text
  {
    get
    {
      return text;
    }
    set
    {
      text = value;
    }
  }
  // Menu's shortcut, if any.
  private Shortcut menuShortcut = Shortcut.None;
  public Shortcut MenuShortcut
  {
    get
    {
      return menuShortcut;
    }
    set
    {
      menuShortcut = value;
    }
  }
  // Default click method
  public virtual void Click(object sender, System.EventArgs e)
  {
    MessageBox.Show(iapi.TheMainForm, text);
  }

  // Click event handler
  public virtual EventHandler ClickEventHandler
  {
    get
    {
      return new EventHandler(Click);
    }
  }
  // Default code to run when user highlights this custom menu item
  public virtual void Select(object sender, System.EventArgs e)
  {
    iapi.TheStatusBar.Text = text.Replace("&", "");
  }
  // Select event handler
  public virtual EventHandler SelectEventHandler
  {
    get
    {
      return new EventHandler(Select);
    }
  }
 ...
}
Back to article


Listing Four
// Re-load toolbar with custom toolbar buttons
private void UpdateProgrammableToolbarButtons()
{
  // Remove custom toolbar buttons, they will be re-added below.
  customToolbar.Buttons.Clear();
  if (customToolbar.ImageList != null)
  {
    // Need to re-create the toolbar's image list 
    // or button graphics will be incorrect.
    customToolbar.ImageList.Dispose();
    customToolbar.ImageList = null;
  }
  CompilerError compilerError;
  if (!inMemoryCompiler.SyntaxErrorsExist(
         sourceCode.Code, out compilerError))
  {
    Type[] types = inMemoryCompiler.GetTypes(sourceCode.Code, true);
    foreach (Type type in types)
    {
      // Only look at classes that implement ICustomToolbarItem
      if (type.GetInterface("ICustomToolBarButton") != null)
      {
        ICustomToolBarButton item = 
          (ICustomToolBarButton) Activator.CreateInstance(type);
        item.SaveAPI(this);
        ICustomToolBarButton customToolbarButton = item;
        textBox.Text += "Adding custom toolbar button: " + 
                    item.GetType().FullName + Environment.NewLine;
        ToolBarButton button = new
                ToolBarButton(customToolbarButton.Text);
        button.Style = customToolbarButton.Style;
        Image image = customToolbarButton.GetImage();
        if (image != null)
        {
          if (customToolbar.ImageList == null)
          {
            customToolbar.ImageList = new ImageList();
            customToolbar.ImageList.ColorDepth       = 
                            ColorDepth.Depth4Bit;
            customToolbar.ImageList.TransparentColor = 
                            System.Drawing.Color.Green;
          }
          customToolbar.ImageList.Images.Add(image);
          button.ImageIndex = 
            customToolbar.ImageList.Images.Count - 1;
        }
        button.ToolTipText = customToolbarButton.ToolTipText;
        button.Tag         = customToolbarButton;
        customToolbar.Buttons.Add(button);
      }
    }
  }
}
Back to article


Listing Five
// Return true if the specified class is nested inside of another class.
private bool TypeIsNested(Type type)
{
  return type.FullName.IndexOf('+') != -1;
}
// Clear and re-create custom menus.
private void UpdateProgrammableMenus()
{
  customMainMenu.MenuItems.Clear();
  CompilerError compilerError;
  if (!inMemoryCompiler.SyntaxErrorsExist(
         sourceCode.Code, out compilerError))
  {
    Type[] types = inMemoryCompiler.GetTypes(sourceCode.Code, true);
    ArrayList nonNestedMenuItems = new ArrayList();

    foreach (Type type in types)
    {
      if (type.GetInterface("ICustomMenuItem") != null &&
          !TypeIsNested(type))
      {
        nonNestedMenuItems.Add(type);
      }
    }
    GetChildMenus(customMainMenu, 
                 (Type[]) nonNestedMenuItems.ToArray(typeof(Type)));
  }
}
// Create a menu item and add subordinate menu items to it.
private void GetChildMenus(MenuItem ParentMenu, Type[] nestedTypes)
{
  foreach (Type type in nestedTypes)
  {
    // Only look at classes that implement ICustomMenuItem
    if (type.GetInterface("ICustomMenuItem") != null)
    {
      //Got one at the right level
      ICustomMenuItem item = 
        (ICustomMenuItem) Activator.CreateInstance(type);
      item.SaveAPI(this);
      CMI.ICustomMenuItem customMenuItem = item;
      //Get a new menu item and set the properties
      MenuItem newMenuItem = new MenuItem(customMenuItem.Text,
                          customMenuItem.ClickEventHandler);
      newMenuItem.Select += new
               EventHandler(customMenuItem.SelectEventHandler);
      newMenuItem.Shortcut = customMenuItem.MenuShortcut;
      newMenuItem.OwnerDraw = customMenuItem.IsOwnerDrawn;
      newMenuItem.MeasureItem  += new MeasureItemEventHandler(
                      customMenuItem.measureItemEventHandler);
      newMenuItem.DrawItem += new
        DrawItemEventHandler(customMenuItem.drawItemEventHandler);
      //Notification to the user
      textBox.Text += "Adding menu class: " + type.FullName + 
                Environment.NewLine;
      //Get the sub-menus for this item
      GetChildMenus(newMenuItem, 
                    type.GetNestedTypes(BindingFlags.Public));
      //Add this item to the parent
      ParentMenu.MenuItems.Add(newMenuItem);
    }
  }
}
Back to article

October, 2004: Dynamic Compilation, Reflection, & Customizable Apps

public class HelloToolbarButton : CustomToolBarButton
{
  public HelloToolbarButton()
  {
    Text        = "Hello World";
    ToolTipText = "Display Hello World Message";
  }
  public override Image GetImage()
  {
    return new Bitmap("World.bmp");
  }
  public override void DoAction(object sender, System.EventArgs e)
  {
    MessageBox.Show(iapi.TheMainForm, "World", "Hello");
  }
}

Example 1: The first class inside of the NS namespace declaration.

October, 2004: Dynamic Compilation, Reflection, & Customizable Apps

Figure 1: Sample App.

October, 2004: Dynamic Compilation, Reflection, & Customizable Apps

Figure 2: Custom code.

October, 2004: Dynamic Compilation, Reflection, & Customizable Apps

To Learn More

Kevin Burton's .NET Common Language Runtime UNLEASHED (Sam's Publishing, 2002) offers in-depth coverage of dynamic compilation and reflection. SharpDevelop (http://www.icsharpcode.net/OpenSource/SD/ Default.aspx) is a free IDE for C# and VB.NET projects on Microsoft's .NET platform. Because it's open sourced, you can read the source code to find out how to do syntax highlighting, Ctrl+Space code completion, IntelliSense, and so on. But note: Because SharpDevelop is subject to the GPL (General Public License), if you use SharpDevelop code in your application, your application must be open sourced, too.

—D.B.S. and E.B.T.

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.