Load Your Own External Command on the Fly

How to reload an external Revit command without having to restart the Revit session has been a subject of discussion many times in the past, e.g. in

reloading an add-in for debug without restart
,
which lists four possibilities:

  • Set up Visual Studio to efficiently load the desired project and the debugee, use the standard cycle hitting F5 to start debugging, and stop debugging when you need to make code changes.
  • Use the

    RevitRubyShell
    or

    RevitPythonShell
    to
    work interactively with the Revit API on the command line.
  • Use the SharpDevelop IDE, temporarily converting the debugee source code to a macro.
  • Use the AddInManager.

Revit 2014 adds some new twists to the scenario by the introduction of the MacroManager API, which enables programmatic listing, creating, removing, editing, debugging, and running of macros, and the option to load an add-in mid-session without restarting Revit.
Unfortunately, there is no option to unload an add-in, once loaded.

The difficulty is that the AppDomain into which the add-in is loaded locks the add-in DLL, preventing any further changes to it.

One could create a new individual AppDomain for each add-in to work around that.

Another option is to not load the add-in from a DLL at all, but stream in the source code using reflection instead.

Here is an example of the latter approach in VB by
David Rock and Yamin Tengono of

BSE – Building Services Engineers
.
David says:

I am running my commands from the ribbon with the following function:


Public Shared Sub InvokeRevitCommand( _
  ByVal strCommandName As String,
  ByVal commandData As Object,
  ByRef message As String,
  ByVal elements As Object,
  ByVal fullPathDllName As String)
 
  ' Load the assembly into a byte array. 
  ' This way it WON'T lock the dll to disk.
 
  Dim assemblyBytes As Byte() = File.ReadAllBytes(
    fullPathDllName)
 
  Dim objAssembly As Assembly = Assembly.Load(
    assemblyBytes)
 
  ' Walk through each type in the assembly.
 
  For Each objType As Type In objAssembly.GetTypes()
    ' Pick up a class.
    If objType.IsClass Then
      If objType.Name.ToLower = strCommandName.ToLower Then
 
        Dim ibaseObject As Object = Activator.CreateInstance(objType)
 
        Dim arguments As Object() = New Object() {
          commandData, message, elements}
 
        Dim result As Object
 
        result = objType.InvokeMember(
          "Execute",
          BindingFlags.[Default] Or BindingFlags.InvokeMethod,
          Nothing, ibaseObject, arguments)
 
        Exit For
      End If
    End If
  Next
End Sub

This way I can re-build the DLL without exiting Revit and my sometimes quite large Revit projects.

I have over 100 Revit commands now and most are loaded via this dynamic method.
This makes it super easy to update them on the fly.

Very many thanks to David and Yamin for developing and sharing this effective solution!


Comments

12 responses to “Load Your Own External Command on the Fly”

  1. Very clever indeed.

  2. I find VB difficult to read ;-) but unless I’m missing something why iterate types?
    I posted a c# simple example I did over morning tea ;-). https://github.com/Redbolts/R14Tests/tree/master/LoadBytes/Redbolts.LoadAgainAndAgain
    Note also assemblies remain in memory so restarting is required if reloading a lot. And as Suzanne Cook notes you’re asking for ‘dll hell’ ;-)
    http://blogs.msdn.com/b/suzcook/archive/2003/05/29/57143.aspx
    AFAIK you can’t use AppDomains with Revit as you can’t marshal across AppDomain boundaries. And performance would probably be a pig;-)
    HTH,
    Guy

  3. Dear Guy,
    Thank you for your additional sample and important hints.
    Yes, of course, this is only intended and recommended for debugging purposes, for people who know what they are doing and understand the risks and consequences.
    Although it actually sounds as if David and Yamin are finding this useful and productive for (probably internal) day-to-day use as well.
    Cheers, Jeremy.

  4. Minor Clarification : Yamin works with us at Arup.
    Guy,
    By iterating over types the method can be quite generic.
    Pass it the typical IExternalCommand.Execute parameters, plus the path of the DLL, and the routine does the rest.
    This assumes a ‘One ExternalCommand per DLL’ structure to your code, but thats a good idea anyway.
    The C# version is at: https://gist.github.com/CapnK/5660888
    We haven’t (yet) run into any major ‘DLL Hell’ problems, but you do need to be careful about loading 3rd party dll references.
    We also hacked TestAssemblyBuilder from Nunit to load DLL’s this way, then you can run NUnit inside Revit.
    Ahhhhh. Code, Compile, Test, code, compile, test. Without restarting Revit
    C# for creating an NUnit test runner here: https://gist.github.com/CapnK/5660936

  5. Dear Steve,
    Thank you very much for the clarification and sorry for the wrong Yamin info.
    Wow, you have an example of NUnit running inside of Revit? Fantastic, brilliant! I have been looking for some simple unit testing framework for Revit add-ins for ages!
    Would you be willing to share a bit more about how that is set up? I think that would be of extreme interest to the community at large. Should be, anyway.
    Thank you!
    Cheers, Jeremy.

  6. Steve,
    Nice example using Nunit. Not familiar with Nunit structure. How do you view the test results? Is this run from the Visual studio Nunit runner or from Revit using a custom runner?
    Guy

  7. Guy,
    It’s using a custom runner inside Revit. We tried using Moq to create mock instances of Revit API objects, but couldn’t even do that outside a running Revit instance. So the choices were to either wrap the entire Revit API, or run Nunit inside Revit.
    Once it’s done, we get the Nunit.Core.TestResult object back, and interrogate it to display in a custom Results window.
    Jeremy,
    Have been meaning to for a while, and struggled to find time to tease it apart from the rest of our code…
    Will see what can be done…

  8. MichaelV Avatar
    MichaelV

    I just started out on writing a Revit plugin and the ideas in this thread have been invaluable, thanks guys!
    For my project I created a proxy class for external commands that only reloads the assembly when the LastWriteTime of the .dll has changed (that is, when I recompile my assembly)
    The write times of the dlls are kept in a static dictionary so this information persists between calls to IExternalCommand.Execute()
    (in fact, I go as far as keeping the actual instance of the IExternalCommand object persistent between calls to Execute…)
    anyways, maybe this method is useful for somebody else so I put the gist of it at
    https://gist.github.com/zmic/6084730

  9. Dear Michael,
    Thank you for your appreciation.
    Congratulations, that sounds like an extremely neat project!
    Thank you very much for sharing!
    Cheers, Jeremy.

  10. OK I don’t program everyday so hopefully you can follow. I have been able to get microsoft studio 2013 express to work by setting the debug variable in another post you had. However, everytime I execute the reflection class it locks the debugger pdb file. The only way I found around this is to continually change the assembly name of the reflected class which then creates another pdb file. I use post build events to copy/rename dll to another location which is the location I use/reflect/load from. How do I unload the reflection pdb or get the debugger to release/unlock the pdb. I have a feeling this is a microsoft studio 2013 bug but hopefully not so I can fix it. This is for revit mep 2014 too.

  11. Hi there.
    Congratulations on getting as far as you have. That sounds pretty good to me, and not like beginner stuff. Unfortunately, I don’t know. I just use the full version of Visual Studio, recompile my add-in and restart Revit every time I change anything.
    On the other hand, have you looked at this discussion?

    Debugging Revit 2014 API with Visual Studio 2013

    Good luck!
    Cheers, Jeremy.

Leave a Reply

Discover more from Autodesk Developer Blog

Subscribe now to keep reading and get access to the full archive.

Continue reading