logo-beycons
  • Product
    Plugins
    Download
  • Service
    Courses
    Outsource
    Structural Design
  • Community
    Forums
    Blogs
  • User
    Register
    Sign In

Detail Blog

Nhu.Truong
Nhu.Truong
8Blogs
3Followers
950Views
0Like
Created on 21/04/2024
Category - AutoCAD API

How to store complex data in AutoCAD?

In some situations, we may need to attach additional data to an AutoCAD document for the purpose of identification, storage, and facilitating easier data management. So, how do we store the data?

Issue

For example, suppose we want to know which add-in version the .dwg file has run with, or which objects in AutoCAD were created by the add-in... If we don't want users to know this information, what should we do?

Solution

In the AutoCAD API, there are extension data features called XData and XRecord. Use XData to attach data to objects, but there's a limitation on the data size, which must not exceed 16K bytes per object. On the other hand, XRecord can store up to 2GB of data and can also be associated or not associated with individual objects in AutoCAD.

For example, if we want to store a variable "Title" with the value "Software Engineer" into an AutoCAD object according to the rules of the AutoCAD API, we would do the following:

  • Create a RegAppTableRecord object with the name "Title".
  • Attach data to an object using the following syntax
var typedValues = new List<TypedValue>
{
  new((int) DxfCode.ExtendedDataRegAppName, "Title"), 
  new((int) DxfCode.ExtendedDataAsciiString, "Software Engineer")
} ;
dbObject.XData = new ResultBuffer( typedValues.ToArray() ) ;

But for more complex data like the one depicted below, if we want to attach it to an AutoCAD object, how can we achieve this with just a simple syntax like this?

  • GetData<ComplexModel>(DBObject dbObject)
  • SetData(DBObject dbObject, ComplexModel data)
public class ComplexModel
{
  // Simple Fields
  public string SimpleString { get ; set ; }
  public bool SimpleBoolean { get ; set ; }
  public int SimpleInteger { get ; set ; }
  public double SimpleDouble { get ; set ; }
  public Point3d SimpleStruct { get ; set ; }

  // Array Fields
  public List<string> ArrayString { get ; set ; }
  public BindingList<double> ArrayDouble { get ; set ; }
  public ObservableCollection<Point3d> ArrayStruct { get ; set ; }

  // Mapping Fields
  public Dictionary<bool, Point3d> MappingStruct { get ; set ; }
  public SortedDictionary<string, double> MappingDouble { get ; set ; }

  // Nested Model Fields
  public SimpleModel SimpleSubModel { get ; set ; }
  public List<SimpleModel> ArraySubModel { get ; set ; }
  public Dictionary<string, SimpleModel> MappingSubModel { get ; set ; }
}

public class SimpleModel
{
  public string SimpleString { get ; set ; }
  public Point3d SimpleStruct { get ; set ; } 
}

In the first approach, we can use JsonConvert to serialize an object into a string and deserialize it from a string back into an object. However, this solution has limitations in deserializing struct data types and also imposes restrictions on the length of the string when storing it. To overcome these limitations, we can create reference types as substitutes for struct types for deserialization, then convert the reference type back to the struct type. Additionally, we can split the string into multiple substrings for storage.

The second approach, AutoCAD allows storing byte[], we can use MemoryStream and BinaryFormatter to serialize into byte[] and deserialize byte[] into an object, with this method it will not be possible to perform with AutoCAD types such as Point3d because it cannot add the Serializable attribute for these types.

The third approach, also the one that this post aims to address, involves serializing objects into a Dictionary<string, object>, where the key combines the class name with the property name, and the value is the property's value. Then, we store these keys and values in AutoCAD. We can deserialize these keys and values into objects using reflection. With this method, we can leverage the types allowed for storage in AutoCAD, including nested models and struct data types.

Below are the basic steps to implement this solution:

  • Create the interface IStorageConverter for the purpose of dependency injection.
public interface IStorageConverter
{
  // Attach data to the DBObject
  void SerializeObject( DBObject dbObject, IDataModel dataModel, bool isUseXData = true ) ;
  TDataModel DeserializeObject<TDataModel>( DBObject dbObject, bool isUseXData = true ) where TDataModel : IDataModel, new() ;
  
  // Attach data to the DBDictionary
  void SerializeObject( IDataModel dataModel ) ;
  TDataModel DeserializeObject<TDataModel>() where TDataModel : IDataModel, new() ;
}
  • Implement the interface IStorageConverter, for example, for the case of attaching data to the DBObject.
public void SerializeObject( DBObject dbObject, IDataModel dataModel, bool isUseXData = true )
{
  dbObject = ! isUseXData ? _storageFactory.FindOrCreateDBDictionary( dbObject ) : dbObject ;
  foreach ( var register in SerializeObject( dbObject, dataModel, string.Empty ) )
    SetAttachData( dbObject, register.Key, register.Value ) ;
}
public TDataModel DeserializeObject<TDataModel>( DBObject dbObject, bool isUseXData = true ) where TDataModel : IDataModel, new()
{
  dbObject = ! isUseXData ? _storageFactory.FindOrCreateDBDictionary( dbObject ) : dbObject ;
  var modelType = typeof( TDataModel ) ;
  return (TDataModel) DeserializeObject( dbObject, modelType ) ;
}
private IDictionary<string, IList<TypedValue>> SerializeObject( DBObject dbObject, IDataModel dataModel, string baseName )
{
  var modelType = dataModel.GetType() ;
  var serialize = new Dictionary<string, IList<TypedValue>>() ;

  if ( modelType.GetAttribute<StorageAttribute>() is not { } storageAttribute )
    return serialize ;

  baseName = baseName.Length > 0 ? $"{baseName}{SIGN_JOIN}{modelType.Name}" : $"{storageAttribute.StorageGuid}_{modelType.Name}" ;
  foreach ( var propertyInfo in modelType.GetProperties( BindingFlags.Public | BindingFlags.Instance ) ) {
    if ( propertyInfo.GetAttribute<FieldAttribute>() is null )
      continue ;

    var propertyValue = propertyInfo.GetValue( dataModel ) ;
    if ( IsSimpleField( propertyInfo.PropertyType ) )
      SerializeSimpleField( dbObject, baseName, propertyInfo, propertyValue ).ForEach( x => serialize.Add( x.Key, x.Value ) ) ;
    else if ( IsArrayField( propertyInfo.PropertyType ) )
      SerializeArrayField( dbObject, baseName, propertyInfo, propertyValue ).ForEach( x => serialize.Add( x.Key, x.Value ) ) ;
    else if ( IsMappingField( propertyInfo.PropertyType ) )
      SerializeMappingField( dbObject, baseName, propertyInfo, propertyValue ).ForEach( x => serialize.Add( x.Key, x.Value ) ) ;
  }

  return serialize ;
}
private object DeserializeObject( DBObject dbObject, Type modelType, string baseName = "" )
{
  var data = Activator.CreateInstance( modelType ) ;
  if ( modelType.GetAttribute<StorageAttribute>() is not { } storageAttribute )
    return data ;

  baseName = baseName.Length > 0 ? $"{baseName}{SIGN_JOIN}{modelType.Name}" : $"{storageAttribute.StorageGuid}_{modelType.Name}" ;
  foreach ( var propertyInfo in modelType.GetProperties( BindingFlags. Public | BindingFlags.Instance ) ) {
    if ( propertyInfo.GetAttribute<FieldAttribute>() is null )
      continue ;

    if ( IsSimpleField( propertyInfo.PropertyType ) )
      DeserializeSimpleField( dbObject, baseName, propertyInfo, data ) ;
    else if ( IsArrayField( propertyInfo.PropertyType ) )
      DeserializeArrayField( dbObject, baseName, propertyInfo, data ) ;
    else if ( IsMappingField( propertyInfo.PropertyType ) )
      DeserializeMappingField( dbObject, baseName, propertyInfo, data ) ;
  }

  return data ;
}

Here is an example of how to serialize and deserialize a simple field.

private IDictionary<string, IList<TypedValue>> SerializeSimpleField( DBObject dbObject, string baseName, PropertyInfo propertyInfo, dynamic? propertyValue )
{
  var simpleSerialize = new Dictionary<string, IList<TypedValue>>() ;
  if ( propertyValue is null )
    return simpleSerialize ;

  baseName = $"{baseName}{SIGN_JOIN}{propertyInfo.Name}" ;
  if ( propertyValue is IDataModel dataModel ) {
    foreach ( var serializeData in SerializeObject( dbObject, dataModel, baseName ) )
      simpleSerialize.Add( serializeData.Key, serializeData.Value ) ;
  }
  else if ( GetTypeCode( dbObject, propertyInfo.PropertyType ) is { } typeCode ) {
    var typedValues = AddRegAppNameTypeCode( dbObject, baseName, new List<TypedValue> { new((int) typeCode, propertyValue) } ) ;
    simpleSerialize.Add( baseName, typedValues ) ;
  }

  return simpleSerialize ;
}
private void DeserializeSimpleField( DBObject dbObject, string baseName, PropertyInfo propertyInfo, object data )
{
  if ( ! IsValueType( propertyInfo.PropertyType ) )
    return ;

  baseName = $"{baseName}{SIGN_JOIN}{propertyInfo.Name}" ;
  if ( typeof( IDataModel ).IsAssignableFrom( propertyInfo.PropertyType ) ) {
    var value = DeserializeObject( dbObject, propertyInfo.PropertyType, baseName ) ;
    propertyInfo.SetValue( data, value ) ;
  }
  else {
    if ( GetAttachData( dbObject, baseName ) is not { Length: 1 } typedValues )
      return ;

    var value = ChangeTypeIfPossible( typedValues[ 0 ].Value, propertyInfo.PropertyType ) ;
    propertyInfo.SetValue( data, value ) ;
  }
}
  • After implementing serialization and deserialization of data, proceed to implement the IStorageService interface to get/set data within AutoCAD.
public interface IStorageService
{
  // Attach data to a DBObject
  TDataModel? GetData<TDataModel>( DBObject dbObject, bool isUseXData = true ) where TDataModel : IDataModel, new() ;
  void SetData<TDataModel>( DBObject dbObject, TDataModel data, bool isUseXData = true ) where TDataModel : IDataModel ;
  bool HasData<TDataModel>( DBObject dbObject, bool isUseXData = true ) where TDataModel : IDataModel ;
  void EraseData<TDataModel>( DBObject dbObject, bool isUseXData = true ) where TDataModel : IDataModel ;

  // Attach data to a DBDictionary
  TDataModel? GetData<TDataModel>() where TDataModel : IDataModel, new() ;
  void SetData<TDataModel>( TDataModel data ) where TDataModel : IDataModel ;
  bool HasData<TDataModel>() where TDataModel : IDataModel ;
  void EraseData<TDataModel>() where TDataModel : IDataModel ;
}
  • Here is an example of applying the implementation of getting/setting data within AutoCAD.
using var transaction = _transactionFactory.FindOrCreateTransaction() ;
var dbObject = transaction.GetObject( objectIds[ 0 ], OpenMode.ForWrite ) ;
var newData = new ComplexModel
{
  SimpleString = "AAA.BBB",
  SimpleBoolean = true,
  SimpleStruct = new Point3d( 10, 47, 34.5 ),
  SimpleDouble = 523486.333345,
  SimpleInteger = 125648,
  
  ArrayString = new List<string> { "AAA", "BBB", "CCC" },
  ArrayDouble = new BindingList<double> { 123.3, 456.3, 909.1, 34.2 },
  ArrayStruct = new ObservableCollection<Point3d> { new(1, 2, 3), new(1.2, 2.3, 3.4), new(4, 5, 6) },
  
  MappingStruct = new Dictionary<bool, Point3d>
  {
    { true, new Point3d( 12.3, 3.5, 6.9 ) }, { false, new Point3d( 2, 6, 9 ) }
  },
  MappingDouble = new SortedDictionary<string, double> { { "AAA", 12 }, { "BBB", 4 }, { "CCC", 123 } },
  
  SimpleSubModel = new SimpleModel { SimpleString = "AAA", SimpleStruct = new Point3d( 1, 2, 3 ) },
  ArraySubModel = new List<SimpleModel>
  {
    new() { SimpleString = "AAA", SimpleStruct = new Point3d( 1, 2, 3.2 ) },
    new() { SimpleString = "BBB", SimpleStruct = new Point3d( 6.8, 2, 3 ) }, 
    new() { SimpleString = "CCC", SimpleStruct = new Point3d( 1, 2.6, 3 ) }
  },
  MappingSubModel = new Dictionary<string, SimpleModel>
  {
    { "AAA", new SimpleModel { SimpleString = "AAA", SimpleStruct = new Point3d( 1, 2, 34 ) } }, 
    { "BBB", new SimpleModel { SimpleString = "BBB", SimpleStruct = new Point3d( 1, 2, 0 ) } }
  }
} ;

_storageService.SetData( dbObject, newData ) ;
var oldData = _storageService.GetData<ComplexModel>( dbObject ) ;
transaction.Commit() ;


Conclusion

     With the implementation as above, it makes getting/setting data easier, now solely dependent on the model without the need to consider type codes when attaching data to entities. The data types are diverse, including string, byte, char, bool, double, int, byte[], Point3d, IDataModel (used for nested model classes), facilitating rapid source code development, convenient coding, and cleaner code.

     If you have any questions or suggestions, please create a topic in this link.

C#
AutoCAD
Modified on 23/04/2024
Back
Summary

    Related Blogs

    How to convert text in a link/import DWG file to Revit?

    Created by Nhu.Truong on 23/12/2023
    1017 Views - 0 Like
    The Revit API does not provide any information about Text objects in a link/import DWG file. This post shares a solution that can read Text objects in a link/import DWG and convert them to Revit.

    How to debug without restarting AutoCAD?

    Created by Nhu.Truong on 17/12/2023
    929 Views - 0 Like
    The approach is applicable not only to AutoCAD but also to the majority of software that supports .NET, like Revit or Navisworks, aiming to simplify the add-in development process.

    Developing a Dynamo package using DI pattern

    Created by Nhu.Truong on 15/12/2023
    938 Views - 0 Like
    Developing a Dynamo package to accurately position Soffit Corner elements using Dynamo API, Revit API, and C# with the Dependency Injection pattern.
    Previous Next
    About Us
    logo-beycons
    Technology Makes Construction Better
    EIN 0316964547
    Address
    Thu Duc City, Ho Chi Minh City
    (+84) 33 248 2470
    contact.beycons@gmail.com
    Follow Us
    Terms & Privacy
    © 2022 - 2025 BeyCons Co.,Ltd.
    Developed by Nhu.Truong.