We make one plugin project with compilation for different versions of Revit / AutoCAD

We make one plugin project with compilation for different versions of Revit / AutoCAD

When developing plug-ins for CAD applications (in my case these are AutoCAD, Revit and Renga) over time, one problem appears - new versions of programs are released, their API changes and new versions of plugins need to be made.

When you have only one plugin or you are still a self-taught beginner in this business, you can simply make a copy of the project, change the necessary places in it and build a new version of the plugin. Accordingly, subsequent changes to the code will entail a multiple increase in labor costs.

As you gain experience and knowledge, you will find several ways to automate this process. I went this way and I want to tell you what I ended up with and how convenient it is.

First, let's look at a method that is obvious and which I have used for a long time

Links to project files

And to make everything simple, clear and understandable, I will describe everything using an abstract example of plugin development.

Let's open Visual Studio (I have the Community 2019 version. And yes - in Russian) and create a new solution. Let's call it MySuperPluginForRevit

We make one plugin project with compilation for different versions of Revit / AutoCAD

We will make a plugin for Revit for versions 2015-2020. Therefore, I will create a new project in the solution (Net Framework Class Library) and call it MySuperPluginForRevit_2015

We make one plugin project with compilation for different versions of Revit / AutoCAD

We need to add references to the Revit API. Of course, we can add links to local files (you will need to install all the necessary SDKs or all versions of Revit), but we will go straight ahead and include the NuGet package. You can find quite a few packages, but I will use my own.

After connecting the package, right-click on the item "references' and select the menu item 'Move packages.config to PackageReference...Β»

We make one plugin project with compilation for different versions of Revit / AutoCAD

If you suddenly start to panic at this place, since there will be no important item in the package properties window "copy locally”, which we definitely need to set to false, then do not panic - go to the project folder, open the file with the .csproj extension in an editor convenient for you (I use Notepad ++) and find an entry about our package there. She looks like this now:

<PackageReference Include="ModPlus.Revit.API.2015">
  <Version>1.0.0</Version>
</PackageReference>

Adding a property to it runtime. It will turn out like this:

<PackageReference Include="ModPlus.Revit.API.2015">
  <Version>1.0.0</Version>
  <ExcludeAssets>runtime</ExcludeAssets>
</PackageReference>

Now, when building a project, the files from the package will not be copied to the output folder.
Let's go ahead - just imagine that our plugin will use something from the Revit API, which has changed over time with the release of new versions. Well, or we just need to change something of our own in the code, depending on the version of Revit for which we are making the plugin. To resolve such differences in code, we will use conditional compilation symbols. Open the project properties, go to the tab "Assembly" and in the field "Conditional compilation notationΒ»Write R2015.

We make one plugin project with compilation for different versions of Revit / AutoCAD

Note that the symbol must be added for both the Debug configuration and the Release configuration.

Well, while we are in the properties window, we immediately go to the tab "application" and in the field "Default namespaceΒ»remove suffix _2015so that our namespace is universal and independent of the assembly name:

We make one plugin project with compilation for different versions of Revit / AutoCAD

In my case, in the final product, plugins of all versions are added to one folder, so my assembly names remain with the suffix of the form _20xx. But you can also remove the suffix from the assembly name if you expect the files to be located in different folders.

Let's go to the code of the file Class1.cs and simulate some code there, taking into account different versions of Revit:

namespace MySuperPluginForRevit
{
    using Autodesk.Revit.Attributes;
    using Autodesk.Revit.DB;
    using Autodesk.Revit.UI;

    [Regeneration(RegenerationOption.Manual)]
    [Transaction(TransactionMode.Manual)]
    public class Class1 : IExternalCommand
    {
        public Result Execute(ExternalCommandData commandData, ref string message, ElementSet elements)
        {
#if R2015
            TaskDialog.Show("ModPlus", "Hello Revit 2015");
#elif R2016
            TaskDialog.Show("ModPlus", "Hello Revit 2016");
#elif R2017
            TaskDialog.Show("ModPlus", "Hello Revit 2017");
#elif R2018
            TaskDialog.Show("ModPlus", "Hello Revit 2018");
#elif R2019
            TaskDialog.Show("ModPlus", "Hello Revit 2019");
#elif R2020
            TaskDialog.Show("ModPlus", "Hello Revit 2020");
#endif
            return Result.Succeeded;
        }
    }
}

I immediately took into account all versions of Revit above the 2015 version (which were at the time of writing the article) and immediately took into account the presence of conditional compilation symbols that I create according to the same template.

Let's move on to the main highlight. We create a new project in our solution, only for the plug-in version under Revit 2016. We repeat all the steps described above, respectively, replacing the number 2015 with the number 2016. But the file Class1.cs removed from the new project.

We make one plugin project with compilation for different versions of Revit / AutoCAD

File with the required code - Class1.cs - we already have and we just need to insert a link to it in a new project. There are two ways to insert links:

  1. Long - click on the project with the right mouse button, select the item "Add"->"Existing element", in the window that opens, find the desired file and instead of the option"AddΒ» select option Β«Add as linkΒ»

We make one plugin project with compilation for different versions of Revit / AutoCAD

  1. Short - right in the solution explorer, select the desired file (or even files. Or even entire folders) and drag it to a new project while holding down the Alt key. When dragging, you will see that when you press the Alt key, the cursor on the mouse will change from a plus sign to an arrow.
    UPD: I made a little confusion in this paragraph - to transfer several files, you should clamp Shift + Alt!

After the procedure, we will have a file in the second project Class1.cs with the corresponding icon (blue arrow):

We make one plugin project with compilation for different versions of Revit / AutoCAD

When editing code in the editor window, you can also choose in the context of which project to display the code, which will allow you to see the code being edited with different conditional compilation symbols:

We make one plugin project with compilation for different versions of Revit / AutoCAD

According to this scheme, we create all other projects (2017-2020). Life hack - if you drag files in the solution explorer not from the base project, but from the project where they are already inserted as a link, then you can not hold down the Alt key!

The described option is quite good until a new version of the plugin is added or until new files are added to the project - all this becomes very dreary. And recently, I suddenly suddenly realized how to sort it all out with one project and we are moving on to the second method.

Configuration Magic

After reading here, you can exclaim, β€œWhat the hell did you describe the first method, if the article is immediately about the second ?!”. And I described everything to make it clearer why we need conditional compilation symbols and in what places our projects differ. And now it becomes clearer to us what kind of project differences we need to implement, leaving only one project.

And to make everything more obvious, we will not create a new project, but we will make changes to our current project created in the first way.

So, first of all, we remove all projects from the solution, except for the main one (containing the files directly). Those. projects for versions 2016-2020. Open the folder with the solution and delete the folders of these projects there.

We have one project left in the solution - MySuperPluginForRevit_2015. Open its properties and:

  1. On the tab β€œapplicationΒ» remove the suffix from the assembly name _2015 (it will become clear why later)
  2. On the tab β€œAssemblyΒ»remove the conditional compilation symbol R2015 from the corresponding field

Note: There is a glitch in the latest version of Visual Studio - conditional compilation symbols are not displayed in the project properties window, although they are available. If you have this glitch, then you need to remove them manually from the .csproj file. However, we still have to work in it, so read on.

Rename the project in the Solution Explorer window by removing the suffix _2015 and then remove the project from the solution. This is necessary to maintain order and feelings of perfectionists! Open the folder of our solution, rename the project folder in the same way and load the project back into the solution.

Open the configuration manager. US configuration Release in principle, it will not be needed, so we delete it. We create new configurations with names that are already familiar to us R2015, R2016,…, R2020. Note that you don't need to copy settings from other configurations and you don't need to create project configurations:

We make one plugin project with compilation for different versions of Revit / AutoCAD

We go to the folder with the project and open the file with the .csproj extension in an editor convenient for you. By the way, you can also open it in Visual Studio - you need to unload the project and then the right item will be in the context menu:

We make one plugin project with compilation for different versions of Revit / AutoCAD

Editing in Visual Studio is even preferable, as the editor both aligns and prompts.

In the file we will see the elements PropertyGroup - at the very top is the general, and then come with the conditions. These elements set the properties of the project when it is built. The first element, which is without conditions, sets general properties, and elements with conditions, respectively, change some properties depending on configurations.

Go to the common (first) element PropertyGroup and look at the property AssemblyName - this is the name of the assembly and we must have it without a suffix _2015. If there is a suffix, then remove it.

Finding an element with a condition

<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">

We do not need it - we delete it.

Element with a condition

<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">

will be needed to work at the development stage and debug the code. You can change its properties to suit your needs - set different output paths, change conditional compilation symbols, etc.

Now we create new elements PropertyGroup for our configurations. In these elements, we just need to set four properties:

  • OutputPath - output folder. I set default value binR20xx
  • DefineConstants are conditional compilation symbols. Should be set to TRACE;R20xx
  • TargetFrameworkVersion – platform version. Different versions of the Revit API require different platforms to be set.
  • AssemblyName – assembly name (i.e. file name). You can write directly the desired assembly name, but for universality, I advise you to write the value $(AssemblyName)_20xx. To do this, we previously removed the suffix from the assembly name

The most important feature of all these elements is that they can be simply copied to other projects without changing at all. Later in the article, I will attach the entire contents of the .csproj file.

Well, we figured out the properties of the project - it's not difficult. But what to do with the included libraries (NuGet packages). If we look further, we will see that the included libraries are specified by the elements ItemGroup. But here's the problem - this element incorrectly processes conditions, as an element PropertyGroup. Perhaps this is even a Visual Studio glitch, but if you set several elements ItemGroup with configuration conditions, and insert different links to NuGet packages inside, then when changing the configuration, all specified packages are connected to the project.

An element comes to our aid Choose, which works according to the logic familiar to us if-then-else.

Using element Choose, set different NuGet packages for different configurations:

All contents of csproj

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0"  ">Debug</Configuration>
    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
    <ProjectGuid>{5AD738D6-4122-4E76-B865-BE7CE0F6B3EB}</ProjectGuid>
    <OutputType>Library</OutputType>
    <AppDesignerFolder>Properties</AppDesignerFolder>
    <RootNamespace>MySuperPluginForRevit</RootNamespace>
    <AssemblyName>MySuperPluginForRevit</AssemblyName>
    <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
    <FileAlignment>512</FileAlignment>
    <Deterministic>true</Deterministic>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
    <DebugSymbols>true</DebugSymbols>
    <DebugType>full</DebugType>
    <Optimize>false</Optimize>
    <OutputPath>binDebug</OutputPath>
    <DefineConstants>DEBUG;R2015</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'R2015|AnyCPU' ">
    <OutputPath>binR2015</OutputPath>
    <DefineConstants>TRACE;R2015</DefineConstants>
    <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
    <AssemblyName>$(AssemblyName)_2015</AssemblyName>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'R2016|AnyCPU' ">
    <OutputPath>binR2016</OutputPath>
    <DefineConstants>TRACE;R2016</DefineConstants>
    <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
    <AssemblyName>$(AssemblyName)_2016</AssemblyName>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'R2017|AnyCPU' ">
    <OutputPath>binR2017</OutputPath>
    <DefineConstants>TRACE;R2017</DefineConstants>
    <TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
    <AssemblyName>$(AssemblyName)_2017</AssemblyName>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'R2018|AnyCPU' ">
    <OutputPath>binR2018</OutputPath>
    <DefineConstants>TRACE;R2018</DefineConstants>
    <TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
    <AssemblyName>$(AssemblyName)_2018</AssemblyName>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'R2019|AnyCPU' ">
    <OutputPath>binR2019</OutputPath>
    <DefineConstants>TRACE;R2019</DefineConstants>
    <TargetFrameworkVersion>v4.7</TargetFrameworkVersion>
    <AssemblyName>$(AssemblyName)_2019</AssemblyName>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'R2020|AnyCPU' ">
    <OutputPath>binR2020</OutputPath>
    <DefineConstants>TRACE;R2020</DefineConstants>
    <TargetFrameworkVersion>v4.7</TargetFrameworkVersion>
    <AssemblyName>$(AssemblyName)_2020</AssemblyName>
  </PropertyGroup>
  <ItemGroup>
    <Reference Include="System" />
    <Reference Include="System.Core" />
    <Reference Include="System.Xml.Linq" />
    <Reference Include="System.Data.DataSetExtensions" />
    <Reference Include="Microsoft.CSharp" />
    <Reference Include="System.Data" />
    <Reference Include="System.Net.Http" />
    <Reference Include="System.Xml" />
  </ItemGroup>
  <ItemGroup>
    <Compile Include="Class1.cs" />
    <Compile Include="PropertiesAssemblyInfo.cs" />
  </ItemGroup>
  <Choose>
    <When Condition=" '$(Configuration)'=='R2015' ">
      <ItemGroup>
        <PackageReference Include="ModPlus.Revit.API.2015">
          <Version>1.0.0</Version>
          <ExcludeAssets>runtime</ExcludeAssets>
        </PackageReference>
      </ItemGroup>
    </When>
    <When Condition=" '$(Configuration)'=='R2016' ">
      <ItemGroup>
        <PackageReference Include="ModPlus.Revit.API.2016">
          <Version>1.0.0</Version>
          <ExcludeAssets>runtime</ExcludeAssets>
        </PackageReference>
      </ItemGroup>
    </When>
    <When Condition=" '$(Configuration)'=='R2017' ">
      <ItemGroup>
        <PackageReference Include="ModPlus.Revit.API.2017">
          <Version>1.0.0</Version>
          <ExcludeAssets>runtime</ExcludeAssets>
        </PackageReference>
      </ItemGroup>
    </When>
    <When Condition=" '$(Configuration)'=='R2018' ">
      <ItemGroup>
        <PackageReference Include="ModPlus.Revit.API.2018">
          <Version>1.0.0</Version>
          <ExcludeAssets>runtime</ExcludeAssets>
        </PackageReference>
      </ItemGroup>
    </When>
    <When Condition=" '$(Configuration)'=='R2019' ">
      <ItemGroup>
        <PackageReference Include="ModPlus.Revit.API.2019">
          <Version>1.0.0</Version>
          <ExcludeAssets>runtime</ExcludeAssets>
        </PackageReference>
      </ItemGroup>
    </When>
    <When Condition=" '$(Configuration)'=='R2020' or '$(Configuration)'=='Debug'">
      <ItemGroup>
        <PackageReference Include="ModPlus.Revit.API.2020">
          <Version>1.0.0</Version>
          <ExcludeAssets>runtime</ExcludeAssets>
        </PackageReference>
      </ItemGroup>
    </When>
  </Choose>
  <Import Project="$(MSBuildToolsPath)Microsoft.CSharp.targets" />
</Project>

Please note that in one of the conditions I specified two configurations via OR. Thus, the required package will be connected during configuration Debug.

And here we are almost perfect. We load the project back, include the configuration we need, call in the context menu of the solution (not the project) the item "Restore all NuGet packages” and see how our packages change.

We make one plugin project with compilation for different versions of Revit / AutoCAD

And at this stage, I came to a dead end - in order to collect all the configurations at once, we could use the batch assembly (menu "Assembly"->"Batch assembly”), but when switching configurations, packages are not automatically restored. And when assembling the project, it also does not happen, although, in theory, it should. I have not found a solution to this problem by standard means. And most likely this is also a Visual Studio bug.

Therefore, for batch assembly, it was decided to use a special automated assembly system Nuke. I didn't really want this as I think it's overkill in terms of plugin development, but at the moment I don't see any other solution. And to the question β€œWhy Nuke?” The answer is simple - we use it at work.

So, go to the folder of our solution (not the project), hold down the key Shift and right-click on an empty space in the folder - in the context menu, select the item "Open PowerShell window hereΒ».

We make one plugin project with compilation for different versions of Revit / AutoCAD

If you have not installed nuke, then first write the command

dotnet tool install Nuke.GlobalTool –global

Now write a command nuke and you will be prompted to configure nuke for the current project. I don’t know how to write it in Russian correctly - in English it will be written Could not find .nuke file. Do you want to setup a build? [y/n]

Press the Y key and then there will be direct settings. We want the simplest option using MSBuild, so we answer as in the screenshot:

We make one plugin project with compilation for different versions of Revit / AutoCAD

Let's go to Visual Studio, which will prompt us to reload the solution, since a new project has been added to it. Reload the solution and see that we have a project build in which we are only interested in one file - Build.cs

We make one plugin project with compilation for different versions of Revit / AutoCAD

We open this file and write a script to build the project for all configurations. Well, or use my script, which you can edit for yourself:

using System.IO;
using Nuke.Common;
using Nuke.Common.Execution;
using Nuke.Common.ProjectModel;
using Nuke.Common.Tools.MSBuild;
using static Nuke.Common.Tools.MSBuild.MSBuildTasks;

[CheckBuildProjectConfigurations]
[UnsetVisualStudioEnvironmentVariables]
class Build : NukeBuild
{
    public static int Main () => Execute<Build>(x => x.Compile);

    [Solution] readonly Solution Solution;

    // If the solution name and the project (plugin) name are different, then indicate the project (plugin) name here
    string PluginName => Solution.Name;

    Target Compile => _ => _
        .Executes(() =>
        {
            var project = Solution.GetProject(PluginName);
            if (project == null)
                throw new FileNotFoundException("Not found!");

            var build = new List<string>();
            foreach (var (_, c) in project.Configurations)
            {
                var configuration = c.Split("|")[0];

                if (configuration == "Debug" || build.Contains(configuration))
                    continue;

                Logger.Normal($"Configuration: {configuration}");

                build.Add(configuration);

                MSBuild(_ => _
                    .SetProjectFile(project.Path)
                    .SetConfiguration(configuration)
                    .SetTargets("Restore"));
                MSBuild(_ => _
                    .SetProjectFile(project.Path)
                    .SetConfiguration(configuration)
                    .SetTargets("Rebuild"));
            }
        });
}

We return to the PowerShell window and write the command again nuke (you can write the command nuke indicating the required Target. But we have one Targetwhich starts by default). After pressing the Enter key, we will feel like real hackers, because, like in a movie, our project will be automatically assembled for different configurations.

By the way, you can use PowerShell directly from Visual Studio (menu "View"->"Other windows"->"Package Manager Console”), but everything will be black and white, which is not very convenient.

This concludes my article. I am sure that you can figure out the option for AutoCAD yourself. I hope that the material presented here will find its "clients".

Thank you for attention!

Source: habr.com

Add a comment