Detail Blog
Nhu.Truong
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.