I got a note a couple weeks back from Andrew MacNeill that he had built a quick and dirty Html Help Workshop importer for Help Builder. He casually pointed out, that he was looking for the ‘add-in manager’ and well, there wasn’t one. <g>

 

So, I thought why not, he’s right. It sure would be nice if there was a standard way to create an add-in for Help Builder. Over the years people have built all sorts of custom importers and custom renderers for Help Builder, but very few ever got shared in any way. So an Add-in architecture might get more people to muck around with what’s available for extending Help Builder.

 

So, I created an Add-in interface that works with FoxPro (since Help Builder is a FoxPro app), but it also supports .NET and COM based add-ins. Since these days Help Builder sells mostly to .NET developers, having .NET support for add-ins is fairly important.

 

The add-in architecture is pretty simple actually. It consists of an Add-in manager that allows specifying of an add-in file that is the executable and a class and method that gets called in this add-in. The Add-in Manager demonstrates this functionality best:

 

 

Once installed there’s also an Add-ins… menu option that gives you direct access to the add-in outside of the manager.

 

All the add-ins types work the same way behind the scenes regardless of language used. The add-in developer basically creates a class with a target method. The target method must accept a single parameter that  Help Builder passes to the method, which is a reference to the Help Builder IDE. This instance is a top level instance and it provides access to the Help Builder UI, the active project and the active topic.

 

 

************************************************************************

* AddinManager :: ActivateAddin

****************************************

***  Function: Activates an add-in based on it’s Add-in PK

***      Pass:

***    Return:

************************************************************************

FUNCTION ActivateAddin(lcPk)

LOCAL llError, loAddin

 

loAddIn = this.GetAddin(lcPk)

IF ISNULL(loAddin)

   RETURN .F.

ENDIF

 

IF loAddin.Type # "COM" AND !FILE(loAddin.Filename)

   this.SetError("Addin file does not exist: " + loAddin.Filename)

   RETURN .F.

ENDIF

 

DO CASE

   CASE loAddin.Type = "FOX"

      RETURN this.ActivateFoxAddin(loAddin)

   CASE loAddin.Type = ".NET"

      RETURN this.ActivateNetAddin(loAddin)

   CASE loAddin.Type = "COM"

      RETURN THIS.ActivateCOMAddin(loAddin)

   CASE loAddin.Type = "EXE"     

ENDCASE

 

RETURN .T.

ENDFUNC

*  AddinManager :: ActivateAddin

 

For FoxPro developers creating an add-in is as simple as creating a PRG file, creating a class and method that is called. Help Builder can compile the class on the fly and execute it. FoxPro Exes are also supported – in that case the EXE’s mainline is called first to load all classes methods or whatever is needed to run the Add-in. Then it instantiates the class and calls it. The process is actually fairly simple and demonstrates dynamic PRG execution in the case of PRG files.

 

************************************************************************

* AddinManager :: ActivateFoxAddin

****************************************

***  Function: Activates a VFP based add-in. Handles both PRG and EXEs

***    Assume:

***      Pass:

***    Return:

************************************************************************

PROTECTED FUNCTION ActivateFoxAddin(loAddin)

LOCAL loException, llError

 

llError = .f.

 

lcFileName = LOWER(TRIM(loAddin.FileName))

DO CASE

  CASE JUSTEXT(lcFileName) = "prg"

      llNeedToCompile = .T.

      TRY

         *** This catches both fxp file not existing and being out of date

         *** Slightly more efficient than checking file, and the dates

         llNeedToCompile = FDATE(lcFilename,1) > FDATE(FORCEEXT(lcFileName,"fxp"),1)

      CATCH

         llNeedToCompile = .t.

      ENDTRY

 

      IF llNeedToCompile

         TRY

            COMPILE (lcFileName)

         CATCH TO loException

            llError = .t.

         ENDTRY

         IF llError

            this.SetError("Error compiling PRG file:" + CRLF +;

                          loException.Message)

            RETURN .f.

         ENDIF

      ENDIF

     

      TRY

         loObject = NEWOBJECT(TRIM(loAddin.ObjName),lcFileName)

         lcAddinMethod = "loObject." + TRIM(loAddIn.ObjMethod) + "(goHelp)"

         llResult = EVALUATE( lcAddinMethod )

      CATCH TO loException

         llError = .t.

      ENDTRY

      IF llError

         THIS.SetError("Error executing addin method: " + lcAddinMethod + CRLF  +;

                      loException.Message)

         RETURN .F.                     

      ENDIF

     

      RETURN .T.

 

  CASE JUSTEXT(lcFileName) = "exe" OR JUSTEXT(lcFilename) =  "app"

      LOCAL lcAddinMethod

      lcAddinMethod = ""

      TRY

         *** Load the libraries

         DO (lcFileName)

        

         loObject = NEWOBJECT(TRIM(loAddin.ObjName),lcFileName)

         lcAddinMethod = "loObject." + TRIM(loAddIn.ObjMethod) + "(goHelp)"

         llResult = EVALUATE( lcAddinMethod )

      CATCH TO loException

         llError = .t.

      ENDTRY

      IF llError

         THIS.SetError("Error executing addin method: " + lcAddinMethod + CRLF  +;

                      loException.Message)

         RETURN .F.                     

      ENDIF

     

      RETURN .T.

ENDCASE

 

THIS.SetError("Unsupported file format for a FoxPro addin")

RETURN .F.

ENDFUNC

*  AddinManager :: ActivateFoxAddin

 

 

Ok, the FoxPro stuff was really trivial, since HB is after all a Fox application so dynamically invoking other Fox code is pretty straight forward.

Onward to .NET

Doing the same for .NET was a fun exercise. The basics of the process are pretty easy to accomplish through COM interop. So let’s look at the basics required. The idea is that we need to execute code dynamically based on an assembly, and .NET type and method that the user provides in the Add-in manager.

 

To do this I need to use an intermediate  .NET Interop Assembly that provides the ‘dynamic Invokation’ mechanism. Help Builder already has such an assembly – wwReflection.dll - which performs all the .NET type parsing for importing .NET types. So I ended up adding another object and method that handles the Add-in Execution. Here’s the Fox code that launches the process:

 

************************************************************************

* AddinManager :: ActivateNetAddin

****************************************

***  Function: activates a .NET addin.

***    Assume: requires wwReflection.dll in the current path!

***            and it must be registered.

***      Pass:

***    Return:

************************************************************************

FUNCTION ActivateNetAddin(loAddin)

 

lcFileName = LOWER(TRIM(loAddin.FileName))

 

*** Make sure COM object exists

IF !ISCOMOBJECT("wwReflection.AddinFactory")

   lcError = ""

   DotNetTypeParserCheck(lcError,.T.) && Force Registration

 

   IF !ISCOMOBJECT("wwReflection.AddinFactory")

      THIS.SetError("Unable to register Interop Assembly." + CHR(13) + CHR(10)+;

                    lcError)

      RETURN .F.

   ENDIF

ENDIF

 

LOCAL aiFactory as wwReflection.AddinFactory

 

LOCAL ai as wwReflection.AddinExecution

aiFactory = CREATEOBJECT("wwReflection.AddinFactory")

ai = aiFactory.CreateAddin()

Result = ai.Execute(TRIM(loAddin.Filename),TRIM(loAddin.ObjName),;

                    TRIM(loAddin.ObjMethod),goHelp)

                   

IF !Result

   this.SetError(ai.ErrorMessage)

   aiFactory.UnloadAddin()

   RETURN .f.

ENDIF

  

aiFactory.UnloadAddin()

                      

RETURN .t.                   

ENDFUNC

*  AddinManager :: ActivateNetAddin

 

The first thing that needs to happen is to make sure that the COM Interop assembly is registered. For more details on this check out my COM interop article that talks about how to do this in detail. In short, if the object is not registered it tries to do this from within the application, by finding RegAsm and executing it against the assembly to register it.

 

Once you know the COM Interop assembly is in place we can set out to actually create the Add-in. You notice that I’m creating a factory object here that acts as a proxy to the .NET object that you specified. I’ll come back to this in a minute. For now let’s look at the actual dynamic invocation code:

 

/// <summary>

/// This class manages executing a .NET method in a .NET assembly

/// </summary>

[ClassInterface(ClassInterfaceType.AutoDual)]

[ProgId("wwReflection.AddinExecution")]

public class AddinExecution : MarshalByRefObject

{

      public string ErrorMessage = "";

      public bool Error = false;

 

      /// <summary>

      /// The Actual worker method that dynamically invokes the assembly, type and method.

      /// </summary>

      public bool Execute(string AssemblyFile, string Typename,

                          string Method, object Parameter)

      {

            Assembly ass = null;

            try

            {

                  ass = Assembly.LoadFrom(AssemblyFile);

            }

            catch(Exception ex)

            {

                  this.Error = true;

                  this.ErrorMessage = ex.Message;

                  return false;

            }

           

            Type TypeRef = null;

            try

            {

                  TypeRef     = ass.GetType(Typename,true,true);

            }

            catch(Exception ex)

            {

                  this.ErrorMessage = ex.Message;

                  return false;

            }

 

           

            try

            {

                  object Instance = Activator.CreateInstance(TypeRef);

           

                  // *** Convert object into strongly typed object

                  wwHelpForm Form = new wwHelpForm(Parameter);

 

                  bool Result = (bool) Instance.GetType().InvokeMember(
Method,BindingFlags.InvokeMethod | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,null,Instance,new object[1] { Form } );

 

                  return Result;

            }

            catch(Exception ex)

            {

                  this.Error = true;

                  this.ErrorMessage = ex.Message;

                  return false;

            }

 

            return true;

      }

}

 

This class consists of a single method that makes a pass-through call to the add-in assembly by using reflection to load the assembly. The process of dyamic invocation starts by loading the assembly, then retrieving the type and creating an instance of it. Finally a call is made on the type to call the method specified. Straight forward. From Fox the straight code looks like this:

 

ai = CREATEOBJECT("wwReflection.AddinExecution")

Result = ai.Execute(TRIM(loAddin.Filename),TRIM(loAddin.ObjName),;

                    TRIM(loAddin.ObjMethod),goHelp)

 

So far so good, but there’s a problem with this approach.  If this method is called directly as is, it would cause the Add-In DLL to be loaded into the primary AppDomain and get locked there which probably is not a great idea. Why? Well, if you wanted to debug your add-in or compile it after you have loaded it into Help Builder, you wouldn’t be able to because Help Builder would have it locked. The problem is that once an assembly loads into an AppDomain it can NEVER be unloaded individually. You can only unload the AppDomain which releases the assembly with it.

 

Soooo… since I figure you want to debug the thing, the way to do this right is to load the Add-in a new AppDomain, make the call, then shut the AppDomain down. Notice that the .NET class is marked as MarshalByRefObject, which means this object can be activated via .NET Remoting which is required to execute code in another AppDomain. To do this I’m going to create the Assembly in another AppDomain and do the loading and calling in that AppDomain by using a Factory that creates the AppDomain and passes a back the created instance as Proxy via Remoting. As far as the calling code is concerned it works the same as before, but there’s a call to the factory to get the proxy instance first. The new code with AppDomain loading looks like this:

 

aiFactory = CREATEOBJECT("wwReflection.AddinFactory")

ai = aiFactory.CreateAddin()
ai.ExecuteAddin(…)
aiFactory.UnloadAddin()

 

The AddinFactory class is a small class who’s only task is create a new appdomain and create an instance of our class there and the return the pointer of this class to our calling code. Note that .NET automatically fixes up this remoting Proxy reference to be returned back to Visual FoxPro. When we receive this object in Fox we’re actually using Remoting from FoxPro!

 

[ClassInterface(ClassInterfaceType.AutoDual)]

[ProgId("wwReflection.AddinFactory")]

public class AddinFactory : MarshalByRefObject

{

      /// <summary>

      /// Reference to the AppDomain that the TypeParser

      /// is loaded into.

      /// </summary>

      AppDomain LocalAppDomain = null;

 

      /// <summary>

      /// TypeParser Factory method that loads the TypeParser

      /// object into a new AppDomain so it can be unloaded.

      /// Creates AppDomain and creates type.

      /// </summary>

      /// <returns></returns>

      public object CreateAddin()

      {

            if (!CreateAppDomain(null))