При создании пользовательских объектов на традиционном C++ API (NRX в nanoCAD, ObjectARX в AutoCAD) для обеспечения сохранения объектов и чтения их из файла чертежа необходимо в явном виде описывать запись (сериализацию) и чтение (десериализацию) каждого поля. В MultiCAD.NET API применён более привычный .NET разработчикам описательный подход, в основе которого лежит стандартная .NET сериализация.
Применение сериализации, нечувствительной к версии объектов (Version Tolerance Serialization), предоставляет разработчикам более гибкий механизм управления совместимостью объектов разных версий, чем существующий в традиционном C++ API, где предусмотрено чтение предыдущих версий, но чтение файлов «из будущего» невозможно.
В MultiCAD.NET при описании новых версий объектов можно указать, что вновь добавленные поля необязательны, и тогда чертёж, сохранённый в формате новой версии приложения, прочтётся и в предыдущей версии. Разумеется, без изменений остался и традиционный подход, приводящий к созданию прокси объектов (кешированной графики объектов) при загрузке чертежа в предыдущую версию приложения.
Под катом мы обсудим, как достичь совместимости двух версий объекта, а также, как обеспечить традиционный уровень совместимости, когда новые версии приложения читают старые чертежи, но не наоборот.
Давайте рассмотрим как MultiCAD.NET использует оба этих подхода (VTS и механизм proxy-объектов) для организации управления совместимостью объектов. В качестве наглядного примера рассмотрим две версии пользовательского объекта «Крестовая метка».
Создание класса для экспериментов
Об основах создания пользовательских примитивов в MultiCAD.NET мы рассказывали в одной из прошлых статей, поэтому не будем подробно на этом останавливаться, а сразу перейдём к описанию пользовательского примитива «Крестовая метка»:
[CustomEntity("1C925FA1-842B-49CD-924F-4ABF9717DB62", 1, "Crossmark", "Crossmark Sample Entity")]
[Serializable]
public class CrossMark : McCustomBase
{
private Point3d pnt1;
private Point3d pnt2;
private Point3d pnt3;
private Point3d pnt4;
}
Полный код приложения доступен здесь. После компиляции приложения и запуска его командой «crossmark» по результату пользовательского ввода на чертеж будет добавлен следующий примитив:
Сохраните полученный .dwg-файл. Теперь несколько изменим наш класс, добавив в него дополнительную функциональность.
Вторая версия класса и обеспечение межверсионной совместимости объектов
Во второй версии класса мы изменим геометрию нашего примитива, добавив окружность в центр крестовой метки. В качестве дополнительного поля добавим радиус этой окружности, снабдив его атрибутом
[OptionalField]
, чтобы отметить его, как необязательное в случае десериализации более ранней версии объекта. Укажем также и номер новой версии, используюя свойство VersionAdded
:[OptionalField(VersionAdded = 2)]
private double radius = -1;
Инициализируем новую переменную в конструкторе и добавим общедоступное свойство для получения доступа к этому полю:
public CrossMark()
{
...
radius = 10;
}
[Category("Circular component")]
[DisplayName("Circle radius")]
[Description("Radius of circular component of the cross mark")]
public double Radius
{
get
{
return radius;
}
set
{
radius = value;
}
}
Для того, чтобы проинициировать добавленнное поле после десериализации объекта правильным значением, будет использоваться метод
OnDeserialized
с атрибутом [OnDeserialized]
:[OnDeserialized]
private void OnDeserialized(StreamingContext context)
{
if (radius == -1)
{
radius = 10;
}
}
Теперь приложения, работающие с новыми версиями типа, могут также принимать объекты более старых версий. Кроме этого, VTS также решает проблему обратной совместимости: приложения, работающие со старыми версиями объектов могут принимать и новые версии: «лишние» поля при этом просто игнорируются и десериализация проходит успешно.
Таким образом, мы обеспечили полную совместимость версий нашего класса. Осталось только добавить отрисовку окружности в метод
OnDraw()
:public override void OnDraw(GeometryBuilder dc)
{
...
dc.DrawCircle(new Point3d(pnt1.X + 25, pnt1.Y, pnt1.Z), radius);
}
Полный код второй версии приложения также доступен в архиве. Компилируем проект, запускаем nanoCAD, загружаем построенную сборку и запускаем новую версию приложения все той же командой «crossmark». После указания точки вставки получаем .dwg файл с обновленной версией нашего примитива:
Так же сохраняем полученный файл.
Результаты
А теперь убедимся в полной совместимости обеих версий.
Запускаем nanoCAD, загружаем вторую версию сборки и открываем первую версию файла. Файл будет успешно открыт и на экране нас ожидает сохраненный в первой версии примитив. Как только будет произведено какое-либо действие, приводящие к перерисовке объекта (например, перемещение) объект будет перерисован в новой версии с учетом работы конструктора сериализации: крестовая метка будет нарисована с окружностью заданного радиуса. Для этого также может быть использована команда REGENOBJ.
Закрываем файл. Загружаем первую версию приложения и открываем вторую версию файла. Файл также успешно загрузится и первоначально будет отрисован объект второй версии, как он был сохранен в файле. После же перерисовки крестовая метка будет изображена в том виде, который был определен в первой версии.
Традиционный подход. Совместимость сверху вниз
Механизм VTS, позволяющий обеспечить совместимость снизу вверх, применим далеко не всегда, поскольку не все данные в новых версиях объектов могут быть сочтены несущественными. Для того, чтобы обеспечить совместимость сверху вниз, при создании новой версии объекта необходимо использовать атрибут следующего вида:
[CustomEntity(guid, majorVersion, databaseName, localName)]
Здесь свойство
majorVersion
как раз и задает главную версию объекта, в рамках которой будет обеспечиваться полная совместимость. Если новые объекты созданы с majorVersion
, значение которого больше предыдущего, то при попытке открытия таких объектов более старой версией кода будет получен код возврата eMakeMeProxy
. В этом случае объекты на чертеже будут представлены как прокси-объекты, что исключает возможность их изменения и редактирования.Заключение
В этой статье мы рассмотрели пример создания двух версий пользовательского объекта и процесс управления совместимостью между версиями в самом простом случае — при добавления в новой версии дополнительных полей данных. Об обеспечении совместимости версий пользовательских объектов при более серьёзных изменениях, таких как удаление, переименование полей или изменение их типов мы расскажем в одной из следующих статей.
Обсуждение статьи доступно также и на нашем форуме: forum.nanocad.ru/index.php?showtopic=6515.
Перевод статьи на английский: Serializing objects in MultiCAD.NET. Managing drawings compatibility and proxy objects.