Support multiple versions of Revit in invokebuttons (dll projects, Visual Studio)

Hey guys!

Here at Woods Bagot we have our Wombat plugin that is a mix of script files (script.cs and .py) and a few DLLs that come from VS solutions and then invoked by a few .inovkebutton

To give you a very simple example, let’s say that I need to create a ParameterType object. In Revit 2020 I’d need to use this line of code: ParameterType parameterType = ParameterType.Text; which would although fail in Revit 2023, as I now need to use SpecTypeId.String.Text

How can I deal with API changes between versions of Revit, from my VS solution?
I know I can use conditions in my .csproj (i.e. The Building Coder: Multi-Targeting Revit Versions, CAD Terms, Textures) or I could use shared projects in VS (i.e. https://archi-lab.net/how-to-maintain-revit-plugins-for-multiple-versions-continued/), but I don’t think this would be effective for pyRevit (as I’m not targeting Revit directly), would it?

What would be the best way to do this?

One thing I can think of, is creating 2 projects in my VS solution. Let’s call them Wombat2020 and Wombat2023. And then, reference the Revit 2020 APIs in the Wombat2020 project and the Revit 2023 APIs in the Wombat2023 project. In this way I will be creating 2 distinct DLLs in the lib folder (Wombat2020.dll and Wombat2023.dll) that reference 2 different APIs. Then I could use the revit version property in the bundle.yaml to enable/disable the commands that invoke the 2 libraries accordingly.

Although, this sounds a bit painful, especially if you have to change just one line of code in a command.
Is there a better way?

2 Likes

I prefer to just gather the version info at the start of your code. And from there just “if then” inside your code for two different functions. I think this is still the simplest way even for pyRevit files. Having two dll files to maintain sounds like a pain. (Compile, Test, Rinse, Repeat, Repeat, Repeat) Which is why I’ve moved away from any complied code for our office of 25. Just too much overhead for stuff you want to continue to develop and improve. But this is all probably more a question of style and workflow more than what is the best.

Thanks for your reply @aaronrumple !

Unfortunately in a VS project you cannot load multiple versions of the same API without making a mess :smiley: (or at least I’m not aware of how to do that?) and just decide which version of the API to use depending on the version of Revit.
You’re probably able to do that in a simple script (I mean the .pushbutton scenario), but if you have a more complex codebase, you need to have a C# project.

Thinking out loud here, and I have never looked at how the pyRevit C# side is coded, but I think that looking at it may give you a cue and maybe a way to solve it?

I mean, maybe (not entirely sure actually), but it’s kinda like going through a mechanical engineering degree when you have to change your car’s oil…surely you’ll do a proper job, but would you say it was an overkill? :smiley:

Unless I’m missing something, I’m afraid that you cannot work around the need to package 2 different dlls, that’s the way it is with .NET (and any other compiled programming languages I dare say).
pyRevit Itself has solutions for each supported revit versions.

The two options I see are:

  • move away from pyrevit and package a proper revit .NET add-in, targeting the various revit versions as explained int the posts you linked
  • move away from .NET and rewrite the wombat library in python, using HOST_APP.is_newer_than() to handle the different calls

Hey everyone!

I wanted to give you guys an update on what I ended up doing with this, in case someone will find it useful in the future.

The solution I adopted is quite simple (which is why I like it!) and I’ve tested it for a while now. It’s been behaving very well so far…but you never know!

What I ended up doing is keeping my main libraries as they were (for example Wombat.dll stays as is), but then adding additional version-specific libraries, where you can reference the correct version of the Revit APIs, making sure that Copy Local is set to False, obviously.
You then reference the version-specific library in the main library and you’re able to call all the methods! As easy as that!


.
And the best thing is that you don’t need to rewrite big chunks of code in multiple projects. You just create specific methods, as small as you can, for just what you need to do, and then you call it in the “main logic”.

Hopefully this makes sense! And I hope this helps :heart:

1 Like

@AndreaTas do you have a sample somewhere?

@Jean-Marc unfortunately we haven’t used it in any public repo yet, but it’s easy enough to give you an example here :wink:

I’ll use the exemplar case I was proposing in my first post: creating a new parameter in Revit < 2023 (where you gotta use ParameterType parameterType = ParameterType.Text) VS Revit >= 2023 where you use SpecTypeId.String.Text

I have a Wombat2023 project, that loads the Revit 2023 APIs, where I have a method

public static ExternalDefinitionCreationOptions ReturnCreationOptions(string name,
    TypesOfParameter type)
{
    string parameterName = name;
    ForgeTypeId parameterType = ParameterTypeConverter.ConvertToForgeTypeId(type);

    return new ExternalDefinitionCreationOptions(parameterName, parameterType);
}

And you also have a Wombat2020 project (referencing the Revit 2020 APIs)

public static ExternalDefinitionCreationOptions ReturnCreationOptions(string name,
    TypesOfParameter type)
{
    string parameterName = name;
    ParameterType parameterType = ParameterTypeConverter.ConvertToRevitParameterType(type);

    return new ExternalDefinitionCreationOptions(parameterName, parameterType);
}

As you can see both methods return a ExternalDefinitionCreationOptions and that can be used by the main project Wombat by specifying which library/project to use:

// Create a new shared parameter
ExternalDefinitionCreationOptions externalDefinitionCreationOptions;
if (int.Parse(_document.Application.VersionNumber) >= 2023)
{
    externalDefinitionCreationOptions =
        ParameterUtilities2024.ReturnCreationOptions(parameterName, type);
}
else
{
    externalDefinitionCreationOptions =
        ParameterUtilities2020.ReturnCreationOptions(parameterName, type);
}

ExternalDefinition def = _document.Application.OpenSharedParameterFile()
        .Groups.Create(groupName).Definitions.Create(externalDefinitionCreationOptions)
    as ExternalDefinition;

And voila’!

4 Likes

@AndreaTas, I have approached the same scenario similarly too, and have put together a sample project for others to use incase someone else faces a similar sitiuation, here:

A few things to note:

  1. you can make use of the dynamic object type to handle the varying API return types where needed within your executing command classes.
  2. I am using the Costura.Fody nuget package to utilise the version-specific APIs as embedded resources within my main compiled library and avoid missing references at runtime invocation of the commands.
  3. If you’re majorly writing your command for newer versions of the API, and need to support small functionality for older versions, then you only need the version-specific library project for the older API and wrap its execution into a conditional syntax that ensures it will only get triggered by Revit version checks.

Here are code samples from the repo:

The Main executing Command class

#region Namespaces
using Autodesk.Revit.ApplicationServices;
using Autodesk.Revit.Attributes;
using Autodesk.Revit.DB;
using Autodesk.Revit.UI;
using pyApi18;
using pyApi21;
#endregion

namespace pyRevitGirihXPlayground
{
    [Transaction(TransactionMode.Manual)]
    public class Command : IExternalCommand
    {
        public Result Execute(ExternalCommandData commandData, ref string message, ElementSet elements)
        {
            UIApplication uiapp = commandData.Application;
            UIDocument uidoc = uiapp.ActiveUIDocument;
            Application app = uiapp.Application;
            Document doc = uidoc.Document;
            int.TryParse(app.VersionNumber, out int version);

            string[] unitsNames;
            dynamic unitsTypes;
            dynamic currUnitType;

            if (version <= 2020)
            //unitsTypes is DisplayUnitType[] / currUnitType is DisplayUnitType
            { unitsNames = Helpers2018.VersionControl.GetUnitChoices(doc, out unitsTypes, out currUnitType); }
            else
            //unitsTypes is ForgeTypeId[] / currUnitType is ForgeTypeId
            { unitsNames = Helpers2021.VersionControl.GetUnitChoices(doc, out unitsTypes, out currUnitType); }

            TaskDialog.Show("Available Units", "Available Unit Options:\n" + string.Join("\n", unitsNames));
            if (version <= 2020)
            {
                TaskDialog.Show("Current Unit", $"Current Document Units:\n{currUnitType}");
                foreach (var unit in unitsTypes) TaskDialog.Show("Unit Option Type", $"{unit.GetType()}:\n{unit}");
            }
            else 
            /*even though the "ForgeTypeId.TypeId" property is only available in 2021+ APIs, 
            * it will still work fine in 2020 and earlier versions as this code will not be executed 
            * for 2020 and earlier versions; You MUST build the below code against 2021+ APIs 
            * to compile it without errors.*/
            {
                TaskDialog.Show("Current Unit", $"Current Document Units:\n{currUnitType.TypeId}");
                foreach (var unit in unitsTypes) TaskDialog.Show("Unit Option Type", $"{unit.GetType()}:\n{unit.TypeId}");
            }

            return Result.Succeeded;
        }
    }
}

the 2018-2020 API supporting methods class library:

#region Namespaces
using Autodesk.Revit.DB;
using Autodesk.Revit.UI;
using System.Linq;
#endregion

namespace pyApi18
{
    public static class Helpers2018
    {
        public static class VersionControl
        {
            public static string[] GetUnitChoices(Document doc, out dynamic UnitOptions, out dynamic CurrentUnit)
            {
                TaskDialog.Show("Dynamic Version Test", "Helpers2018!");
                
                DisplayUnitType currentUnit = doc.GetUnits().GetFormatOptions(UnitType.UT_Length).DisplayUnits;
                DisplayUnitType[] unitOptions = new DisplayUnitType[]
                { DisplayUnitType.DUT_METERS, DisplayUnitType.DUT_CENTIMETERS, DisplayUnitType.DUT_MILLIMETERS, DisplayUnitType.DUT_DECIMAL_FEET };

                string[] UnitsNames = unitOptions.Select(i => i.ToString()).ToArray();
                UnitOptions = unitOptions;
                CurrentUnit = currentUnit;
                return UnitsNames;
            }
        }
    }
}

the 2021-2024 API supporting methods class library:

#region Namespaces
using Autodesk.Revit.DB;
using Autodesk.Revit.UI;
using System.Linq;
#endregion

namespace pyApi21
{
    public static class Helpers2021
    {
        public static class VersionControl
        {
            public static string[] GetUnitChoices(Document doc, out dynamic UnitOptions, out dynamic CurrentUnit)
            {
                TaskDialog.Show("Dynamic Version Test", "Helpers2021!");

                ForgeTypeId currentUnit = doc.GetUnits().GetFormatOptions(SpecTypeId.Length).GetUnitTypeId();
                ForgeTypeId[] unitOptions = new ForgeTypeId[] 
                { UnitTypeId.Meters, UnitTypeId.Centimeters, UnitTypeId.Millimeters, UnitTypeId.Feet };

                string[] UnitsNames = unitOptions.Select(i => i.TypeId).ToArray();
                UnitOptions = unitOptions;
                CurrentUnit = currentUnit;
                return UnitsNames;
            }
        }
    }
}

Also, worth noting, the sample repo has:

  • Boilerplate for the invoke commands extension library folders and .invokebutton setup with a root-level lib folder

  • The VisualStudio project automatically copies the compiled solution to the extension’s lib folder with post-build events

  • The VisualStudio project conditionally switches references to the Revit API/UI from 2018-2024 based on the build configuration options (debug_2018, debug_2019, …etc) for ease of API reference testing
    and compilation against the version of choice.

@AndreaTas, @Jean-Marc, and all, please feel free to improve and reshare with a PR.

5 Likes

Fantastic @ali.tehami. This will greatly help others.

1 Like