
Nello sviluppo di plugin per applicazioni CAD ( si tratta di AutoCAD, Revit e Renga) nel tempo emerge un problema: escono nuove versioni dei programmi, cambia la loro API e occorre fare nuove versioni dei plugin.
Quando hai solo un plugin o sei ancora un principiante autodidatta in questo campo, puoi semplicemente fare una copia del progetto, modificare i punti necessari e assemblare una nuova versione del plugin. Di conseguenza, apportare successive modifiche al codice comporterà un aumento esponenziale del carico di lavoro.
Con l'accumulo di esperienza e conoscenze troverai diversi modi per automatizzare questo processo. Ho percorso questo cammino e voglio raccontarti a cosa sono arrivato alla fine e quanto sia comodo.
Per iniziare, consideriamo un metodo che è ovvio e che ho utilizzato a lungo.
Link ai file del progetto
E per rendere tutto semplice, visibile e comprensibile, descriverò tutto utilizzando un esempio astratto di sviluppo di un plugin.
Apriamo Visual Studio (io ho la versione Community 2019. E sì – in lingua russa) e creiamo una nuova soluzione. Chiamiamola MySuperPluginForRevit

Stiamo sviluppando un plugin per Revit per le versioni 2015-2020. Pertanto, creiamo un nuovo progetto (Libreria classi Net Framework) nella soluzione e lo chiamiamo MySuperPluginForRevit_2015

Dobbiamo aggiungere riferimenti all'API di Revit. Certo, possiamo aggiungere riferimenti a file locali (dovremo installare tutti i necessari SDK o tutte le versioni di Revit), ma seguiremo subito il percorso giusto e collegheremo un pacchetto NuGet. Puoi trovare una buona quantità di pacchetti, ma utilizzerò i miei.
Dopo aver collegato il pacchetto, fai clic destro sul punto «Link» e scegli dal menu il punto «Spostare packages.config in PackageReference…»

Se improvvisamente ti viene un attacco di panico perché nella finestra delle proprietà del pacchetto manca il punto importante «Copiare localmente», che dobbiamo necessariamente impostare su falsenon c'è bisogno di farsi prendere dal panico - andiamo nella cartella del progetto, apriamo il file con estensione .csproj in un editor a te comodo (io uso Notepad++) e troviamo lì la registrazione del nostro pacchetto. Attualmente appare così:
1.0.0Aggiungiamo la proprietà runtime. Risulterà in questo modo:
1.0.0
runtimeOra, durante la costruzione del progetto, i file del pacchetto non verranno copiati nella cartella di uscita.
Proseguiamo – immaginiamo subito che il nostro plugin utilizzerà alcune parti della Revit API, che sono cambiate nel tempo con l'uscita di nuove versioni. O magari dobbiamo semplicemente modificare alcune cose nel codice in base alla versione di Revit per cui stiamo sviluppando il plugin. Per gestire tali differenze nel codice, utilizzeremo i simboli di compilazione condizionale. Apriamo le proprietà del progetto, andiamo alla scheda «Build» e nel campo «Simboli di compilazione condizionale» scriviamo R2015.

Nota che il simbolo deve essere aggiunto sia per la configurazione Debug che per quella Release.
E mentre siamo nella finestra delle proprietà, passiamo subito alla scheda «Applicazione» e nel campo «Spazio dei nomi predefinito» e rimuoviamo il suffisso _2015, in modo che il nostro spazio dei nomi sia universale e non dipendente dal nome dell'assembly:

Nel mio caso, nel prodotto finale, i plugin di tutte le versioni vengono raggruppati in una sola cartella, quindi i nomi degli assembly rimangono con un suffisso del tipo _20xx. Ma puoi rimuovere il suffisso anche dal nome della build, se i file saranno posizionati in cartelle diverse.
Procediamo al codice del file Class1.cs e imitiamo un certo codice tenendo conto delle diverse versioni di 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;
}
}
}Ho subito considerato tutte le versioni di Revit superiori alla versione 2015 (che erano disponibili al momento della scrittura dell'articolo) e ho tenuto conto dei simboli di compilazione condizionale, creati secondo lo stesso modello.
Passiamo al pezzo forte. Creiamo un nuovo progetto nella nostra soluzione, ma questa volta per la versione del plugin per Revit 2016. Ripetiamo tutte le operazioni descritte sopra, sostituendo il numero 2015 con il numero 2016. Ma il file Class1.cs del nuovo progetto lo rimuoviamo.

Il file con il codice necessario è già disponibile da noi. Class1.cs Dobbiamo semplicemente inserire un collegamento nel nuovo progetto. Ci sono due modi per inserire collegamenti:
- Lungo – clicchiamo con il tasto destro sul progetto, selezioniamo l'opzione «Aggiungi» -> «Elemento esistente», nella finestra che si apre troviamo il file desiderato e invece dell'opzione «Aggiungi» scegliamo l'opzione «Aggiungi come collegamento»

- Breve – direttamente nell'esplora soluzioni selezioniamo il file (o anche più file, e persino intere cartelle) e trasciniamo nel nuovo progetto tenendo premuto il tasto Alt. Durante il trascinamento vedrete che, premendo il tasto Alt, il cursore cambierà da un segno più a una freccia.
UPD: Ho introdotto un po' di confusione in questo paragrafo: per trasferire più file è necessario tenere premuto Shift+Alt!
Dopo aver effettuato questa procedura, nel secondo progetto avremo un file Class1.cs con l'icona corrispondente (freccia blu):

Durante la modifica del codice nella finestra dell'editor, puoi anche scegliere in quale contesto visualizzare il codice, consentendoti di vedere e modificare il codice con diversi simboli di compilazione condizionale:

Seguendo questo schema, creiamo tutti gli altri progetti (2017-2020). Un trucco utile: se trascini file nell'esplora soluzioni non dal progetto di base, ma da un progetto dove sono già collegati, non è necessario tenere premuto il tasto Alt!
Questa opzione è piuttosto buona fino al momento in cui aggiungi una nuova versione del plugin o nuovi file al progetto; tutto questo diventa molto complicato. E recentemente ho improvvisamente capito come gestire tutto con un solo progetto e ora passiamo al secondo metodo.
La magia delle configurazioni
Arrivato fin qui, potresti esclamare «Perché hai descritto il primo metodo se l'articolo parla subito del secondo?!». L'ho fatto per chiarire a cosa ci servono i simboli di compilazione condizionale e in quali punti i nostri progetti differiscono. Ora ci è più chiaro quali differenze dobbiamo implementare, mantenendo un solo progetto.
E per rendere tutto più chiaro, non creeremo un nuovo progetto, ma apporteremo modifiche al nostro progetto attuale, creato con il primo metodo.
Innanzitutto rimuoviamo dalla soluzione tutti i progetti tranne quello principale (quello contenente direttamente i file). Ossia, i progetti per le versioni 2016-2020. Apriamo la cartella della soluzione e rimuoviamo le cartelle di questi progetti.
Nella nostra soluzione ci rimane un solo progetto — MySuperPluginForRevit_2015. Apriamo le sue proprietà e:
- Nella scheda «Applicazione» rimuoviamo il suffisso dal nome dell'assemblaggio _2015 (sarà chiaro più avanti il perché)
- Nella scheda «Build» rimuoviamo il simbolo di compilazione condizionale R2015 dal campo corrispondente
Nota: nell'ultima versione di Visual Studio c'è un bug – i simboli di compilazione condizionale non vengono visualizzati nella finestra delle proprietà del progetto, anche se sono presenti. Se hai riscontrato questo bug, dovrai rimuoverli manualmente dal file .csproj. Tuttavia, dobbiamo comunque lavorare in esso, quindi continuiamo a leggere.
Rinominiamo il progetto nella finestra dell'esplora soluzioni, rimuovendo il suffisso _2015 e poi rimuoviamo il progetto dalla soluzione. Questo è necessario per mantenere l'ordine e il senso di perfezionismo! Apriamo la cartella della nostra soluzione, rinominiamo in questo modo anche la cartella del progetto e ricarichiamo il progetto nella soluzione.
Apriamo il gestore configurazioni. La nostra configurazione Release in linea di massima non sarà necessaria, quindi la rimuoviamo. Creiamo nuove configurazioni con nomi già a noi familiari R2015, R2016, …, R2020. Tieni presente che non è necessario copiare i parametri da altre configurazioni e non è necessario creare configurazioni di progetto:

Andiamo nella cartella del progetto e apriamo il file con estensione .csproj nell'editor che preferisci. A proposito, puoi aprirlo anche in Visual Studio: devi scaricare il progetto e poi troverai la voce corretta nel menu contestuale:

Modificare in Visual Studio è addirittura preferibile, poiché l'editor allinea e fornisce suggerimenti.
Nel file vedremo gli elementi – in cima c'è quello generale, e seguito ci sono quelli con le condizioni. Questi elementi definiscono le proprietà del progetto durante la compilazione. Il primo elemento, che non ha condizioni, definisce le proprietà generali, mentre gli elementi con condizioni modificano alcune proprietà a seconda delle configurazioni.
Passiamo all'elemento generale (il primo) PropertyGroup e controlliamo la proprietà AssemblyName – questo è il nome dell'assembly e deve essere senza suffisso _2015. Se è presente un suffisso, lo rimuoviamo.
Troviamo l'elemento con la condizione
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">Non ci serve – lo rimuoviamo.
Elemento con condizione
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">sarà necessario per lo sviluppo e il debug del codice. Puoi modificare le sue proprietà secondo le tue esigenze, ad esempio impostando diversi percorsi di output e cambiando i simboli di compilazione condizionale.
Ora creiamo nuovi elementi PropertyGroup per le nostre configurazioni. In questi elementi dobbiamo impostare solo quattro proprietà:
- OutputPath – cartella di output. Imposto il valore standard binR20xx
- DefineConstants – simboli di compilazione condizionale. Dovresti impostare il valore TRACE;R20хх
- TargetFrameworkVersion – versione della piattaforma. Per diverse versioni di Revit API è necessario specificare piattaforme diverse.
- AssemblyName – nome dell'assemblaggio (cioè il nome del file). Puoi scrivere il nome dell'assemblaggio desiderato, ma per universalità ti consiglio di utilizzare il valore $(AssemblyName)_20хх. Per questo motivo in precedenza abbiamo rimosso il suffisso dal nome dell'assemblaggio.
La cosa più importante di tutti questi elementi è che possono essere banalmente copiati in altri progetti senza alcuna modifica. Più avanti nell'articolo allegherò tutto il contenuto del file .csproj.
Bene, abbiamo chiarito le proprietà del progetto – non è difficile. Ma cosa fare con le librerie collegate (pacchetti NuGet)? Se andiamo avanti, vedremo che le librerie collegate sono definite negli elementi . Ma c'è un problema – questo elemento gestisce in modo errato le condizioni, come l'elemento PropertyGroup. Potrebbe essere persino un bug di Visual Studio, ma se definiamo più elementi ItemGroup con le condizioni di configurazione, e all'interno inseriamo riferimenti diversi ai pacchetti NuGet, allora cambiando configurazione a progetto vengono collegati tutti i pacchetti indicati.
Ci viene in aiuto l'elemento , che funziona secondo la logica a noi familiare if-then-else.
Utilizzando l'elemento Scegliere, definiamo diversi pacchetti NuGet per diverse configurazioni:
Tutto il contenuto 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>Si prega di notare che in una delle condizioni ho specificato due configurazioni tramite OR (Oppure). In questo modo si collegherà il pacchetto corretto durante la configurazione Debug.
E a questo punto siamo quasi perfetti. Ricarichiamo il progetto, attiviamo la configurazione necessaria, selezioniamo nel menu contestuale della soluzione (non del progetto) l'opzione «Ripristina tutti i pacchetti NuGet» e vediamo come cambiano i nostri pacchetti.

E a questo punto mi sono bloccato: per raccogliere tutte le configurazioni potremmo utilizzare la compilazione di pacchetti (menu «Build» -> «Compilazione di pacchetti»), ma quando si cambiano le configurazioni non avviene il ripristino automatico dei pacchetti. E nemmeno durante la compilazione del progetto, sebbene, in teoria, dovrebbe. Non ho trovato soluzioni a questo problema con strumenti standard. E molto probabilmente è un bug di Visual Studio.
Pertanto, per la compilazione di pacchetti è stato deciso di utilizzare un sistema di compilazione automatizzata . In effetti, non lo volevo, poiché lo considero superfluo nell'ambito dello sviluppo di plugin, ma al momento non vedo altre soluzioni. E alla domanda «Perché proprio Nuke?» la risposta è semplice: lo usiamo al lavoro.
Quindi, accediamo alla cartella della nostra soluzione (non del progetto), premiamo il tasto Shift e facciamo clic con il pulsante destro del mouse su uno spazio vuoto nella cartella – nel menu contestuale selezioniamo l'opzione "Apri PowerShell qui».

Se non hai installato nuke, prima scrivi il comando
dotnet tool install Nuke.GlobalTool –globalOra scrivi il comando nuke e ti verrà chiesto di configurare nuke per il progetto attuale. Non so come scriverlo in modo più corretto in italiano – in inglese verrà scritto Could not find .nuke file. Do you want to setup a build? [y/n]
Premiamo il tasto Y e poi ci saranno i passaggi di configurazione. Abbiamo bisogno della soluzione più semplice utilizzando MSBuild, quindi rispondiamo come nella schermata:

Passiamo a Visual Studio, che ci propone di ricaricare la soluzione, poiché è stato aggiunto un nuovo progetto. Ricarichiamo la soluzione e vediamo che abbiamo un progetto build in cui ci interessa solo un file – Build.cs

Apriamo questo file e scriviamo lo script per la compilazione del progetto per tutte le configurazioni. Oppure utilizziamo il mio script, che puoi modificare secondo le tue esigenze:
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(x => x.Compile);
[Solution] readonly Solution Solution;
// Se il nome della soluzione e il nome del progetto (plugin) sono diversi, indica il nome del progetto (plugin) qui
string PluginName => Solution.Name;
Target Compile => _ => _
.Executes(() =>
{
var project = Solution.GetProject(PluginName);
if (project == null)
throw new FileNotFoundException("Non trovato!");
var build = new List();
foreach (var (_, c) in project.Configurations)
{
var configuration = c.Split("|")[0];
if (configuration == "Debug" || build.Contains(configuration))
continue;
Logger.Normal($"Configurazione: {configuration}");
build.Add(configuration);
MSBuild(_ => _
.SetProjectFile(project.Path)
.SetConfiguration(configuration)
.SetTargets("Restore"));
MSBuild(_ => _
.SetProjectFile(project.Path)
.SetConfiguration(configuration)
.SetTargets("Rebuild"));
}
});
}Torniamo alla finestra di PowerShell e scriviamo di nuovo il comando nuke (puoi scrivere il comando nuke indicando quello necessario Obiettivo. Ma abbiamo uno Obiettivo, che viene avviato di default). Dopo aver premuto il tasto Invio, ci sentiremo dei veri hacker, poiché come nel film avverrà la generazione automatica del nostro progetto per diverse configurazioni.
A proposito, è possibile utilizzare PowerShell direttamente da Visual Studio (menu «Visualizza» -> «Altre finestre» -> «Console del gestore pacchetti»), ma lì tutto sarà in bianco e nero, il che non è molto comodo.
Con questo si conclude il mio articolo. Sono sicuro che con l'opzione per AutoCAD saprete orientarvi da soli. Spero che il materiale presentato qui trovi i suoi «clienti».
Grazie per l'attenzione!
Fonte: habr.com
