Binary Serialization to XRecord

Binary Serialization to XRecord

Anonymous
Not applicable
5,118 Views
8 Replies
Message 1 of 9

Binary Serialization to XRecord

Anonymous
Not applicable

I have a working XML Serializer which serializes a C# object to an entity in AutoCAD. I'd like to be able to do the same thing but with Binary Serialization for the cases in which XML does not work. So far my serialization method looks like this:

public static void BinarySave(Entity entityToWriteTo, Object objToSerialize, string key = "default")
    {        using (MemoryStream stream = new MemoryStream())
        {
            BinaryFormatter serializer = new BinaryFormatter();            serializer.Serialize(stream, objToSerialize);            stream.Position = 0;


            ResultBuffer data = new ResultBuffer();

            /*Code to get binary serialization into result buffer*/            using (Transaction tr = db.TransactionManager.StartTransaction())            using (DocumentLock docLock = doc.LockDocument())
            {
                if (!entityToWriteTo.IsWriteEnabled)
                {                    entityToWriteTo = tr.GetObject(entityToWriteTo.Id, OpenMode.ForWrite) as Entity;
                }
                if (entityToWriteTo.ExtensionDictionary == ObjectId.Null)
                {                    entityToWriteTo.CreateExtensionDictionary();
                }                using (DBDictionary dict = tr.GetObject(entityToWriteTo.ExtensionDictionary, OpenMode.ForWrite, false) as DBDictionary)
                {
                    Xrecord xrec;
                    if (dict.Contains(key))
                    {                        xrec = tr.GetObject(dict.GetAt(key), OpenMode.ForWrite) as Xrecord;                        xrec.Data = data;
                    }
                    else
                    {                        xrec = new Xrecord();                        xrec.Data = data;                        dict.SetAt(key, xrec);                        tr.AddNewlyCreatedDBObject(xrec, true);
                    }                    xrec.Dispose();
                }                tr.Commit();
            }            data.Dispose();
        }

    }

It's heavily based on my XML Serializer except I have no idea how to get the serialized object into a resultbuffer to be added to the Xrecord of entityToWriteTo.

0 Likes
5,119 Views
8 Replies
Replies (8)
Message 2 of 9

augusto.goncalves
Alumni
Alumni

Hi,

 

If I understand you correctly, first you need to convert the XML data into a array of AutoCAD TypedValue, then pass this ResultBuffer into the XRecord.Data property. That way, it will appear as expected for other AutoCAD apps (e.g. a LISP code).

 

 

TypedValue[] vals = new TypedValue[]

{

  new TypedValue((int)DxfCode.Text, "Some text"), // string

  new TypedValue((int)DxfCode.Real, 3.14),  // double

  new TypedValue((int)DxfCode.Int32, 2015) // integer

};

 

ResultBuffer data = new ResultBuffer(vals);

 

If you don't want to decompose like above, then you can pass the XML as a DxfCode.BinaryChunk, but that will be a single chunk of XML data, not easily readble from other AutoCAD apps...

 

Hope this helps.

Regards,



Augusto Goncalves
Twitter @augustomaia
Autodesk Developer Network
0 Likes
Message 3 of 9

dgorsman
Consultant
Consultant

IMHO, storing information in binary format in XRecords is only justified if there is some security context; as in, it should *never* be readable or modified from any other context.  Under most cases that concern doesn't exist, so the more conventional means of "decomposing" the properties into typed values works very well.  Keeping the process simple (no conversion between binary, XML, etc. in either direction) also means less to create, maintain, or search through when something misbehaves.

----------------------------------
If you are going to fly by the seat of your pants, expect friction burns.
"I don't know" is the beginning of knowledge, not the end.


0 Likes
Message 4 of 9

Anonymous
Not applicable

I have a similar situation. However, I store XML for a complex and potentially very large object hierarchy in a NOD entry. 

 

When a drawing created with my application becomes complex enough, literally megabytes of XML are currently stored in this NOD entry. I do this by splitting the XML string into 1024 character chunks, then storing the chunks as DxfCode.Text ResultBuffer values. 

 

When I began developing my AutoCAD plugin years ago, I read that serializing to binary and storing the binary data in a NOD entry using DxfCode.BinaryChunk would cause drawing validation errors and that deserializing the data after a drawing has been saved and reloaded will fail. Here is the Post where I read this about BinaryChunk, although there were multiple sources for this information at the time, including an AU article, as i recall: http://forums.autodesk.com/t5/net/serialized-custom-object-to-xrecord-can-not-save-dwg/m-p/3368241/h...

 

Quote:

 

"After 2 1/2 weeks of hell, I settled on one conclusion, storing binary chunks on an Xrecord does not work.  The AUDIT just removes all of my xrecords from the file, thus defeating the purpose.  So, I decided to serialize to XML and store XML strings to the xrecord.  They still have to be chunked because there is a 1024 character string limit, but it works.  It works AWESOME!!!  It even handles all custom class object references as well.  You just can't serialize an AutoCAD object onto an AutoCAD object, which makes sense.  Here is a sample of the serialization to xrecord:"

 

The poster then provides working code (although the code he provides is flawed). 

 

Ideally, if it is in fact possible to store a large amount of binary data in many ResutBuffer values of type BinaryChunk, I would serialize to binary, then compress the resulting stream, then store the deflated binary data in a NOD entry. Later i would reverse the process to inflate/deserialize. Will this work, or is the Post I provided above correct? Does this only work in newer versions of AutoCAD? I only provide support for ACAD 2013+.

 

Thanks,

Kevin

Message 5 of 9

joantopo
Mentor
Mentor

Hi.

In my add-on I have a form with a datagridview which the DataSource is a List <T>.

This list can have severals rows and this class has more than 20 fields, which can be boolean, double, string, ObjectId,...,

 

This data is about alignments, which AutoCAD entity recommend me to save the data to that object?

 

Is there any example with List and Extension Dictionary?

 

Thanks in advance.

Autocad C3D 2019 SP3, 2020 & 2021
Intel I9 9900K with frontal watercooler alphacool eisbaer 360 (original fans mounted in pull)- 3 fans Corsair 120 ML PRO in push.
MOBO Gygabyte Z390 Aorus Master- Corsair RGB Vengeance 64GB RAM (4x16) CL16
Nvidia Quadro RTX 4000
Samsung 970 EVO PLUS 1TB (unit C). Samsung 970 PRO 512GB (for data)
Power Supply: Corsair TX850M PLUS


Descubre mi programa VisorNET para Civil 3D:
https://apps.autodesk.com/CIV3D/es/Detail/Index?id=appstore.exchange.autodesk.com%3avisornet_windows32and64%3aes
0 Likes
Message 6 of 9

joantopo
Mentor
Mentor

Woow, this pdf is really good for beginners:

 

https://www.google.es/url?sa=t&source=web&rct=j&url=http://aucache.autodesk.com/au2012/sessionsFiles...

Autocad C3D 2019 SP3, 2020 & 2021
Intel I9 9900K with frontal watercooler alphacool eisbaer 360 (original fans mounted in pull)- 3 fans Corsair 120 ML PRO in push.
MOBO Gygabyte Z390 Aorus Master- Corsair RGB Vengeance 64GB RAM (4x16) CL16
Nvidia Quadro RTX 4000
Samsung 970 EVO PLUS 1TB (unit C). Samsung 970 PRO 512GB (for data)
Power Supply: Corsair TX850M PLUS


Descubre mi programa VisorNET para Civil 3D:
https://apps.autodesk.com/CIV3D/es/Detail/Index?id=appstore.exchange.autodesk.com%3avisornet_windows32and64%3aes
0 Likes
Message 7 of 9

Anonymous
Not applicable

If you want to embed serialized .net object data in an Entity, use the Entity's ExtentionDictionary. I tend to serialize an entire graph of .net objects, and store it in one location; thus I use the Drawing's NOD for this purpose.

 

One issue you will run into is that ObjectID is not serializable. I recommend creating a serializable version of your class. I employ the Memento Design Pattern (http://www.dofactory.com/net/memento-design-pattern). Thus, you would serialize a List of this serializable version of your class. Instead of the ObjectID, use ObjectID.Handle.Value, which is a value of type Long. This value is unique to a drawing. 

 

When a drawing is opened and you detect your plugin data in the drawing database, use the following function to retrieve the ObjectID using the handle you have stored in your serializable version of your class:

 

    Public Function GetObjectIDByHandle(Doc As Autodesk.AutoCAD.ApplicationServices.Document, Handle As Long) As Autodesk.AutoCAD.DatabaseServices.ObjectId Implements EstimateSystem.IDrawingObjectFinder.GetObjectIDByHandle
        Dim han As New Handle(Handle)
        Return Doc.Database.GetObjectId(False, han, 0)
    End Function

Kevin

0 Likes
Message 8 of 9

Anonymous
Not applicable

Please note that I have found embedding binary data in ExtensionDictionaries to be just as reliable as storing string data. I have actually taken to compressing the binary data stream stored in the drawing database to minimize the impact and drawing file size, but I embed large amounts of data, so maybe this added step is not needed for your application.

 

Here are two methods from a class I use to Save/Load my object graph from the drawing's NOD (maybe you can rework the code to be useful to you:

 

    Private Sub SaveEstimateDataBinary(Doc As Document)
        Dim cacheData As Byte()
        'serialze block state cache object to binary
        Using ms As New MemoryStream()
            Dim ser As New BinaryFormatter()
            Dim dwgModelMem As DrawingModelMemento = _Drawing.GetMemento()
            ser.Serialize(ms, dwgModelMem)
            cacheData = ms.ToArray()
        End Using
        'compress stream
        Dim compressedDwgData As Byte()
        Using destMS As New MemoryStream()
            Using gz As New GZipStream(destMS, CompressionMode.Compress, True)
                gz.Write(cacheData, 0, cacheData.Length)
            End Using
            compressedDwgData = destMS.ToArray()
        End Using
        'max BinaryChunk length is 255 bytes
        Using buff As New ResultBuffer()
            Dim position As Integer = 0
            Dim remaining As Integer = compressedDwgData.Length
            While remaining > 0
                If remaining >= 255 Then
                    Dim chunk(254) As Byte
                    Buffer.BlockCopy(compressedDwgData, position, chunk, 0, 255)
                    buff.Add(New TypedValue(DxfCode.BinaryChunk, chunk))
                    remaining -= 255
                    position += 255
                Else
                    Dim chunk(remaining - 1) As Byte
                    Buffer.BlockCopy(compressedDwgData, position, chunk, 0, remaining)
                    buff.Add(New TypedValue(DxfCode.BinaryChunk, chunk))
                    remaining = 0
                End If
            End While
            Using tr As Transaction = Doc.TransactionManager.StartTransaction()
                Try
                    Using nod As DBDictionary = tr.GetObject(Doc.Database.NamedObjectsDictionaryId, OpenMode.ForWrite, False)
                        'write object data to drawing db
                        If nod.Contains(DRAWING_NOD_KEY) Then
                            'update existing record
                            Using xrec As Xrecord = tr.GetObject(nod.GetAt(DRAWING_NOD_KEY), OpenMode.ForWrite)
                                xrec.Data = buff
                            End Using
                        Else
                            'create new record
                            Using xrec As New Xrecord()
                                xrec.Data = buff
                                nod.SetAt(DRAWING_NOD_KEY, xrec)
                                tr.AddNewlyCreatedDBObject(xrec, True)
                            End Using
                        End If
                    End Using
                    tr.Commit()
                Catch ex As System.Exception
                    'TODO: Handle error
                    tr.Abort()
                    Throw ex
                End Try
            End Using
        End Using
    End Sub

    Private Function LoadEstimateDataBinary(Doc As Document) As DrawingModelMemento
        Dim dwgModelMem As DrawingModelMemento = Nothing
        Using objStream As New MemoryStream()
            Using sr As New BinaryWriter(objStream)
                Using tr As Transaction = Doc.TransactionManager.StartTransaction()
                    Try
                        Using nod As DBDictionary = tr.GetObject(Doc.Database.NamedObjectsDictionaryId, OpenMode.ForRead, False)
                            If Not nod.Contains(DRAWING_NOD_KEY) Then Throw New ApplicationException("NOD does not contain key " + DRAWING_NOD_KEY)
                            Using xrec As Xrecord = tr.GetObject(nod.GetAt(DRAWING_NOD_KEY), OpenMode.ForRead)
                                Using buff As ResultBuffer = xrec.Data
                                    If buff Is Nothing Then Throw New ApplicationException("NOD entry with key " + DRAWING_NOD_KEY + " contains no data")
                                    Dim tvals As TypedValue() = buff.AsArray()
                                    Dim chunk As Byte()
                                    If tvals.Length = 0 Then Throw New ApplicationException("NOD entry with key " + DRAWING_NOD_KEY + " contains too few entries")
                                    If tvals(0).TypeCode <> DxfCode.BinaryChunk Then Throw New ApplicationException("NOD entry with key " + DRAWING_NOD_KEY + " contains header entry with incorrect data type")
                                    For x As Integer = 0 To tvals.Length - 1
                                        chunk = tvals(x).Value
                                        sr.Write(chunk)
                                    Next
                                    sr.Flush()
                                End Using
                            End Using
                        End Using
                        Dim compressedData As Byte() = objStream.ToArray()
                        Using sourceMS As New MemoryStream(compressedData)
                            Using destMS As New MemoryStream()
                                Using gz As New GZipStream(sourceMS, CompressionMode.Decompress, True)
                                    sourceMS.Position = 0
                                    gz.CopyTo(destMS)
                                End Using
                                destMS.Position = 0
                                Dim ser As New BinaryFormatter()
                                dwgModelMem = ser.Deserialize(destMS)
                            End Using
                        End Using
                        tr.Commit()
                    Catch ex As System.Exception
                        tr.Abort()
                        Throw ex
                    End Try
                End Using
            End Using
        End Using
        Return dwgModelMem
    End Function

I subscribe to the Document.Database class's BeginSave (Autodesk.AutoCAD.ApplicationServices.Application.DocumentManager.MdiActiveDocument.Database.BeginSave) event, writing an updated version of my object graph to the drawings NOD each time the drawing is saved. 

 

To support AutoSaves, you need to subscribe to the DocumentLockModeChanged event of the DocumentManager class. The DocumentLockModeChangedEventArgs object passed as an argument to your handler for this event provides the GlobalCommandName property. When this property is set to "AUTO_SAVE", you should serialize your class data and write it to the ExtensionDictionary. Note that you cannot lock the drawing when code is invoked from this event handler in this context. So you need to alter your code so that you do not attempt to lock the Document (you will need to lock the Document when invoking from the BeginSave event handler).

 

Oh, and use the DocumentManager.DocumentCreated event to determine if the drawing which has been opened contains you plugin's data. If it does, you want to load you embedded data and initialize your application UI. Subscribe to the DocumentManager events in the Initialize method of the IExtensionApplication interface in the Autodesk.AutoCAD.Runtime namespace. Use the Terminate method of that interface to clean up after your application. 

 

Take the information I've provided with a grain of salt; there may be a better way to accomplish some of these requirements. I do not claim to be an expert, but the methods described have proved reliable over time and testing for me.

 

Kevin

Message 9 of 9

Anonymous
Not applicable

One more thing. Use the events DocumentToBeActivated, DocumentToBeDeactivated of the DocumentManager class to support multiple drawings containing your plugin data being opened at once when the user changes the active drawing. Use DocumentManager.DocumentToBeDestroyed to handle a drawing being closed. You may want to prompt the user to save the drawing when a drawing is closed. AutoCAD will not detect changes to data in extension dictionaries and prompt the user to save, in my experience. But whether or not this is necessary is specific to your application. 

 

Kevin

0 Likes