среда, 16 января 2013 г.

DBObject.HandOverTo() - подмена существующего объекта новым, с сохранением ObjectId и Handle оригинала

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

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

Иногда вместо преобразования можно воспользоваться обычной заменой, т.е. удалить существующий объект и на его месте создать новый. Но проблема в том, что новый объект получит иное значение для ObjectId и для Handle. Как следствие - все поля (Fields), ранее ссылавшиеся на некоторые свойства удалённого нами объекта, потеряют связь с источником и в качестве значения будут отображать ######. Кроме того, Если ранее была осуществлена подписка на некоторые события (Events) удаляемого нами объекта, то в случае обычной замены, мы уже не получим ожидаемой реакции.

Для того, чтобы решить обозначенную проблему, следует прибегать к помощи метода DBObject.HandOverTo().

Метод DBObject.HandOverTo() имеет следующую сигнатуру:

   1:  public void HandOverTo(
   2:      [CallerMustClose] DBObject newPointer, 
   3:      [MarshalAs(UnmanagedType.U1)] bool keepXData, 
   4:      [MarshalAs(UnmanagedType.U1)] bool keepExtensionDictionary
   5:  );

Метод DBObject.HandOverTo() предоставляет возможность подменять имеющиеся в базе данных объекты (DBRO - database-resident object), иными объектами, не входящими в состав этой базы данных (NDBRO - non-database-resident object), при этом подменяющему объекту будут назначены ObjectId, Handle и список зарегистрированных событий подменяемого объекта. Пример такой ситуации - когда используется команда _.BREAK для того, чтобы разрезав Circle, преобразовать его в Arc. Arc - это абсолютно другой класс, отличный от Circle, таким образом, в базе данных чертежа, новым объектом Arc нужно заменить уже существующий объект Circle, при этом Arc должны быть назначены Handle и ObjectId, которые до этого имел Circle.

Для того, чтобы использовать метод DBObject.HandOverTo(), тот объект, который будет заменен (Circle в примере выше), должен быть открыт в режиме OpenMode.ForWrite. Метод DBObject.HandOverTo() объекта вызывается с параметром newPointer, указывающим на новый, "не резидентный объект базы данных", который его заменяет (Arc в примере выше).

Параметр keepXData следует назначать true, если xdata должен быть сохранен, или же false - если xdata должен быть выброшен.

Параметр keepExtensionDictionary следует назначать true, если расширенный словарь данных и его содержание должны быть сохранены, или же false - если словарь расширенных данных и его содержание должны быть выброшены.

Внимание! Если в коде не используется OpenCloseTransaction (ниже в коде я покажу оба варианта), то после того, как замещяемый объект (Circle в примере выше) открыт с опцией OpenMode.ForWrite - для него позднее должен быть обязательно вызван метод Dispose(), а для замещающего - метод Close() для того, чтобы зафиксировать изменение и закрыть запись в файле "UNDO.ac$". Обратите внимание на то, что удалять старый объект (Circle в примере выше), вызывая экземплярный метод Erase(), в данном случае не нужно, так как эта операция удаления выполняется в коде метода DBObject.HandOverTo(). Однако очистка памяти от замещаемого объекта (Circle в примере выше) путём вызова его метода Dispose() нужна, т. к. в коде метода HandOverTo() эта очистка не выполняется.

Примечание
Файл UNDO.ac$ находится в каталоге %Temp% и используется в AutoCAD для того, чтобы можно было отменить уже выполненные изменения.
Файл REDO.ac$ находится в каталоге %Temp% и используется в AutoCAD для того, чтобы можно было вернуть обратно уже отменённые изменения.

Внимание!
Метод DBObject.HandOverTo()не разрешён к использованию на объектах, которые являются резидентными объектами транзакции. Если объект, на котором вызывают метод, является резидентным объектом транзакции, то метод DBObject.HandOverTo() не выполнит ожидаемой от него работы. В данном случае, под "резидентным" подразумеваются не только созданные в рамках транзакции объекты, но и все объекты, открытые в этой транзакции.

Пример использования
В качестве примера использования DBObject.HandOverTo() выполним следующие действия (программно):
  1. Создадим окружность
  2. Создадим однострочный текст с полем, связанным со значеним радиуса окружности, созданной в п.1. 
  3. Создадим однострочный текст без поля.
  4. Зарегистрируемся на событие Modified  окружности, созданной в п.1, считывая значение её радиуса и обновляя им содержимое текста, созданного в п.3.
  5. Заменим окружность дугой и проверим, продолжают ли текстовые объекты по прежнему считывать информацию, но уже с дуги, вместо окружности. 
После выполнения указанной в п.5 замены, все текстовые объекты, созданные в п.2 и п.3 должны по прежнему корректно обновляться в случае изменения свойств дуги.

Исходный код примера

   1:  // © Andrey Bushman, 2013
   2:  // Загрузив сборку в AutoCAD, сначала выполните команду AddCircleAndText,
   3:  // создав тем самым окружность и пару текстовых объектов, один из которых содержит поле.
   4:  // Затем запустите команду CircleToArc и смотрите результат преобразования.
   5:   
   6:  using System;
   7:  using cad = Autodesk.AutoCAD.ApplicationServices.Application;
   8:  using AppSrv = Autodesk.AutoCAD.ApplicationServices;
   9:  using DbSrv = Autodesk.AutoCAD.DatabaseServices;
  10:  using EdInp = Autodesk.AutoCAD.EditorInput;
  11:  using Geom = Autodesk.AutoCAD.Geometry;
  12:  using Rtm = Autodesk.AutoCAD.Runtime;
  13:   
  14:  [assembly: Rtm.CommandClass(typeof(AndreyBushman.Samples.TestClass))]
  15:   
  16:  namespace AndreyBushman.Samples {
  17:   
  18:      public sealed class TestClass {
  19:   
  20:          static DbSrv.ObjectId idText = DbSrv.ObjectId.Null;
  21:          static DbSrv.ObjectId idGeom = DbSrv.ObjectId.Null;
  22:   
  23:   
  24:          [Rtm.CommandMethod("AddCircleAndText")]
  25:          public void AddCircleAndText() {
  26:   
  27:              AppSrv.Document doc = cad.DocumentManager.MdiActiveDocument;
  28:              DbSrv.Database db = doc.Database;
  29:              EdInp.Editor ed = doc.Editor;
  30:   
  31:              // Используем транзакцию
  32:              using (DbSrv.Transaction tr = db.TransactionManager.StartTransaction()) {
  33:   
  34:                  // Добавляем окружность
  35:                  DbSrv.Circle circle = new DbSrv.Circle();
  36:                  circle.SetDatabaseDefaults();
  37:                  circle.Center = new Geom.Point3d(0.0, 0.0, 0.0);
  38:                  circle.Radius = 30.0;
  39:   
  40:                  DbSrv.BlockTable bt = tr.GetObject(db.BlockTableId, DbSrv.OpenMode.ForRead)
  41:                      as DbSrv.BlockTable;
  42:   
  43:                  DbSrv.BlockTableRecord ms = tr.GetObject(bt[DbSrv.BlockTableRecord.ModelSpace],
  44:                      DbSrv.OpenMode.ForWrite) as DbSrv.BlockTableRecord;
  45:   
  46:                  ms.AppendEntity(circle);
  47:                  tr.AddNewlyCreatedDBObject(circle, true);
  48:   
  49:                  // Добавляем текст с полем
  50:                  DbSrv.DBText txtFild = new DbSrv.DBText();
  51:                  txtFild.SetDatabaseDefaults();
  52:                  txtFild.Position = new Geom.Point3d(0.0, 40.0, 0.0);
  53:                  txtFild.TextString = @"%<\AcObjProp.16.2 Object(%<\_ObjId " + circle.ObjectId.OldIdPtr
  54:                      + @">%).Radius \f ""%lu2%pr2%ps[, mm]%zs8"">%";
  55:   
  56:                  ms.AppendEntity(txtFild);
  57:                  tr.AddNewlyCreatedDBObject(txtFild, true);
  58:   
  59:                  // Добавляем текст без поля
  60:                  DbSrv.DBText txt2 = new DbSrv.DBText();
  61:                  txt2.SetDatabaseDefaults();
  62:                  txt2.Position = new Geom.Point3d(0.0, 50.0, 0.0);
  63:                  txt2.TextString = circle.Radius.ToString() + " mm";
  64:   
  65:                  ms.AppendEntity(txt2);
  66:                  tr.AddNewlyCreatedDBObject(txt2, true);
  67:   
  68:                  idText = txt2.ObjectId;
  69:                  idGeom = circle.ObjectId;
  70:   
  71:                  circle.Modified += circle_Modified;
  72:   
  73:                  tr.Commit();
  74:              }
  75:          }
  76:   
  77:          void circle_Modified(object sender, EventArgs e) {
  78:   
  79:              if (idText == DbSrv.ObjectId.Null || idText.IsErased || !idText.IsValid)
  80:                  return;
  81:   
  82:              AppSrv.Document doc = cad.DocumentManager.MdiActiveDocument;
  83:              DbSrv.Database db = doc.Database;
  84:              EdInp.Editor ed = doc.Editor;
  85:   
  86:              using (DbSrv.Transaction tr = db.TransactionManager.StartTransaction()) {
  87:                  DbSrv.DBText txt2 = tr.GetObject(idText, DbSrv.OpenMode.ForWrite) as DbSrv.DBText;
  88:   
  89:                  if (sender is DbSrv.Circle) {
  90:                      DbSrv.Circle circle = sender as DbSrv.Circle;
  91:                      txt2.TextString = Math.Round(circle.Radius, 2).ToString() + " mm";
  92:                  }
  93:                  else if (sender is DbSrv.Arc) {
  94:                      DbSrv.Arc arc = sender as DbSrv.Arc;
  95:                      txt2.TextString = Math.Round(arc.Radius, 2).ToString() + " mm";
  96:                  }
  97:                  else {
  98:                      txt2.TextString = "Объект не является окружностью или дугой!";
  99:                  }
 100:                  tr.Commit();
 101:              }
 102:          }
 103:   
 104:          /// <summary>
 105:          /// В этом варианте метода CircleToArc операцию преобразования выполняем с эмуляцией транзакции
 106:          /// </summary>
 107:          [Rtm.CommandMethod("CircleToArc")]
 108:          public void CircleToArc() {
 109:   
 110:              AppSrv.Document doc = cad.DocumentManager.MdiActiveDocument;
 111:              DbSrv.Database db = doc.Database;
 112:              EdInp.Editor ed = doc.Editor;
 113:   
 114:              using (DbSrv.OpenCloseTransaction tr = db.TransactionManager.StartOpenCloseTransaction()) {
 115:   
 116:                  if (idGeom == DbSrv.ObjectId.Null || idGeom.IsErased || !idGeom.IsValid)
 117:                      return;
 118:   
 119:                  DbSrv.Circle circle = tr.GetObject(idGeom, DbSrv.OpenMode.ForRead) as DbSrv.Circle;
 120:   
 121:                  // Создаём дугу
 122:                  DbSrv.Arc arc = new DbSrv.Arc(new Geom.Point3d(0.0, 0.0, 0.0), circle.Radius, 1.117, 3.5605);
 123:                  arc.SetDatabaseDefaults();
 124:   
 125:                  circle.UpgradeOpen();
 126:                  circle.HandOverTo(arc, true, true);
 127:                  Boolean x = circle.IsDisposed; // Здесь получаю false, значит очистить память нужно самому:
 128:                  circle.Dispose();
 129:                  tr.AddNewlyCreatedDBObject(arc, true);
 130:                  tr.Commit();
 131:              }
 132:          }
 133:      }
 134:  }

Обратите внимание на реализацию метода CircleToArc - в нём модификация объектов выполняется в контексте эмуляции транзакции. Этот вариант более предпочтителен, чем редактировать объекты без такой эмуляции, что выглядело бы так (альтернативный вариант реализации метода CircleToArc):

   1:  /// <summary>
   2:  /// В этом варианте показан вызов метода HandOverTo без использования эмуляции транзакции,
   3:  /// т. е. без работы в контексте объекта OpenCloseTransaction.
   4:  /// </summary>
   5:  [Rtm.CommandMethod("CircleToArc")]
   6:  public void CircleToArc() {
   7:   
   8:      AppSrv.Document doc = cad.DocumentManager.MdiActiveDocument;
   9:      DbSrv.Database db = doc.Database;
  10:      EdInp.Editor ed = doc.Editor;
  11:   
  12:      // ВНИМАНИЕ! В данном случае следует использовать именно метод Open, а не GetObject.
  13:      // Метод GetObject следует использовать лишь в рамках эмуляции транзакции (OpenCloseTransaction).
  14:      DbSrv.Circle circle = idGeom.Open(DbSrv.OpenMode.ForRead) as DbSrv.Circle;
  15:      DbSrv.Arc arc = new DbSrv.Arc(new Geom.Point3d(0.0, 0.0, 0.0), circle.Radius, 1.117, 3.5605);
  16:      arc.SetDatabaseDefaults();
  17:      circle.UpgradeOpen();
  18:      circle.HandOverTo(arc, true, true);
  19:      arc.Close();
  20:      Boolean x = circle.IsDisposed; // Здесь получаю false, значит очистить память нужно самому:
  21:      circle.Dispose();
  22:   
  23:      // ВНИМАНИЕ! Не следует вызывать метод circle.Erase(), т. к. удаление этого объекта выполняется 
  24:      // в коде метода HandOverTo.
  25:  }

Загрузив в AutoCAD данный код и запустив на исполнение команду AddCircleAndText, мы получим такой результат:


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


Теперь запустим команду CircleToArc. Результат будет выглядеть так:


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




Комментариев нет: