Fazemos um projeto de plugin com compilação para diferentes versões do Revit/AutoCAD

Fazemos um projeto de plugin com compilação para diferentes versões do Revit/AutoCAD

Ao desenvolver plug-ins para aplicativos CAD (No meu caso estes são AutoCAD, Revit e Renga) com o tempo, surge um problema - novas versões de programas são lançadas, suas APIs são alteradas e novas versões de plugins precisam ser feitas.

Quando você possui apenas um plugin ou ainda é um iniciante autodidata no assunto, basta fazer uma cópia do projeto, alterar os locais necessários nele e montar uma nova versão do plugin. Conseqüentemente, alterações subsequentes no código implicarão um aumento múltiplo nos custos trabalhistas.

À medida que você ganha experiência e conhecimento, encontrará diversas maneiras de automatizar esse processo. Percorri esse caminho e quero contar a vocês o que acabei fazendo e como é conveniente.

Primeiro, vejamos um método que é óbvio e que utilizo há muito tempo.

Links para arquivos do projeto

E para tornar tudo simples, visual e compreensível, descreverei tudo usando um exemplo abstrato de desenvolvimento de plugins.

Vamos abrir o Visual Studio (tenho a versão Community 2019. E sim - em russo) e criar uma nova solução. Vamos ligar para ele MeuSuperPluginForRevit

Fazemos um projeto de plugin com compilação para diferentes versões do Revit/AutoCAD

Estaremos fazendo um plugin para Revit para as versões 2015-2020. Portanto, vamos criar um novo projeto na solução (Net Framework Class Library) e chamá-lo MeuSuperPluginForRevit_2015

Fazemos um projeto de plugin com compilação para diferentes versões do Revit/AutoCAD

Precisamos adicionar links para a API do Revit. Claro, podemos adicionar links para arquivos locais (precisaremos instalar todos os SDKs necessários ou todas as versões do Revit), mas seguiremos imediatamente o caminho certo e conectaremos o pacote NuGet. Você pode encontrar alguns pacotes, mas usarei o meu próprio.

Após conectar o pacote, clique com o botão direito no item “referências" e selecione o item "Mova packages.config para PackageReference...»

Fazemos um projeto de plugin com compilação para diferentes versões do Revit/AutoCAD

Se de repente neste ponto você começar a entrar em pânico, porque na janela de propriedades do pacote não haverá nenhum item importante “Copiar localmente", que definitivamente precisamos definir para o valor falso, então não entre em pânico - vá até a pasta com o projeto, abra o arquivo com a extensão .csproj em um editor conveniente para você (eu uso o Notepad++) e encontre lá uma entrada sobre nosso pacote. Ela está assim agora:

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

Adicione uma propriedade a ele tempo de execução. Será assim:

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

Agora, ao construir um projeto, os arquivos do pacote não serão copiados para a pasta de saída.
Vamos mais longe - vamos imaginar imediatamente que nosso plugin usará algo da API do Revit, que mudou ao longo do tempo quando novas versões foram lançadas. Bem, ou só precisamos mudar algo no código dependendo da versão do Revit para a qual estamos fazendo o plugin. Para resolver essas diferenças no código, usaremos símbolos de compilação condicional. Abra as propriedades do projeto, vá para a aba “montagem"e no campo"Notação de compilação condicional"vamos escrever R2015.

Fazemos um projeto de plugin com compilação para diferentes versões do Revit/AutoCAD

Observe que o símbolo deve ser adicionado para as configurações Debug e Release.

Bem, enquanto estamos na janela de propriedades, vamos imediatamente para a aba “Aplicação"e no campo"Namespace padrão» remover o sufixo _2015para que nosso namespace seja universal e independente do nome do assembly:

Fazemos um projeto de plugin com compilação para diferentes versões do Revit/AutoCAD

No meu caso, no produto final, os plugins de todas as versões são colocados em uma pasta, então meus nomes de assembly permanecem com o sufixo do formulário _20хх. Mas você também pode remover o sufixo do nome do assembly se os arquivos estiverem localizados em pastas diferentes.

Vamos ao código do arquivo Classe1.cs e simule algum código lá, levando em consideração diferentes versões do 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;
        }
    }
}

Levei imediatamente em consideração todas as versões do Revit acima da versão 2015 (que estavam disponíveis no momento da redação deste artigo) e imediatamente levei em consideração a presença de símbolos de compilação condicional, que são criados usando o mesmo modelo.

Passemos ao destaque principal. Criamos um novo projeto em nossa solução, apenas para a versão do plugin para Revit 2016. Repetimos todos os passos descritos acima, respectivamente, substituindo o número 2015 pelo número 2016. Mas o arquivo Classe1.cs excluir do novo projeto.

Fazemos um projeto de plugin com compilação para diferentes versões do Revit/AutoCAD

Arquivo com o código necessário - Classe1.cs – já o temos e só precisamos inserir um link para ele em um novo projeto. Existem duas maneiras de inserir links:

  1. Longo – clique com o botão direito no projeto e selecione “adicionar"->"Elemento existente", na janela que se abre, encontre o arquivo desejado e em vez da opção "adicionar"selecione a opção"Adicionar como conexão»

Fazemos um projeto de plugin com compilação para diferentes versões do Revit/AutoCAD

  1. Curto – diretamente no gerenciador de soluções, selecione o arquivo desejado (ou mesmo arquivos, ou mesmo pastas inteiras) e arraste-o para um novo projeto enquanto mantém pressionada a tecla Alt. Ao arrastar, você verá que ao pressionar a tecla Alt, o cursor do mouse mudará de um sinal de mais para uma seta.
    UPD: Fiz uma pequena confusão neste parágrafo - para transferir vários arquivos você deve manter pressionado Shift + Alt!

Após realizar o procedimento teremos um arquivo no segundo projeto Classe1.cs com o ícone correspondente (seta azul):

Fazemos um projeto de plugin com compilação para diferentes versões do Revit/AutoCAD

Ao editar o código na janela do editor, você também pode escolher em qual contexto do projeto exibir o código, o que permitirá ver o código sendo editado sob diferentes símbolos de compilação condicional:

Fazemos um projeto de plugin com compilação para diferentes versões do Revit/AutoCAD

Criamos todos os outros projetos (2017-2020) usando este esquema. Life hack - se você arrastar arquivos no Solution Explorer não do projeto base, mas do projeto onde eles já estão inseridos como um link, não será necessário manter pressionada a tecla Alt!

A opção descrita é bastante boa até o momento de adicionar uma nova versão do plugin ou até o momento de adicionar novos arquivos ao projeto - tudo isso se torna muito tedioso. E recentemente, de repente, percebi como resolver tudo com um projeto e estamos passando para o segundo método

A magia das configurações

Ao terminar de ler aqui, você pode exclamar: “Por que você descreveu o primeiro método, se o artigo é imediatamente sobre o segundo?!” E descrevi tudo para deixar mais claro por que precisamos de símbolos de compilação condicional e em que pontos nossos projetos diferem. E agora fica mais claro para nós exatamente quais diferenças de projetos precisamos implementar, restando apenas um projeto.

E para deixar tudo mais óbvio, não criaremos um novo projeto, mas faremos alterações em nosso projeto atual criado da primeira forma.

Então, em primeiro lugar, removemos todos os projetos da solução, exceto o principal (que contém os arquivos diretamente). Aqueles. projetos para versões 2016-2020. Abra a pasta com a solução e exclua as pastas desses projetos de lá.

Ainda temos um projeto em nossa decisão - MeuSuperPluginForRevit_2015. Abra suas propriedades e:

  1. Na guia “Aplicação"remova o sufixo do nome do assembly _2015 (ficará claro o porquê mais tarde)
  2. Na guia “montagem» remover o símbolo de compilação condicional R2015 do campo correspondente

Observação: a versão mais recente do Visual Studio possui um bug - os símbolos de compilação condicional não são exibidos na janela de propriedades do projeto, embora estejam disponíveis. Se você enfrentar essa falha, será necessário removê-los manualmente do arquivo .csproj. No entanto, ainda temos que trabalhar nisso, então continue lendo.

Renomeie o projeto na janela Solution Explorer removendo o sufixo _2015 e, em seguida, remova o projeto da solução. Isso é necessário para manter a ordem e os sentimentos dos perfeccionistas! Abrimos a pasta da nossa solução, renomeamos a pasta do projeto da mesma forma e carregamos o projeto de volta na solução.

Abra o gerenciador de configuração. Configuração dos EUA Solte em princípio, não será necessário, por isso o excluímos. Criamos novas configurações com nomes que já nos são familiares R2015, R2016..., R2020. Observe que você não precisa copiar configurações de outras configurações e não precisa criar configurações de projeto:

Fazemos um projeto de plugin com compilação para diferentes versões do Revit/AutoCAD

Vá até a pasta com o projeto e abra o arquivo com extensão .csproj em um editor que lhe seja conveniente. Aliás, você também pode abri-lo no Visual Studio - você precisa descarregar o projeto e então o item desejado estará no menu de contexto:

Fazemos um projeto de plugin com compilação para diferentes versões do Revit/AutoCAD

A edição no Visual Studio é ainda preferível, já que o editor alinha e avisa.

No arquivo veremos os elementos Grupo de propriedades – no topo está o geral, e depois vêm as condições. Esses elementos definem as propriedades do projeto quando ele é construído. O primeiro elemento, que não tem condições, define propriedades gerais, e os elementos com condições, respectivamente, alteram algumas propriedades dependendo das configurações.

Vá para o elemento comum (primeiro) Grupo de propriedades e olhe para a propriedade Nome da Assembleia – este é o nome da assembleia e devemos tê-lo sem sufixo _2015. Se houver um sufixo, remova-o.

Encontrando um elemento com uma condição

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

Não precisamos disso - nós o excluímos.

Elemento com condição

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

será necessário para trabalhar na fase de desenvolvimento e depuração do código. Você pode alterar suas propriedades para atender às suas necessidades - definir diferentes caminhos de saída, alterar símbolos de compilação condicional, etc.

Agora vamos criar novos elementos Grupo de propriedades para nossas configurações. Nestes elementos precisamos apenas definir quatro propriedades:

  • Caminho de saída - pasta de saída. Eu configurei o valor padrão binR20xx
  • Definir Constantes – símbolos de compilação condicional. O valor deve ser especificado TRAÇO;R20хх
  • Versão TargetFramework – versão da plataforma. Versões diferentes da API do Revit exigem a especificação de plataformas diferentes.
  • Nome da Assembleia – nome do assembly (ou seja, nome do arquivo). Você pode escrever o nome exato da montagem, mas para versatilidade recomendo escrever o valor $(NomeAssembly)_20хх. Para fazer isso, removemos anteriormente o sufixo do nome do assembly

A característica mais importante de todos esses elementos é que eles podem simplesmente ser copiados para outros projetos sem alterá-los. Posteriormente neste artigo anexarei todo o conteúdo do arquivo .csproj.

Ok, descobrimos as propriedades do projeto - não é difícil. Mas o que fazer com bibliotecas de plug-ins (pacotes NuGet). Se olharmos mais adiante, veremos que as bibliotecas incluídas são especificadas nos elementos Grupo de itens. Mas azar - este elemento processa incorretamente as condições como um elemento Grupo de propriedades. Talvez isso seja até uma falha do Visual Studio, mas se você especificar vários elementos Grupo de itens com condições de configuração e inserir diferentes links para pacotes NuGet dentro, quando você alterar a configuração, todos os pacotes especificados serão conectados ao projeto.

O elemento vem em nosso auxílio Escolha, que funciona de acordo com nossa lógica usual se-então-senão.

Usando elemento Escolha, definimos diferentes pacotes NuGet para diferentes configurações:

Todo o conteúdo 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>

Observe que em uma das condições especifiquei duas configurações via OU. Desta forma, o pacote necessário será conectado durante a configuração depurar.

E aqui temos quase tudo perfeito. Carregamos o projeto de volta, habilitamos a configuração que precisamos, chamamos o item “ no menu de contexto da solução (não do projeto)Restaurar todos os pacotes NuGet"e vemos como nossos pacotes mudam.

Fazemos um projeto de plugin com compilação para diferentes versões do Revit/AutoCAD

E nesta fase cheguei a um beco sem saída - para coletar todas as configurações de uma vez, poderíamos usar a montagem em lote (menu "montagem"->"Construção em lote"), mas ao alternar configurações, os pacotes não são restaurados automaticamente. E na hora de montar o projeto isso também não acontece, embora, em tese, devesse. Não encontrei uma solução para este problema usando meios padrão. E provavelmente isso também é um bug do Visual Studio.

Portanto, para montagem em lote, optou-se por utilizar um sistema especial de montagem automatizada Nuke. Na verdade eu não queria isso porque acho um exagero em termos de desenvolvimento de plugins, mas no momento não vejo outra solução. E para a pergunta “Por que Nuke?” A resposta é simples - usamos no trabalho.

Então, vá até a pasta da nossa solução (não do projeto), mantenha pressionada a tecla Shift e clique com o botão direito em um espaço vazio da pasta - no menu de contexto selecione o item “Abra a janela do PowerShell aqui".

Fazemos um projeto de plugin com compilação para diferentes versões do Revit/AutoCAD

Se você não o tiver instalado nuke, então primeiro escreva o comando

dotnet tool install Nuke.GlobalTool –global

Agora escreva o comando nuke e você será solicitado a configurar nuke para o projeto atual. Não sei como escrever isso mais corretamente em russo - em inglês estará escrito Não foi possível encontrar o arquivo .nuke. Você deseja configurar uma compilação? [s/n]

Pressione a tecla Y e então haverá itens de configurações diretas. Precisamos da opção mais simples usando MSBuildName, então respondemos como na captura de tela:

Fazemos um projeto de plugin com compilação para diferentes versões do Revit/AutoCAD

Vamos para o Visual Studio, que nos solicitará que recarreguemos a solução, pois um novo projeto foi adicionado a ela. Recarregamos a solução e vemos que temos um projeto construir em que estamos interessados ​​​​em apenas um arquivo - Construir.cs

Fazemos um projeto de plugin com compilação para diferentes versões do Revit/AutoCAD

Abra este arquivo e escreva um script para construir o projeto para todas as configurações. Bem, ou use meu script, que você pode editar para atender às suas necessidades:

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

Voltamos à janela do PowerShell e escrevemos o comando novamente nuke (você pode escrever o comando nuke indicando o necessário Target. Mas nós temos um Target, que é executado por padrão). Após pressionar a tecla Enter, nos sentiremos como verdadeiros hackers, pois, como em um filme, nosso projeto será montado automaticamente para diferentes configurações.

A propósito, você pode usar o PowerShell diretamente do Visual Studio (menu "Ver"->"Outras janelas"->"Console do gerenciador de pacotes"), mas tudo será em preto e branco, o que não é muito conveniente.

Isso conclui meu artigo. Tenho certeza de que você mesmo pode descobrir a opção do AutoCAD. Espero que o material aqui apresentado encontre seus “clientes”.

Obrigado!

Fonte: habr.com

Adicionar um comentário