среда, 11 июня 2014 г.

Единая "точка входа" (DLL файл) в плагин .NET, ARX или VBA, не зависящая от версии AutoCAD

Поскольку .NET плагины AutoCAD (и не только они) имеют зависимость от версий обозначенной САПР, то нередко приходится один и тот же исходный код компилировать отдельно под разные версии AutoCAD. Результаты компиляции я, как правило, размещаю либо в одном и том же каталоге плагина, либо по специальным его подкаталогам, имена которых указывают на целевую версию ядра AutoCAD, а так же зависимость от разрядности приложения (если таковая присутствует). Какой из двух перечисленных вариантов использовать - зависит от конкретного случая.

Т.о. результат может выглядеть, к примеру, либо так:


либо так:


Имена файлов, зависящих от версии AutoCAD, так же содержат в виде суффикса версию ядра приложения, а так же, в случае необходимости, разрядность целевой платформы. Например, согласно обозначенному выше скрину, в подкаталоге .\R17.2x64 будет находиться файл RegionTools.17.2x64.dll, а в подкаталоге .\R20.0 - файл RegionTools.20.0.dll.

Я использую версию ядра AutoCAD, а не год (2009, 2010, 2011 и т.д.), обозначенный в имени САПР, т.к. программно нужную версию DLL файла удобней находить именно по версии ядра AutoCAD - это наиболее надёжная и точная информация.

Конечно, при такой системе наименований рядовому пользователю может быть сложно понять, какой именно DLL файл из набора имеющихся подкаталогов, ему следует загружать в установленную у него версию AutoCAD. Как вариант: можно в файле readme.txt разместить текстовую информацию о том, для какой версии AutoCAD какой DLL файл следует загружать.

Однако можно эту задачу решить иначе: непосредственно в подкаталоге .\bin создавать единственный DLL файл, который предназначен для загрузки в любую версию AutoCAD. Предназначение этого файла заключается в том, чтобы загрузить в AutoCAD наиболее подходящую версию плагина, найдя её либо в текущем каталоге сборки, либо в соответствующих подкаталогах. На скрине показанном выше эта роль возложена на файл RegionTools.dll.

Т.о. Какую бы версию AutoCAD пользователь не имел на своей машине, ему всегда нужно загружать только файл RegionTools.dll

Аналогичная проблема актуальна так же для плагинов ARX и VBA (VBA "хромает" по части разрядности: x86\x64). Поэтому было бы вполне логично расширить решение таким образом, чтобы оно работало не только для .NET, но так же и для C++, VBA. Для ARX и VBA плагинов систему наименований, а так же структуру подкаталогов использую такие же, как и для .NET (указана мною выше).

Поскольку данный подход удобно применять во всех плагинах,  то его можно оформить в виде шаблона проекта:



Далее приводится подробно комментированный код обозначенной выше логики. Проект компилирую с опцией AnyCPU под .NET 3.5,  хотя AutoCAD 2009 по умолчанию использует 3.0. Мой выбор обусловлен тем, что AutoCAD 2009 может вместо 3.0 использовать 3.5, в которой присутствует технология LINQ, активно мною используемая. Версии ниже чем AutoCAD 2009 мне не интересны. Однако тут возникает один нюанс: на исходном компьютере так же должен быть установлен .NET Framework 3.5 SP1.

/* EntryPoint.cs
 * © Andrey Bushman, 2014
 * Поиск и загрузка версии плагина .NET, ARX или VBA, наиболее пригодной для 
 * текущей версии AutoCAD.
 * http://bushman-andrey.blogspot.ru/2014/06/dll-autocad.html
 */
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
 
#if AUTOCAD
using cad = Autodesk.AutoCAD.ApplicationServices.Application;
using Rt = Autodesk.AutoCAD.Runtime;
#endif
 
[assembly: Rt.ExtensionApplication(typeof(Bushman.CAD.EntryPoint
  .EntryPoint))]
 
namespace Bushman.CAD.EntryPoint {
  /// <summary>
  /// Задачей данного класса является поиск и загрузка в AutoCAD наиболее 
  /// подходящей для него версии плагина.
  /// </summary>
  public sealed class EntryPoint : Rt.IExtensionApplication {
    const String netPluginExtension = ".dll";
    static readonly String[] extensions = new String[] { ".arx"".dvb" };
    static readonly String[] methodNames = new String[] { "LoadArx""LoadDVB" 
    };
 
    /// <summary>
    /// Код этого метода будет запущен на исполнение при загрузке сборки в 
    /// AutoCAD. В результате его работы происходит попытка найти и загрузить в
    /// AutoCAD наиболее подходящую версию плагина из имеющихся в наличии.
    /// </summary>
    public void Initialize() {
      // Для начала извлекаем информацию о текущей версии AutoCAD и ищем
      // соответствующую ей версию файла. Имя такого файла должно 
      // формироваться по правилу: 
      //    ИмяТекущейСборки.Major.Minor[x86|x64].(dll|arx|dvb).
      // Где <Major> и <Minor> - это значения одноимённых свойств объекта 
      // Version, полученного из Application.Version.
      Version version = cad.Version;
 
      String fileFullName = GetType().Assembly.Location;
 
      Version minVersion = new Version(17, 2);
 
      FileInfo targetDllFullName = FindFile(fileFullName, version, minVersion);
 
      if(targetDllFullName == null)
        return;
 
      // Если найден файл, соответствующий нашей версии AutoCAD, то 
      // загружаем его.
      Assembly asm = null;
      try {
        if(targetDllFullName.Extension.Equals(netPluginExtension,
          StringComparison.CurrentCultureIgnoreCase))
          asm = Assembly.LoadFrom(targetDllFullName.FullName);
        else {
          Int32 index = Array.IndexOf(extensions, targetDllFullName.Extension);
 
          if(index >= 0) {
            Object application = cad.AcadApplication;
 
            application.GetType().InvokeMember(methodNames[index], BindingFlags
              .InvokeMethod, null, application, new Object[] { 
                targetDllFullName.FullName });
          }
        }
      }
      catch {
      }
    }
 
    /// <summary>
    /// Получить имя наиболее подходящего файла, для его последующей загрузки в
    /// AutoCAD. Если такой файл не будет найден, то возвращается null.
    /// </summary>
    /// <param name="fileFullName">"Базовое" имя файла, т.е. полное имя 
    /// файла без указания в нём версий ядра и разрядности платформы.</param>
    /// <param name="expectedVersion">Версия AutoCAD, для которой следует 
    /// выполнить поиск соответствующей версии файла.</param>
    /// <param name="minVersion">Наименьшая версия AutoCAD, ниже которой не 
    /// следует выполнять поиск.</param>
    /// <returns>Возвращается FileInfo наиболее подходящего файла, для его 
    /// последующей загрузки в AutoCAD. Если такой файл не будет найден, то 
    /// возвращается null.</returns>
    private FileInfo FindFile(String fileFullName, Version expectedVersion,
      Version minVersion) {
 
      if(fileFullName == null)
        throw new ArgumentNullException("fileFullName");
 
      if(fileFullName.Trim() == String.Empty)
        throw new ArgumentException(
          "fileFullName.Trim() == String.Empty");
 
      if(expectedVersion < minVersion)
        throw new ArgumentException(
          "expectedVersion < minVersion");
 
      Int32 major = expectedVersion.Major;
      Int32 minor = expectedVersion.Minor;
 
      String directory = Path.GetDirectoryName(fileFullName);
      String fileName = Path.GetFileNameWithoutExtension(fileFullName);
 
      String coreString = String.Format("{0}.{1}", major.ToString(),
        minor.ToString());
 
      String subDirectoryName = "R" + coreString;
      String subDirectoryName_xPlatform = subDirectoryName + (IntPtr.Size == 4
        ? "x86" : "x64");
 
      String targetFileName = String.Empty;
      String targetFileName_xPlatform = String.Empty;
      String targetFileFullName = String.Empty;
      String targetFileFullName_xPlatform = String.Empty;
 
      List<String> items = new List<String>(extensions);
      items.Insert(0, netPluginExtension);
 
      String name = String.Empty;
 
      foreach(String extension in items) {
 
        targetFileName = String.Format("{0}.{1}{2}", fileName, coreString,
          extension);
        targetFileName_xPlatform = String.Format("{0}.{1}{2}{3}", fileName,
          coreString, (IntPtr.Size == 4 ? "x86" : "x64"), extension);
 
        // Сначала выполняем поиск в текущем каталоге
        targetFileFullName = Path.Combine(directory, targetFileName);
        if(File.Exists(targetFileFullName)) {
          name = targetFileFullName;
          break;
        }
        targetFileFullName_xPlatform = Path.Combine(directory,
          targetFileName_xPlatform);
        if(File.Exists(targetFileFullName_xPlatform)) {
          name = targetFileFullName_xPlatform;
          break;
        }
 
        // Если в текущем каталоге подходящий файл не найден, то продолжаем
        // поиск по соответствующим подкаталогам
        targetFileFullName = directory + "\\" + subDirectoryName +
          "\\" + targetFileName;
        if(File.Exists(targetFileFullName)) {
          name = targetFileFullName;
          break;
        }
 
        targetFileFullName_xPlatform = directory + "\\" +
          subDirectoryName_xPlatform + "\\" + targetFileName_xPlatform;
        if(File.Exists(targetFileFullName_xPlatform)) {
          name = targetFileFullName_xPlatform;
          break;
        }
      }
 
      // Если найден файл, соответствующий нашей версии AutoCAD, то возвращаем 
      // соответтствующий ему объект FileInfo.
      if(File.Exists(name)) {
        return new FileInfo(name);
      }
      // Если соответствия не найдено, то продолжаем поиск, последовательно 
      // проверяя наличие подходящего файла для более ранних версий AutoCAD
      else {
        if(minor == 0) {
          minor = 3;
          --major;
        }
        else {
          --minor;
        }
 
        Version version = new Version(major, minor);
        if(version < minVersion)
          return null;
        FileInfo file = FindFile(fileFullName, new Version(major, minor),
          minVersion);
        return file;
      }
    }
 
    /// <summary>
    /// Код данного метода выполняется при завершении работы AutoCAD.
    /// </summary>
    public void Terminate() {
    }
  }
}

6 комментариев:

Андрей Бушман комментирует...

В исходный код внесены изменения: теперь его можно использовать для автоматического поиска и загрузки нужной версии не только файлов DLL, но и ARX, DVB.

Евгений Хрущ комментирует...

Спасибо за интересные и полезные сведения. Хотелось бы ещё узнать, как компилируются все эти файлы dll для разных версий: "вручную" или этот процесс как-то автоматизирован тоже?

Евгений Хрущ комментирует...

Прочитав "Замечания к исходному коду, опубликованному на хабре" в статье http://bushman-andrey.blogspot.ru/2013/08/net-nanosoft.html решил тоже высказать маленькое замечание к коду в данной статье.
В случае, когда условный блок завершается выходом из функции, слово else использовать не требуется (Resharper его даже помечает как излишнее). Это касается части кода:
if(File.Exists(name)) {
return new FileInfo(name);
}
// Если соответствия не найдено, то продолжаем поиск, последовательно
// проверяя наличие подходящего файла для более ранних версий AutoCAD
else {

Андрей Бушман комментирует...

Я не понял, что подразумевается под "вручную"... В IDE я жму кнопку пересборки решения и на выходе получаю скомпилированные версии плагина под все интересующие меня версии AutoCAD. Автоматом формируется нужная структура каталогов (при необходимости). Т.е. всё делается одним кликом мышки.

Евгений Хрущ комментирует...

Расскажите, пожалуйста, как вы этого добились? Я пишу на c# в VisualStudio 2012 и что бы скомпилировать плагин к автокадам разных версий делаю четыре действия:
1. Выбираю конфигурацию компилирования, в которой задаётся только путь до итоговой dll (остальное для разных версий одинаковое)
2. Использую #ACAD2008 или #ACAD2012 что бы учесть отличия в структуре объектов (их мало, но они есть, например свойство TextStyle у DBText в более поздних версиях называется TextStyleId)
3. Устанавливаю нужную версию .Net Framework (для 2008 использую 3.5, для остальных 4.0)
4. Меняю подключаемые в reference библиотеки на соответствующие нужной версии автокада.
Этот долгий путь компилирования просто выматывает, а как сделать в один щелчёк мыши - не могу найти.

Андрей Бушман комментирует...

Например, "с нуля" делаю так:

1. Создаю пустое решение (solution).

2. В решение добавляю новый проект (project) для создания расширения под AutoCAD 2009 SP3 и выполняю настройку всех его [решения] свойств, для всех нужных мне конфигураций (Release|Debug)) и платформ (AnyCPU или x86|x64).

3. В проекте подключаю все нужные мне ссылки (references) на библиотеки AutoCAD 2009 и, при необходимости, правлю их свойства: например, для некоторых устанавливаю "Copy Local" в значение "False".

4. Пишу программный код добавляя в проект, по мере необходимости, файлы *.CS, *.RESX и т.п.

5. Когда код написан и оттестирован под AutoCAD 2009, я выполняю пункты 2 и 3, но уже для другой версии AutoCAD, например для AutoCAD 2010. Важно[!] в текущем solution создавать и настраивать новые проекты именно "с нуля", т.е. на основе шаблона, а не копировать и переименовывать первый, ранее созданный нами (в этом решении) проект.

6. Подключаю в проект, созданный в п.5 все исходные файлы проекта, созданного в п.2. Важный момент[!]: подключение исходных файлов первого проекта следует выполнять в виде ссылок, т.е. выбирая вариант ADD AS LINK. Добавленные таким способом файлы будут иметь особые иконки в Solution Explorer.

7. Компилирую код проекта AutoCAD 2010. Если код не компилируется, то в файлы исходного кода добавляю необходимые директивы препроцессора (#if\#elseif\#else\#endif), которыми обособляю код, специфичный для рассматриваемой версии AutoCAD. Например, если код специфичен для AutoCAD 2010, то я добавляю AUTOCAD_2010. Если же код специфичен для версий AutoCAD новее, чем 2010, но не работает в версиях 2009 и 2010, то добавляю NEWER_THAN_AUTOCAD_2010.

8. По аналогии выполняю действия для AutoCAD версий 2011-2015.

Т.о. в составе решения определён один проект, в котором находятся непосредственно файлы исходного кода, а так же набор дополнительных проектов (CSPROJ-файлов), каждый из которых настроен под свою версию AutoCAD. Но эти проекты не содержат файлов исходного кода, а вместо этого ссылаются на файлы исходного кода первого проекта. Т.о. изменения, выполненные в файлах одного проекта, автоматом применяются ко всем проектам. Если в проектах открывать файлы исходников двойным кликом по их ссылкам, то высвечивается код именно тех директив, которые специфичны для проекта, в составе которого находится ссылка - это очень удобно.

Затем, одним кликом мышки запуская процесс построения решения (solution), на выходе получаем откомпилированные версии расширений под каждую интересующую нас версию AutoCAD.