Сериализация объектов в MultiCAD.NET. Управление совместимостью чертежей и прокси-объектами
При создании пользовательских объектов на традиционном 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, загружаем вторую версию сборки и открываем первую версию файла. Файл будет успешно открыт и на экране нас ожидает сохраненный в первой версии примитив. Как только будет произведено какое-либо действие, приводящие к перерисовке объекта (например, перемещение) объект будет перерисован в новой версии с учетом работы конструктора сериализации: крестовая метка будет нарисована с окружностью заданного радиуса.Закрываем файл. Загружаем первую версию приложения и открываем вторую версию файла. Файл также успешно загрузится и первоначально будет отрисован объект второй версии, как он был сохранен в файле. После же перерисовки крестовая метка будет изображена в том виде, который был определен в первой версии.
Традиционный подход. Совместимость сверху вниз Механизм VTS, позволяющий обеспечить совместимость снизу вверх, применим далеко не всегда, поскольку не все данные в новых версиях объектов могут быть сочтены несущественными. Для того, чтобы обеспечить совместимость сверху вниз, при создании новой версии объекта необходимо использовать атрибут следующего вида: [CustomEntity (guid, majorVersion, databaseName, localName)] Здесь свойство majorVersion как раз и задает главную версию объекта, в рамках которой будет обеспечиваться полная совместимость. Если новые объекты созданы с majorVersion, значение которого больше предыдущего, то при попытке открытия таких объектов более старой версией кода будет получен код возврата eMakeMeProxy. В этом случае объекты на чертеже будут представлены как прокси-объекты, что исключает возможность их изменения и редактирования.Заключение В этой статье мы рассмотрели пример создания двух версий пользовательского объекта и процесс управления совместимостью между версиями в самом простом случае — при добавления в новой версии дополнительных полей данных. Об обеспечении совместимости версий пользовательских объектов при более серьёзных изменениях, таких как удаление, переименование полей или изменение их типов мы расскажем в одной из следующих статей.