An Application Program Interface or API is a blessing for any developer when needing to interact with or build on top of any software. APIs provide a common set of methods and instructions publicly available to expand or add functionality to the base software.
In the AEC industry, one of the most known API is the Revit API, that allows us developers to add functionality and custom user interfaces as add-ins or plugins to Revit. But as with any API, every new release can introduce breaking changes with previous versions making a project incompatible. So, how can I maintain a plugin for multiple versions of Revit?
This is a recurring issue when dealing with projects that must support many API versions (i.e., Revit 2016 to 2021). Although this has been discussed previously on other blog posts as in How to maintain Revit Plugins for multiple versions, continued... by Konrad k Sobon or How we automated releases of Speckle for Revit 2019, 2020 and 2021 with different versions of CefSharp by our friends at Speckle, I wanted to share my preferred setup when dealing with projects supporting multiple Revit versions (or just one, either way it works the same!).
Note: After a bit of googling, of course I found a similar example of this approach at The Building Coder blog. I will expand a bit more on it and provide some samples and resources to get you started.
Brief intro to SDK .csproj format
For those unfamiliar with developing projects in Visual Studio and C#, the .csproj is XML formatted file containing information about your project; for example, which files are included, dependencies and assemblies consumed, project GUID, versions and much more.
Back with the release of Visual Studio 2017 and .NET Core, this file went through a refactoring process resulting in what is commonly known as "SDK-style project", which through a common set of assumptions the Microsoft .NET team achieved a leaner, simple format with some new features. Among these features we will focus throughout this piece on the TargetFrameworks that allows us to target multiple platforms with the same code source.
We are going to create a Revit add-in that allows the user to select any floor and return its surface area in square meters. Simple right? Well, there is a catch. Revit stores units internally in Imperial System and we want to use Revit API's UnitUtils.ConverFrominternalUnits() method to get the corresponding measure in Metric System. In Revit 2021, a new method overload using an instance of ForgeTypeId was added, marking the original one using DisplayUnitType enums as obsolete. This translates on the latter being removed in Revit 2022. We want our project to support any Revit versions from 2018 onwards, so we must define a mechanism that allows to call one method overload or the other depending on the version we are running on.
The repository for this sample can be found on the Multiple Revit Version Sample on GitHub. This approach is based on the amazing work done by Yaroslav Zhmayey in his project VS Templates Revit Addin, from which I made a fork a few years back and it's been my go-to approach when starting a new Revit API project.
Defining supporting Revit versions
In our project's .csproj file we can define which versions of Revit our plugin will support. This is done by using the TargetFrameworks directive and mapping each .NET version to a Revit API using conditional statements.
When building the project in Debug or Release mode, it will compile it as many times as target frameworks are specified. Further down on the .csproj file, we can override the default OutputPath directory where the compiled assembly is generated so we can easily identify the targeted Revit version.
The image below, show how files have been generated for each Revit version from 2018 to 2021.
During development we might just want to compile against a single Revit version, either because it is the only one we have installed or to reduce the compilation time when dealing with large, complex projects. For this we can define a Debug One configuration mode and define which .Net version should target.
Referencing Revit SDK
The first step to create a Visual Studio project for a Revit plugin is to reference the Revit SDK assemblies. If you have gone through Revit's My First Revit Plugin tutorial, it details how to add a reference to Revit assemblies from your local Revit installation directory.
While this is completely valid, our current approach uses packages available in the NuGet Repository to conditionally reference Revit SDK assemblies depending on the targeting version. This also allows us to create a plugin without necessarily having Revit installed (although it helps to Debug and make sure it works as expected). This configuration can be found on the csproj as:
In order to let your code "know" which version of Revit you are targeting, we use Compilation Constants, which can be used to define compilation conditional statements allowing certain parts of the code to be compiled or not depending on the condition.
In our csproj, this is defined using the DefineConstants and PreprocessorDefinitions statements where we create a REVIT$(RevitVersion) constant that can be referenced in our code.
In our sample, we want to use the new ForgeTypeId class for versions where it is available (2020 onwards), so we can add another constant that defines which versions don't have it, being when RevitVersion is not 2018, 2019 or 2020 as we are only supporting versions greater or equal to 2018 and this class is first defined in Revit 2020.
Using this constant in our code looks something like the below where we declare an #if USE_FORGETYPEID compilation statement.
Template additional goodies!
As I said, the sample project is based on the amazing Visual Studio Revit Template project which comes with additional features such as:
· After-build tasks to generate and copy the plugin .addin file to the corresponding Revit addins folder
· After-clean tasks to remove any generated .addin file to keep thing clean and tidy.
· Project launchSettings.json configuration that allows to define actions to directly open the corresponding Revit version when debugging.
· For IExternalApplications, helper extension methods to easily create Ribbon Panels and buttons for your created commands.
These are not required features to implement the SDK-style project to support multiple Revit versions, so you can keep using your current setup to build, deploy and share your new plugin, but it makes things a lot easier. So please don't hesitate to have a look at its documentation to get a deeper understanding of how this approach can substantially simplify your next Revit plugin project!