Hacemos un proyecto de complemento con compilación para diferentes versiones de Revit / AutoCAD



Al desarrollar complementos para aplicaciones CAD ( en mi caso, AutoCAD, Revit y Renga), surge un problema con el tiempo: salen nuevas versiones de programas, se deben realizar cambios en la API y nuevas versiones de complementos.


Cuando solo tiene un complemento o todavía es un principiante autodidacta en este asunto, simplemente puede hacer una copia del proyecto, cambiar los lugares necesarios y crear una nueva versión del complemento. En consecuencia, los cambios posteriores al código implicarán un aumento múltiple en los costos laborales.


A medida que gane experiencia y conocimiento, encontrará varias formas de automatizar este proceso. Fui de esta manera y quiero contarles a qué me he enfrentado al final y qué tan conveniente es.


Para comenzar, considere un método que es obvio y que he estado usando durante mucho tiempo.


Enlaces a archivos de proyecto


Y para que todo sea simple, claro y comprensible, describiré todo utilizando un ejemplo abstracto de desarrollo de plug-in.


Abra Visual Studio (tengo la versión Community 2019. Y sí, en ruso) y cree una nueva solución. Llamémoslo MySuperPluginForRevit


imagen1

Crearemos un complemento para Revit para las versiones 2015-2020. Por lo tanto, crearemos un nuevo proyecto en la solución (Net Framework Class Library) y lo llamaremos MySuperPluginForRevit_2015


imagen2

Necesitamos agregar enlaces a la API de Revit. Por supuesto, podemos agregar enlaces a archivos locales (necesitará instalar todos los SDK necesarios o todas las versiones de Revit), pero seguiremos el camino correcto y conectaremos el paquete NuGet. No puede encontrar una pequeña cantidad de paquetes, pero usaré el mío.


Después de conectar el paquete, haga clic derecho en el elemento " Enlaces " y seleccione " Transferir paquetes.configar a PackageReference ... " en el menú


imagen3

Si repentinamente entra en pánico en este punto, ya que la ventana de propiedades del paquete no tiene el elemento importante " Copiar localmente ", que definitivamente debemos establecer en falso , entonces no entre en pánico: vaya a la carpeta del proyecto y abra el archivo con la extensión. csproj en un editor conveniente (uso Notepad ++) y encontramos un registro sobre nuestro paquete. Se ve así ahora:


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

Agregue la propiedad <ExcludeAssets> runtime </ExcludeAssets> . Resulta así:


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

Ahora, al crear un proyecto, los archivos del paquete no se copiarán en la carpeta de salida.
Vamos más allá: inmediatamente imagine que nuestro complemento usará algo de la API de Revit, que ha cambiado con el lanzamiento de nuevas versiones. Bueno, o simplemente, necesitamos cambiar algo en el código, dependiendo de la versión de Revit para la que estamos haciendo el complemento. Para resolver tales diferencias en el código, utilizaremos símbolos de compilación condicional. Abra las propiedades del proyecto, vaya a la pestaña " Ensamblaje " y en el campo " Símbolos de compilación condicional " escriba R2015 .


imagen4

Tenga en cuenta que el símbolo debe agregarse tanto para la configuración de depuración como para la configuración de lanzamiento.


Bueno, mientras estamos en la ventana de propiedades, inmediatamente vamos a la pestaña " Aplicación " y en el campo " Espacio de nombres predeterminado " eliminamos el sufijo _2015 para que nuestro espacio de nombres sea universal e independiente del nombre del ensamblado:


imagen5

En mi caso, en el producto final, los complementos de todas las versiones se colocan en una carpeta, por lo que mis nombres de ensamblado permanecen con el sufijo del formulario _20xx . Pero también puede eliminar el sufijo del nombre del ensamblado si se supone la ubicación de los archivos en diferentes carpetas.


Pasamos al código del archivo Class1.cs y simulamos algo de código allí, teniendo en cuenta las diferentes versiones de 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; } } } 

Inmediatamente tomé en cuenta todas las versiones de Revit anteriores a la versión 2015 (que estaban en el momento de la escritura) e inmediatamente tomé en cuenta la presencia de símbolos de compilación condicional, que creo usando la misma plantilla.


Pasamos al punto culminante principal. Creamos un nuevo proyecto en nuestra solución, solo para la versión del complemento para Revit 2016. Repetimos todos los pasos descritos anteriormente, respectivamente, reemplazando el número 2015 con el número 2016. Pero eliminamos el archivo Class1.cs del nuevo proyecto.


imagen6

El archivo con el código deseado, Class1.cs , ya lo tenemos y solo necesitamos insertar un enlace en el nuevo proyecto. Hay dos formas de insertar enlaces:


  1. Haga clic largo en el proyecto con el botón derecho del mouse, seleccione “ Agregar ” -> “ Elemento existente ”, en la ventana que se abre, busque el archivo y, en lugar de la opción “ Agregar ”, seleccione “ Agregar como enlace

imagen7

  1. Corto : justo en el explorador de soluciones, seleccione el archivo deseado (o incluso archivos. O incluso carpetas completas) y arrastre y suelte en el nuevo proyecto con la tecla Alt presionada. Al arrastrar, verá que cuando presiona la tecla Alt, el cursor del mouse cambiará de un signo más a una flecha.
    UPD: Confundí un poco en esta sección: para transferir varios archivos, mantenga presionada la tecla Mayús + Alt .

Después del procedimiento, veremos en el segundo proyecto el archivo Class1.cs con el icono correspondiente (flecha azul):


imagen8

Al editar el código en la ventana del editor, también puede elegir en el contexto de qué proyecto mostrar el código, lo que le permite ver el código que se está editando con diferentes símbolos de compilación condicional:


imagen9

Bajo este esquema, creamos todos los demás proyectos (2017-2020). Hack de vida: si arrastra y suelta archivos en el explorador de soluciones no desde el proyecto base, sino desde el proyecto donde ya están insertados como un enlace, ¡entonces no puede mantener presionada la tecla Alt!


La opción descrita es bastante buena hasta el momento en que se agrega la nueva versión del complemento o hasta que se agregan los nuevos archivos al proyecto; todo esto se vuelve muy triste. Y recientemente, de repente me di cuenta de cómo resolver todo esto con un proyecto y pasamos al segundo método.


Configuración mágica


Después de leer aquí, puedes exclamar: "¿Qué demonios describiste el primer método, si el artículo trata inmediatamente del segundo?" Y describí todo para aclarar por qué necesitamos símbolos de compilación condicional y en qué lugares difieren nuestros proyectos. Y ahora nos queda más claro qué diferencias específicas de proyecto necesitamos implementar, dejando solo un proyecto.


Y para que todo sea más obvio, no crearemos un nuevo proyecto, sino que haremos cambios en nuestro proyecto actual, creado de la primera manera.


Entonces, en primer lugar, eliminamos todos los proyectos de la solución, excepto el principal (que contiene los archivos directamente). Es decir proyectos para versiones 2016-2020. Abra la carpeta de la solución y elimine las carpetas de estos proyectos allí.


Nos queda una solución en la solución: MySuperPluginForRevit_2015 . Abrimos sus propiedades y:


  1. En la pestaña Aplicación , elimine el sufijo _2015 del nombre del ensamblado (quedará claro por qué)
  2. En la pestaña " Ensamblaje ", elimine el símbolo de compilación condicional R2015 del campo correspondiente

Nota: en la última versión de Visual Studio hay una falla: los símbolos de compilación condicional no se muestran en la ventana de propiedades del proyecto, aunque existen. Si tiene este problema técnico, debe eliminarlos manualmente del archivo .csproj. Sin embargo, todavía trabajamos en él, así que sigue leyendo.

Cambie el nombre del proyecto en la ventana del explorador de soluciones eliminando el sufijo _2015 y luego elimine el proyecto de la solución. ¡Esto es necesario para mantener el orden y los sentimientos de los perfeccionistas! Abrimos la carpeta de nuestra solución, cambiamos el nombre de la carpeta del proyecto de la misma manera y volvemos a cargar el proyecto en la solución.


Abre el administrador de configuración. Nosotros, en principio, no necesitaremos la configuración de lanzamiento , por lo que la eliminaremos. Estamos creando nuevas configuraciones con los nombres R2015 , R2016 , ..., R2020 que ya nos son familiares. Tenga en cuenta que no necesita copiar parámetros de otras configuraciones y no necesita crear configuraciones de proyecto:


imagen10

Vamos a la carpeta del proyecto y abrimos el archivo con la extensión .csproj en un editor conveniente. Por cierto, también se puede abrir en Visual Studio: debe descargar el proyecto y luego el elemento correcto estará en el menú contextual:


imagen11

La edición en Visual Studio es incluso preferible, ya que el editor se alinea y solicita.


En el archivo veremos los elementos de PropertyGroup : en la parte superior está el general y luego con las condiciones. Estos elementos establecen las propiedades del proyecto durante su ensamblaje. El primer elemento, que sin condiciones, establece propiedades generales, y los elementos con condiciones, respectivamente, cambian algunas propiedades dependiendo de las configuraciones.


Vamos al elemento general (primer) del PropertyGroup y miramos la propiedad AssemblyName : este es el nombre del ensamblado y deberíamos tenerlo sin el sufijo _2015 . Si hay un sufijo, elimínelo.


Encontramos un elemento con la condición


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

No lo necesitamos, lo eliminamos.


Cláusula de condición


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

Será necesario para trabajar en la etapa de desarrollo y depuración del código. Puede cambiar sus propiedades según sus necesidades: establecer diferentes rutas de salida, cambiar símbolos de compilación condicional, etc.


Ahora cree nuevos elementos PropertyGroup para nuestras configuraciones. En estos elementos, solo necesitamos establecer cuatro propiedades:


  • OutputPath es la carpeta de salida. Configuré el valor estándar bin \ R20xx
  • DefineConstants : símbolos de compilación condicional. Establecer RASTREO; R20xx
  • TargetFrameworkVersion - versión de la plataforma. Para diferentes versiones de la API de Revit, se deben definir diferentes plataformas.
  • AssemblyName : nombre del ensamblado (es decir, nombre del archivo). Puede escribir directamente el nombre del ensamblado deseado, pero para la universalidad, le recomiendo escribir el valor de $ (AssemblyName) _20xx . Para hacer esto, previamente eliminamos el sufijo del nombre del ensamblado

La característica más importante de todos estos elementos es que pueden copiarse trivialmente a otros proyectos sin cambiarlos en absoluto. Más adelante en el artículo adjuntaré todo el contenido del archivo .csproj.


Bueno, descubrimos las propiedades del proyecto, no es difícil. Pero qué hacer con las bibliotecas de complementos (paquetes NuGet). Si buscamos más, veremos que las bibliotecas de complementos están definidas por elementos de ItemGroup . Pero aquí está la mala suerte: este elemento procesa incorrectamente las condiciones, como un elemento PropertyGroup . Quizás esto sea incluso una falla de Visual Studio, pero si configura varios elementos de ItemGroup con condiciones de configuración e inserta diferentes enlaces a los paquetes de NuGet en su interior, cuando cambie la configuración, todos los paquetes especificados se conectarán al proyecto.


El elemento Choose viene en nuestra ayuda, que funciona de acuerdo con la lógica habitual if-then-else .


Usando el elemento Choose , definimos diferentes paquetes NuGet para diferentes configuraciones:


Todo el contenido de csproj
 <?xml version="1.0" encoding="utf-8"?> <Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" /> <PropertyGroup> <Configuration Condition=" '$(Configuration)' == '' ">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>bin\Debug\</OutputPath> <DefineConstants>DEBUG;R2015</DefineConstants> <ErrorReport>prompt</ErrorReport> <WarningLevel>4</WarningLevel> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'R2015|AnyCPU' "> <OutputPath>bin\R2015\</OutputPath> <DefineConstants>TRACE;R2015</DefineConstants> <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> <AssemblyName>$(AssemblyName)_2015</AssemblyName> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'R2016|AnyCPU' "> <OutputPath>bin\R2016\</OutputPath> <DefineConstants>TRACE;R2016</DefineConstants> <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> <AssemblyName>$(AssemblyName)_2016</AssemblyName> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'R2017|AnyCPU' "> <OutputPath>bin\R2017\</OutputPath> <DefineConstants>TRACE;R2017</DefineConstants> <TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion> <AssemblyName>$(AssemblyName)_2017</AssemblyName> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'R2018|AnyCPU' "> <OutputPath>bin\R2018\</OutputPath> <DefineConstants>TRACE;R2018</DefineConstants> <TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion> <AssemblyName>$(AssemblyName)_2018</AssemblyName> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'R2019|AnyCPU' "> <OutputPath>bin\R2019\</OutputPath> <DefineConstants>TRACE;R2019</DefineConstants> <TargetFrameworkVersion>v4.7</TargetFrameworkVersion> <AssemblyName>$(AssemblyName)_2019</AssemblyName> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'R2020|AnyCPU' "> <OutputPath>bin\R2020\</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="Properties\AssemblyInfo.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> 

Tenga en cuenta que en una de las condiciones especifiqué dos configuraciones a través de OR (Or) . Por lo tanto, el paquete necesario se conectará durante la configuración de depuración .


Y aquí tenemos casi todo es perfecto. Volvemos a cargar el proyecto, activamos la configuración que necesitamos, llamamos al elemento " Restaurar todos los paquetes NuGet " en el menú contextual de la solución (no el proyecto) y vemos cómo están cambiando los paquetes.


imagen12

Y en esta etapa me detuve: para recopilar todas las configuraciones a la vez, podríamos usar el conjunto de lotes (menú " Conjunto " -> " Conjunto de lotes "), pero al cambiar las configuraciones, los paquetes no se restauran automáticamente. Y al armar el proyecto, tampoco sucede, aunque, en teoría, debería suceder. No encontré una solución a este problema por medios estándar. Y lo más probable es que esto también sea un error de Visual Studio.


Por lo tanto, para el ensamblaje por lotes, se decidió utilizar un sistema de ensamblaje automatizado Nuke especial. En realidad, no quería esto, ya que lo considero innecesario en el marco del desarrollo de complementos, pero por el momento no veo otra solución. Y la pregunta "¿Por qué exactamente Nuke?" La respuesta es simple: la usamos en el trabajo.


Entonces, vamos a la carpeta de nuestra solución (no al proyecto), mantenga presionada la tecla Mayús y haga clic derecho en un lugar vacío de la carpeta; en el menú contextual, seleccione el elemento " Abrir ventana de PowerShell aquí ".


imagen13

Si no tiene instalado Nuke , primero escriba el comando


 dotnet tool install Nuke.GlobalTool –global 

Ahora escriba el comando nuclear y se le pedirá que configure la bomba nuclear para el proyecto actual. No sé cómo escribirlo más correctamente en ruso. No se pudo encontrar el archivo .nuke que se escribirá en inglés. ¿Quieres configurar una compilación? [s / n]


Presione la tecla Y y luego habrá la configuración inmediata. Necesitamos la opción más simple usando MSBuild , por lo que respondemos como en la captura de pantalla:


imagen14

Pasemos a Visual Studio, que nos ofrecerá volver a cargar la solución, ya que se le ha agregado un nuevo proyecto. Reiniciamos la solución y vemos que tenemos un proyecto de compilación en el que solo estamos interesados ​​en un archivo: Build.cs


imagen15

Abra este archivo y escriba un script para construir el proyecto para todas las configuraciones. Bueno, o usa mi script, que puedes editar por ti mismo:


 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")); } }); } 

Regresamos a la ventana de PowerShell y volvemos a escribir el comando nuclear (puede escribir el comando nuclear con el Target deseado. Pero tenemos un Target , que comienza por defecto). Después de presionar la tecla Intro, nos sentiremos como verdaderos piratas informáticos, porque, como en una película, nuestro proyecto se ensamblará automáticamente para diferentes configuraciones.


Por cierto, puede usar PowerShell directamente desde Visual Studio (menú " Ver " -> " Otras ventanas " -> " Package Manager Console "), pero todo será en blanco y negro, lo que no es muy conveniente.


Sobre esto mi artículo está terminado. Estoy seguro de que puede descubrir la opción para AutoCAD usted mismo. Espero que el material presentado aquí encuentre a sus "clientes".


Gracias por su atencion!

Source: https://habr.com/ru/post/482308/


All Articles