Как выполнять транзакции в AutoCad

Обновлено: 03.07.2024

Блог для разработчиков, использующих платформы Autodesk, особенно AutoCAD и Forge. Особое внимание уделяется AR/VR и IoT.

8 апреля 2013 г.

В конце прошлой недели я получил интересное электронное письмо от Бруно Сабойя, у которого возникли некоторые проблемы с производительностью кода, который он написал для возврата всех объектов определенного типа — в его случае линий — из пространства модели активный рисунок.

Мы обменялись несколькими электронными письмами на эту тему, и Бруно любезно разрешил мне опубликовать его код и мои предложения по изменениям. В сегодняшнем посте мы рассмотрим как код, так и мои предложения, а в следующем посте (по крайней мере, в этой мини-серии) мы рассмотрим простую среду выполнения, которая позволяет вам записывать грубую производительность. тайминги различных реализаций команд.

Вот исходный код Бруно, который мы подробно рассмотрим.

1 общедоступный статический IEnumerable < Line >GetAllLines(база данных db)

5 Приложение .DocumentManager.MdiActiveDocument.LockDocument()

8 с использованием ( var tr = db.TransactionManager.StartTransaction())

11 tr.GetObject(db.BlockTableId, OpenMode .ForRead)

12 как BlockTable;

15 бит[ BlockTableRecord .ModelSpace],

16 OpenMode .ForRead

17 ) как BlockTableRecord ;

19 foreach ( var obj в btr)

22 tr.GetObject(obj, OpenMode .ForRead) as Line ;

24 если (строка != null )

26 строка возврата доходности;

Первое, на что следует обратить внимание, это то, что функция возвращает IEnumerable. Это «ленивый» тип, который на самом деле просто предоставляет перечислитель обратно вызывающей функции, которую затем можно использовать для пошагового прохождения набора результатов, и это может повлиять на логику кода.

Пропустив вперед, вы увидите в строке 26, что мы «передаем» результаты вызывающей функции. Что на самом деле происходит, так это то, что вызывающая функция, проходя код, по существу выполняет цикл только одну итерацию за раз. Причина, по которой IEnumerable ленив — а во многих ситуациях поэтому более эффективен — заключается в том, что мы выполняем «когда нужно», а не «в случае необходимости». Это очень хорошая стратегия, если вы хотите передать назад то, что может быть очень длинным — возможно, даже бесконечным — набором данных, например, постраничным просмотром результатов поиска. Это также очень хороший способ распределить нагрузку выполнения сложных вычислений на все время выполнения цикла. Если вызывающая функция на самом деле захочет перечислить весь список — что вполне вероятно в данном случае — тогда может оказаться, что лучшей стратегией будет возврат List<>. В этой ветке содержится полезная справочная информация по этому вопросу.

В этом случае код возвращает IEnumerable of Lines. Я настоятельно рекомендую не этого делать: Линии были открыты текущей транзакцией и станут недействительными, как только транзакция завершится. Поскольку на самом деле мы получаем результаты лениво — и, следовательно, вызывающая функция получает результаты по одному, и поэтому любая функция, которую вы вызываете сразу после этого, по-прежнему будет иметь транзакцию в области действия — тогда эта должна быть в порядке в этом конкретном контексте. Но код, написанный таким образом, в конечном итоге оказывается гораздо менее пригодным для компоновки и повторного использования: гораздо безопаснее, чтобы функция возвращала набор идентификаторов ObjectId, которые можно открывать и запрашивать для получения информации при необходимости.

Для этого также существуют разные стратегии: вы можете передать объект Transaction в функцию, чтобы убедиться, что он четко входит в область действия, или вы можете передать лямбда-выражение для оценки и возврата информации, которую необходимо собрать для каждого Линия. В этом случае я собираюсь вернуть IEnumerable и предоставить вызывающей функции (повторно) открывать объекты по мере необходимости.

Прямо сейчас — далее к строкам 3–6: я бы предпочел избегать явной блокировки документа в функции более низкого уровня. Это делается автоматически для активного документа во всех командах, которые не зарегистрированы как команды «контекста сеанса» (т. е. любая команда «контекста документа» имеет неявную блокировку активного документа). Я бы предположил, что любая команда контекста сеанса, вызывающая эту функцию, заранее заблокировала бы документ вручную. Для ясности: это не окажет заметного влияния на производительность, но будет чище удалить его.

В строке 8: мы используем традиционную транзакцию (запускаемую с помощью StartTransaction()) для получения информации, необходимой для содержимого пространства модели. Стандартные транзакции предоставляют некоторые очень полезные возможности — например, они могут быть вложены друг в друга и могут быть прерваны для отката любых изменений, которые были сделаны в рамках этой конкретной транзакции, — но они сопряжены с некоторыми накладными расходами на производительность. Поскольку мы не используем какие-либо расширенные возможности, мы можем повысить производительность, используя вместо этого OpenCloseTransaction (запускаемый через StartOpenCloseTransaction()).См. этот пост в AutoCAD DevBlog для более подробного обсуждения преимуществ этого изменения в производительности.

Также следует отметить — и это может иметь большое значение — что вы всегда должны вызывать Commit() при успешной транзакции, даже если она номинально доступна только для чтения. Если Commit() не вызывается, транзакция будет прервана, и в зависимости от того, что вы делаете, это может привести к снижению производительности. Этот вызов будет добавлен между строками 28 и 29 непосредственно перед тем, как транзакция выйдет из области действия.

Строки 10–17 в порядке, хотя для полноты картины я предлагаю внести несколько незначительных изменений. Я очень редко беспокоюсь об этом, но вместо того, чтобы открывать BlockTable для получения из него ObjectId пространства модели, также можно использовать SymbolUtilityServices.GetBlockModelSpaceId(), передавая объект базы данных. Это вызывает собственный код, чтобы получить ObjectId пространства модели для этой базы данных, что может сократить количество вызовов виртуальных функций здесь и там. Опять же, вероятно, изменение не оказывает заметного влияния, но эй!

Другим несколько педантичным изменением будет использование приведения вместо использования ключевого слова as (которое проверяет информацию о типе среды выполнения перед выполнением любого преобразования ссылки) в строках 12 и 17. Поскольку на самом деле мы не проверяем указатель, возвращаемый "as", чтобы увидеть, является ли оно нулевым, мы также можем выполнить традиционное приведение (что позволит избежать одной или двух проверок).

В строках 21 и 22 мы открываем объект, чтобы проверить, является ли он линией или нет. Я стараюсь избегать этого, если только специально не открываю объект по другой причине (что, по общему признанию, хочет сделать Бруно, поскольку он хочет собрать некоторую информацию о каждой из линий в пространстве модели). Лично я предпочитаю изменить эту строку, чтобы проверить свойство ObjectClass в ObjectId, чтобы увидеть, является ли оно IsDerivedFrom() классом Line. Это позволяет избежать открытия объекта, что, несомненно, улучшит производительность, хотя и перекладывает ответственность на вызывающую функцию, чтобы продолжить и открыть его по мере необходимости.

Итак, вот моя версия функции. Помните, что это не так много, как предыдущая версия, которая оставляет линию открытой, и поэтому любое сравнение производительности, конечно же, будет сравнимым с апельсинами:

12.01.2012

Вот типичное использование для такой ситуации:

использование (Transaction tr = db.TransactionManager.StartTransaction())
<
BlockTable bt = tr.GetObject(db.BlockTableId, OpenMode.ForRead) as BlockTable;
BlockTableRecord btr = tr.GetObject(bt[BlockTableRecord.ModelSpace], OpenMode.ForWrite) as BlockTableRecord;

использование (Circle cir = new Circle())
<
cir.Center = new Point3d(10.0, 0.0, 0.0);
круг.Радиус = 5,0;

btr.AppendEntity(цир);
tr.AddNewlyCreatedDBObject(cir, true);
>

Предположим, мы хотим создать тысячи сущностей (например, кругов) в одном и том же пространстве модели одной и той же базы данных, что нам делать с транзакциями?

В большинстве случаев люди уже создали метод для создания такой сущности (обведите здесь):

public static void CreateCircle(Point3d center, double radius)
<
База данных db = HostApplicationServices.WorkingDatabase;
использование (Transaction tr = db.TransactionManager.StartTransaction())
<
BlockTable bt = tr.GetObject(db.BlockTableId, OpenMode.ForRead) as BlockTable;
BlockTableRecord btr = tr.GetObject(bt[BlockTableRecord.ModelSpace], OpenMode.ForWrite) as BlockTableRecord;

использование (Circle cir = new Circle())
<
cir.Center = center;
cir.Radius = радиус;

btr.AppendEntity(цир);
tr.AddNewlyCreatedDBObject(cir, true);
>

Как можно заметить, создание круга включено в транзакцию. Тогда, вероятно, кажется естественным повторно использовать приведенный выше код и добавить еще несколько для создания тысяч кругов:

[CommandMethod("CreateCircles2")]
public static void CreateCircles2_Method()
<
Editor ed = AcadApplication.DocumentManager.MdiActiveDocument.Editor;

DateTime begin = DateTime.Now;
for (int index = 0; index < 5000; index++)
<
Point3d cen = new Point3d(index / 10.0, 20.0, 0.0);
CreateCircle(cen, 5.0);
>

TimeSpan истек = DateTime.Now.Subtract(begin);
ed.WriteMessage("Время, прошедшее в CreateCircles2: \n", elapsed.TotalMilliseconds);
>

Выглядит довольно структурно, многоразово и удобно для чтения, не так ли?

Конечно, так оно и есть. Тем не менее, это снижает производительность, которая во многих случаях не является главной проблемой, но, безусловно, является проблемой во многих других случаях.

Давайте рассмотрим другой способ сделать то же самое:

[CommandMethod("CreateCircles1")]
public static void CreateCircles1_Method()
<
База данных db = HostApplicationServices.WorkingDatabase;
Editor ed = AcadApplication.DocumentManager.MdiActiveDocument.Editor;

Начало даты и времени = Дата и время.Сейчас;
использование (Transaction tr = db.TransactionManager.StartTransaction())
<
BlockTable bt = tr.GetObject(db.BlockTableId, OpenMode.ForRead) as BlockTable;
BlockTableRecord btr = tr.GetObject(bt[BlockTableRecord.ModelSpace], OpenMode.ForWrite) as BlockTableRecord;

for (int index = 0; index < 5000; index++)
<
использование (Circle cir = new Circle())
<
cir .Center = новый Point3d (индекс / 10,0, 0,0, 0,0);
круг.Радиус = 5,0;

btr.AppendEntity(цир);
tr.AddNewlyCreatedDBObject(cir, true);
>
>

TimeSpan истек = DateTime.Now.Subtract(begin);
ed.WriteMessage("Время, прошедшее в CreateCircles1: \n", elapsed.TotalMilliseconds);
>

Как можно заметить, на этот раз все создания кругов объединены в одну транзакцию. За исключением этого пункта, все остальные операции такие же.

Что в этом хорошего?

Это повышает производительность. Давайте запустим две команды в одной среде и сравним затраченное время, чтобы получить четкое представление:

Command: CreateCircles1
Прошло время в CreateCircles1: 577,201
Command: CREATECIRCLES2
Прошло время в CreateCircles2: 1154,402

Как видно, второй команде потребовалось вдвое больше времени, чтобы сделать то же самое, что и первой.

Это так важно, что здесь тратится на полсекунды больше?

Вероятно, нет в этом простом примере, но в большинстве случаев это, вероятно, будет, учитывая тот факт, что между ними может быть задействовано множество других операций, таких как считывание идентификаторов слоев и типов линий и назначение их кругам. при необходимости транзакции могут быть вложенными и т.п.

Поэтому рекомендуется использовать как можно меньше транзакций и объединять как можно больше операций в одну транзакцию, поскольку транзакции обходятся дорого, особенно вложенные, если производительность является наиболее важным фактором.

Конечно, это означает необходимость дополнительной работы по кодированию и улучшения дизайна. Затем здесь предлагается еще одна хорошая практика: всегда объединять вспомогательные методы создания сущностей и следить за тем, чтобы один оборачивал все в полную транзакцию, а другой принимал дополнительный указатель транзакции. Вот улучшенная версия первой команды:

[CommandMethod("CreateCircles1_New")]
public static void CreateCircles1_New_Method()
<
База данных db = HostApplicationServices.WorkingDatabase;
Editor ed = AcadApplication.DocumentManager.MdiActiveDocument.Editor;

DateTime begin = DateTime.Now;
используя (Transaction tr = db.TransactionManager.StartTransaction())
<
for (int index = 0; index < 5000; index++)
<
CreateCircle( tr, новый Point3d (индекс / 10.0, 0.0, 0.0), 5.0);
>

TimeSpan истек = DateTime.Now.Subtract(begin);
ed.WriteMessage("Время, прошедшее в CreateCircles1_New: \n", elapsed.TotalMilliseconds);
>

public static void CreateCircle(транзакция tr, центр Point3d, двойной радиус)
<
Database db = HostApplicationServices.WorkingDatabase;

BlockTable bt = tr.GetObject(db.BlockTableId, OpenMode.ForRead) as BlockTable;
BlockTableRecord btr = tr.GetObject(bt[BlockTableRecord.ModelSpace], OpenMode.ForWrite) as BlockTableRecord;

использование (Circle cir = new Circle())
<
cir.Center = center;
cir.Radius = радиус;

btr.AppendEntity(цир);
tr.AddNewlyCreatedDBObject(cir, true);
>
>

Если мы запустим новую команду, мы обнаружим, что она имеет такую ​​же хорошую производительность, как и старая:

Команда: CreateCircles1_New
Прошло время в CreateCircles1_New: 577,201

Кроме того, мы можем повторно использовать код для переопределения вспомогательного метода, у которого нет аргумента Transaction:

Таким образом, все цели могут быть достигнуты в балансе. Код по-прежнему выглядит достаточно лаконичным, удобочитаемым, удобным для сопровождения, а также учитываются структура и производительность. Конечно, те же самые идеи можно распространить на другие сценарии, такие как неактуальные базы данных, базы данных XREF и даже длительные транзакции.

Для удобства ниже добавлен полный код:

используя AcadApplication = Autodesk.AutoCAD.ApplicationServices.Application;
используя AcadDocument = Autodesk.AutoCAD.ApplicationServices.Document;
используя AcadWindows = Autodesk.AutoCAD.Windows;

namespace AcadNetAddinWizard_Namespace
<
общедоступный класс TestCommands
<
[CommandMethod("CreateCircles2")]
public static void CreateCircles2_Method()
< < br />Редактор ed = AcadApplication.DocumentManager.MdiActiveDocument.Editor;

DateTime begin = DateTime.Now;
for (int index = 0; index < 5000; index++)
<
Point3d cen = new Point3d(index / 10.0, 20.0, 0.0);
CreateCircle(cen, 5.0);
>

TimeSpan истек = DateTime.Now.Subtract(begin);
ed.WriteMessage("Время, прошедшее в CreateCircles2: \n", elapsed.TotalMilliseconds);
>

public static void CreateCircle(Point3d center, double radius)
<
База данных db = HostApplicationServices.WorkingDatabase;
использование (Transaction tr = db.TransactionManager.StartTransaction())
<
BlockTable bt = tr.GetObject(db.BlockTableId, OpenMode.ForRead) as BlockTable;
BlockTableRecord btr = tr.GetObject(bt[BlockTableRecord.ModelSpace], OpenMode.ForWrite) as BlockTableRecord;

использование (Circle cir = new Circle())
<
cir.Center = center;
cir.Radius = радиус;

btr.AppendEntity(цир);
tr.AddNewlyCreatedDBObject(cir, true);
>

[CommandMethod("CreateCircles1")]
public static void CreateCircles1_Method()
<
База данных db = HostApplicationServices.WorkingDatabase;
Editor ed = AcadApplication.DocumentManager.MdiActiveDocument.Editor;

DateTime begin = DateTime.Now;
использование (Transaction tr = db.TransactionManager.StartTransaction())
<
BlockTable bt = tr.GetObject(db.BlockTableId, OpenMode.ForRead) as BlockTable;
BlockTableRecord btr = tr.GetObject(bt[BlockTableRecord.ModelSpace], OpenMode.ForWrite) as BlockTableRecord;

for (int index = 0; index < 5000; index++)
<
using (Circle cir = new Circle())
<
cir.Center = new Point3d (индекс / 10,0, 0,0, 0,0);
круг.Радиус = 5,0;

btr.AppendEntity(цир);
tr.AddNewlyCreatedDBObject(cir, true);
>
>

TimeSpan истек = DateTime.Now.Subtract(begin);
ed.WriteMessage("Время, прошедшее в CreateCircles1: \n", elapsed.TotalMilliseconds);
>

[CommandMethod("CreateCircles1_New")]
public static void CreateCircles1_New_Method()
<
База данных db = HostApplicationServices.WorkingDatabase;
Editor ed = AcadApplication.DocumentManager.MdiActiveDocument.Editor;

DateTime begin = DateTime.Now;
используя (Transaction tr = db.TransactionManager.StartTransaction())
<
for (int index = 0; index < 5000; index++)
<
CreateCircle( tr, новый Point3d (индекс / 10.0, 0.0, 0.0), 5.0);
>

TimeSpan истек = DateTime.Now.Subtract(begin);
ed.WriteMessage("Время, прошедшее в CreateCircles1_New: \n", elapsed.TotalMilliseconds);
>

public static void CreateCircle(транзакция tr, центр Point3d, двойной радиус)
<
Database db = HostApplicationServices.WorkingDatabase;

BlockTable bt = tr.GetObject(db.BlockTableId, OpenMode.ForRead) as BlockTable;
BlockTableRecord btr = tr.GetObject(bt[BlockTableRecord.ModelSpace], OpenMode.ForWrite) as BlockTableRecord;

использование (Circle cir = new Circle())
<
cir.Center = center;
cir.Radius = радиус;

btr.AppendEntity(цир);
tr.AddNewlyCreatedDBObject(cir, true);
>
>

Наслаждайтесь! В будущем будет создано и продемонстрировано больше советов по кодированию. Пожалуйста, следите за обновлениями.

Блог для разработчиков, использующих платформы Autodesk, особенно AutoCAD и Forge. Особое внимание уделяется AR/VR и IoT.

26 января 2009 г.

Я получил этот вопрос по электронной почте на прошлой неделе:

Необходимо ли когда-нибудь использовать более одной транзакции для каждой программы?

Простой ответ заключается в том, что вам в основном нужна только одна активная транзакция для каждой команды: вы не должны оставлять активную транзакцию вне команды, так как это может вызвать проблемы в какой-то момент, а в вашей собственной команде одна транзакция обычно достаточно, чтобы делать то, что вы хотите.

Тем не менее, механизм транзакций в AutoCAD имеет довольно интересные возможности вложения, которые делают его очень гибким и отличным способом управления наборами операций базы данных и их отката, если они больше не нужны.

В этом посте мы рассмотрим код, который использует несколько вложенных транзакций и позволяет пользователю выбирать, зафиксировать или отменить каждую из них.

Вот что говорится в Руководстве разработчика ObjectARX о «вложенных транзакциях»:

Транзакции могут быть вложенными, то есть вы можете начать одну транзакцию внутри другой и завершить или прервать последнюю транзакцию. Менеджер транзакций поддерживает транзакции в стеке, причем самая последняя транзакция находится наверху стека. Когда вы запускаете новую транзакцию с помощью AcTransactionManager::startTransaction(), новая транзакция добавляется на вершину стека и возвращается указатель на нее (экземпляр AcTransaction). Когда кто-то вызывает AcTransactionManager::endTransaction() или AcTransactionManager::abortTransaction(), транзакция в верхней части стека завершается или прерывается.

Когда указатели объектов получены из идентификаторы объектов, они всегда связаны с самой последней транзакцией. Вы можете получить последнюю транзакцию, используя AcTransactionManager::topTransaction(), а затем использовать AcTransaction::getObject() или AcTransactionManager::getObject(), чтобы получить указатель на объект.Менеджер транзакций автоматически связывает полученные указатели объектов с последней транзакцией. Вы можете использовать AcTransaction::getObject() только с самой последней транзакцией.

При запуске вложенных транзакций указатели объектов, полученные во внешних транзакциях, также доступны для операции. в самой внутренней транзакции. Если недавняя транзакция прервана, все операции, выполненные над всеми объектами (связанными либо с этой транзакцией, либо с содержащимися) с начала последней транзакции, отменяются, и объекты возвращаются к состоянию в начале недавней транзакции. сделка. Указатели объектов, полученные в последней транзакции, перестают быть действительными после ее прерывания.

Если самая внутренняя транзакция успешно завершается вызовом AcTransactionManager::endTransaction(), объекты, указатели которых полученные в этой транзакции, становятся связанными с содержащей ее транзакцией и становятся доступными для работы. Этот процесс продолжается до тех пор, пока не завершится самая внешняя (первая) транзакция, после чего фиксируются модификации всех объектов. Если самая внешняя транзакция прерывается, все операции над всеми объектами отменяются и ничего не фиксируется.

Вот идея. у нас будет внешняя транзакция, внутри которой мы создадим сетку из закрашенных кругов (на самом деле штриховок). Затем мы собираемся вложить ряд транзакций (пока только на один уровень в глубину, хотя ничто не мешает нам вкладываться глубже), которые изменяют цвет различных элементов в сетке.

Вот еще один иерархический взгляд на транзакцию:

  • [Outer] Создать сетку из закрашенных кругов
    • [Вложенный 1] Сделайте все эти круги красными.
    • [Вложенный 2] Сделать чередующиеся строки желтыми
    • [Вложенный 3] Нарисуйте пурпурную синусоиду на сетке

    В конце каждой транзакции пользователю будет предоставлен выбор: зафиксировать или отменить ее.

    Итак, я хотел бы узнать ваше мнение, можно ли пренебречь советом, данным Autodesk, и какова будет цена - утечки памяти?

    Комментарии

    Daniel_Marcotte

    со строкой tr.AddNewlyCreatedDBObject(circle, true);

    Вы передаете управление памятью своего круга транзакции. так что не вызывайте circle.Dispose(); транзакция сделает это за вас

    Ура!

    Керри Браун

    >>> Вы используете метод Dispose или оператор Using, чтобы сообщить, когда объект готов к сборке мусора. Оператор Using в большинстве случаев является предпочтительным методом, поскольку он делает правильные вызовы для закрытия и удаления объекта, когда он больше не нужен.

    Не могли бы вы проверить прикрепленный файл PDF. Слева приведены значения свойства AutoDelete, взятые из BricsCAD, а справа — из AutoCAD (при запуске кода в BricsCAD/AutoCAD).

    Кажется, между ними есть некоторые несоответствия. Не могли бы вы объяснить мне, что это значит и что я должен иметь в виду при написании кода, совместимого как с ACAD, так и с BCAD. Меня беспокоит возможность уничтожения объекта.

    Не могли бы вы объяснить мне, что это значит и что я должен иметь в виду при написании кода, совместимого как с ACAD, так и с BCAD. Меня беспокоит возможность уничтожения объекта.

    Я не читал ссылки, которые вы разместили, но я думаю, что теоретически вы должны просто всегда вызывать Dispose() (предпочтительно с помощью шаблона 'using()<>') для любого объекта, который реализует IDisposable. Другими словами, базовая реализация должна правильно уничтожать неуправляемые (и управляемые) объекты. Конечно, в реализации BricsCAD могут быть ошибки, но также возможно, что различия обусловлены конструкцией. По крайней мере теоретически, я не думаю, что различия должны повлиять на ваш код.

    Daniel_Marcotte

    Попробуйте использовать транзакцию для управления объектами базы данных (открытие/закрытие/удаление). Если вы просто открываете объекты базы данных для чтения, используйте легковесный OpenCloseTransaction. Каждый элемент, открытый транзакцией или добавленный к ней, удаляется при удалении транзакции.

    Как упоминалось выше, вам не следует вызывать dispose для объекта, принадлежащего транзакции, иначе вы, скорее всего, получите исключение.

    Не используйте объект, принадлежащий транзакции, за рамками указанной транзакции, AutoCAD может позволить объекту оставаться открытым для чтения, BricsCAD более строг.

    Не беспокойтесь об этом слишком сильно, у каждого объекта есть финализатор, и сборщик мусора рано или поздно сделает свою работу.Пример: ResultBuffer реализует IDisposable, но это не конец света, если вы его не утилизируете.

    Другие объекты реализуют IDisposable, но вызов dispose ничего не делает, например класс Document.

    Просто придерживайтесь шаблона транзакции, все будет в порядке.

    Кроме того, при отладке, если что-то остается открытым дольше, чем должно быть, вы получите сообщение «Забыли вызвать Dispose?» предупреждение в окне вывода VS.

    размышления о программировании и других не столь важных вещах

    Блог Вольфганга Тертинека

    Ну, тут много чего происходит. На самом деле это означает, что задействовано много стандартного кода, с которым мы на самом деле не хотим иметь дело. Кроме того, необходимо иметь много явных знаний о структуре API, что может быть неочевидно для новичка:

    1. Мы должны начать транзакцию, и в конце она должна быть удалена (строка 9)
    2. Слои хранятся в таблице типа LayerTable (строка 12).
    3. У объекта базы данных есть свойство LayerTableID, которое является идентификатором интересующей нас таблицы (строка 13)
    4. Мы получаем объект LayerTable из транзакции через GetObject, и мы должны соответствующим образом преобразовать его (строки 13 и 14)
    5. Нам нужно перебрать таблицу слоев, чтобы получить идентификаторы отдельных слоев (строка 18).
    6. Объекты слоя имеют тип «LayerTableRecord» (строка 20)
    7. Мы получаем объекты LayerTableRecord из транзакции через GetObject и должны соответствующим образом преобразовать их (строки 21 и 22)

    Взгляд разработчика надстройки

    С точки зрения разработчика надстройки AutoCAD, действительно ли мы хотим заботиться обо всем этом? В листинге 1 мы на самом деле хотим каким-то образом получить объекты слоя и отобразить их имена. Итак, поскольку мы имеем дело с набором слоев, было бы интересно найти способ использовать реализацию IEnumerable, чтобы избавиться от кода, специфичного для транзакции и базы данных, и «скрыть» его от клиентского кода.

    Как мы можем это сделать? Начнем с простого: мы определяем статический класс LayerHelper, который имеет единственный метод GetLayers, возвращающий IEnumerable:

    Хорошо, очень простой интерфейс. Сигнатура GetLayers() уже сообщает нам, что мы получаем, перечислимое количество LayerTableRecords. Таким образом, нам не нужно иметь дело с идентификаторами, мы просто получаем объекты слоя. Теперь нам нужно найти способ вернуть все слои в базе данных чертежей. У нас уже есть этот код в листинге 1. Итак, для начала давайте скопируем и вставим код примера в наш метод GetLayer:

    Выглядит хорошо. Основная модификация, которую мы сделали, заключается в том, что мы удалили часть, где мы собираем имена слоев, и вместо этого получаем объекты LayerTableRecord. Обработка транзакций и идентификаторы скрыты в методе GetLayers. Итак, если мы хотим отобразить имена слоев, как в листинге 1, мы можем использовать наш вспомогательный метод следующим образом:

    Выглядит круто! Наш клиентский код теперь имеет дело только с нашей бизнес-логикой (сбором имен слоев и их отображением). У нас также есть единая точка входа, класс LayerHelper, и метод GetLayers дает нам то, что мы на самом деле хотим, весь объект слоя. А тяжелая работа скрыта в методе GetLayers.

    Есть ли подвох?

    Это слишком круто! Но у большинства крутых вещей есть подвох, значит, где-то он есть, верно? Ну да. Есть подвох. Проблема в том, что мы не должны использовать объекты AutoCAD после удаления транзакции, с помощью которой они были созданы. Это не проблема в листинге 4, но в целом наша реализация GetLayers() ошибочна. Давайте посмотрим на другой пример:

    Это почти то же самое, что код в листинге 4, но проблема в строке 6. ToList() возвращает все объекты слоя, и сразу после этого транзакция уничтожается. Итак, в строке 9 мы используем объекты, доступ к которым небезопасен. Получение свойства Name в листинге 4 работает, но нам не следует этого делать. Мы подробно рассмотрим всю эту проблему в следующем посте. А пока давайте просто исправим проблему (чтобы не оставлять сообщение с кодом, который может не работать).

    Мы добавляем базу данных и параметр транзакции в наш метод GetLayers:

    К сожалению, наш клиентский код стал менее чистым. Нам по-прежнему не нужно иметь дело с идентификаторами и нам не нужно писать код для извлечения объектов из базы данных. Но транзакция вернулась в наш клиентский код. С другой стороны, код по-прежнему лучше, чем в листинге 1, и теперь мы можем безопасно использовать ToList():

    Теперь мы можем использовать запросы LINQ на наших слоях. Давайте отобразим все имена слоев, которые начинаются с заданного префикса, и отсортируем их по алфавиту:

    В следующем посте мы подробно рассмотрим описанную здесь ошибку и посмотрим, как правильно обрабатывать транзакции и их объекты.

    Читайте также: